Кой е най-Pythonic начинът за работа със списък от вложени речници?

Кой е най-Pythonic начинът за идентифициране на различните вложени типове речници, които API връща, така че да може да се приложи правилният тип анализ?

Извършвам API извиквания от Reddit, за да получа URL адреси и получавам вложени речници с различни имена на ключове и различни структури на вложени речници.
Изтеглям URL адресите, от които се нуждая, добре, но имам нужда от по-питоничен начин за идентифициране на различните имена на ключове и различни структури на вложените речници, защото if изразите, които опитах в един for цикъл, се сблъскват с грешки, защото ако речникът не съдържа ключа, получавам NoneType грешка само от израза if, питайки дали споменатият ключ е в речникът.

В още няколко параграфа описвам проблема, но може да успеете да се потопите в примерите за речници и моя код по-долу и да видите моя проблем с невъзможността да идентифицирам един от трите типа речници с едно преминаване. Вложените речници нямат еднакви структури и моят код е пълен с trys и според мен излишни for цикли.

Имам функция за обработка на три типа вложени речници. topics_data (използван по-долу) е Pandas Dataframe, а колоната vid е име на колона в topics_data, което съдържа вложен речник. Понякога обектът в клетката vid е None, ако публикацията, която чета, не е видео публикация.

Има само три основни типа вложени речници, които API връща (ако не None). Най-големият ми проблем е да идентифицирам името на първия ключ, без да получа грешка NoneType, ако се опитам с оператор if да уловя вложен речник с ключа reddit_video, който вместо това започва с друг ключ, като например oembed. Поради този проблем преглеждам списъка с вложени речници три пъти за всеки от трите типа вложени речници. Искам да мога да прегледам списъка с вложени речници веднъж и всеки тип вложен речник да бъде идентифициран и обработен с едно преминаване.

По-долу са примери за трите различни типа вложени речници, които получавам, и грозния код, който съм настроил сега, за да се справя с тях. Кодът, който имам, работи, но е грозен. Моля, разровете и вижте.

Вложените речници...

Вложен речник първи

{'reddit_video': {'fallback_url': 'https://v.redd.it/te7wsphl85121/DASH_2_4_M?source=fallback',
  'height': 480,
  'width': 480,
  'scrubber_media_url': 'https://v.redd.it/te7wsphl85121/DASH_600_K',
  'dash_url': 'https://v.redd.it/te7wsphl85121/DASHPlaylist.mpd?a=1604490293%2CYmQzNDllMmQ4MDVhMGZhODMyYmIxNDc4NTZmYWNlNzE2Nzc3ZGJjMmMzZGJjMmYxMjRiMjJiNDU4NGEzYzI4Yg%3D%3D&v=1&f=sd',
  'duration': 17,
  'hls_url': 'https://v.redd.it/te7wsphl85121/HLSPlaylist.m3u8?a=1604490293%2COTg2YmIxZmVmZGNlYTVjMmFiYjhkMzk5NDRlNWI0ZTY4OGE1NzgxNzUyMDhkYjFiNWYzN2IxYWNkZjM3ZDU2YQ%3D%3D&v=1&f=sd',
  'is_gif': False,
  'transcoding_status': 'completed'}}

Вложен речник втори

{'type': 'gfycat.com',
 'oembed': {'provider_url': 'https://gfycat.com',
  'description': 'Hi! We use cookies and similar technologies ("cookies"), including third-party cookies, on this website to help operate and improve your experience on our site, monitor our site performance, and for advertising purposes. By clicking "Accept Cookies" below, you are giving us consent to use cookies (except consent is not required for cookies necessary to run our site).',
  'title': 'Protestors in Hong Kong are cutting down facial recognition towers.',
  'type': 'video',
  'author_name': 'Gfycat',
  'height': 600,
  'width': 600,
  'html': '<iframe class="embedly-embed" src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fedibleunrulyargentineruddyduck&display_name=Gfycat&url=https%3A%2F%2Fgfycat.com%2Fedibleunrulyargentineruddyduck-hong-kong-protest&image=https%3A%2F%2Fthumbs.gfycat.com%2FEdibleUnrulyArgentineruddyduck-size_restricted.gif&key=ed8fa8699ce04833838e66ce79ba05f1&type=text%2Fhtml&schema=gfycat" width="600" height="600" scrolling="no" title="Gfycat embed" frameborder="0" allow="autoplay; fullscreen" allowfullscreen="true"></iframe>',
  'thumbnail_width': 280,
  'version': '1.0',
  'provider_name': 'Gfycat',
  'thumbnail_url': 'https://thumbs.gfycat.com/EdibleUnrulyArgentineruddyduck-size_restricted.gif',
  'thumbnail_height': 280}}

Вложен речник трети

{'oembed': {'provider_url': 'https://gfycat.com',
  'description': 'Hi! We use cookies and similar technologies ("cookies"), including third-party cookies, on this website to help operate and improve your experience on our site, monitor our site performance, and for advertising purposes. By clicking "Accept Cookies" below, you are giving us consent to use cookies (except consent is not required for cookies necessary to run our site).',
  'title': 'STRAYA! Ski-roos. ???????????? ???? Stephan Grenfell for Australian Geographic',
  'author_name': 'Gfycat',
  'height': 338,
  'width': 600,
  'html': '<iframe class="embedly-embed" src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fgfycat.com%2Fifr%2Fhairyvibrantamericanratsnake&display_name=Gfycat&url=https%3A%2F%2Fgfycat.com%2Fhairyvibrantamericanratsnake-snow-kangaroos&image=https%3A%2F%2Fthumbs.gfycat.com%2FHairyVibrantAmericanratsnake-size_restricted.gif&key=ed8fa8699ce04833838e66ce79ba05f1&type=text%2Fhtml&schema=gfycat" width="600" height="338" scrolling="no" title="Gfycat embed" frameborder="0" allow="autoplay; fullscreen" allowfullscreen="true"></iframe>',
  'thumbnail_width': 444,
  'version': '1.0',
  'provider_name': 'Gfycat',
  'thumbnail_url': 'https://thumbs.gfycat.com/HairyVibrantAmericanratsnake-size_restricted.gif',
  'type': 'video',
  'thumbnail_height': 250},
 'type': 'gfycat.com'}  

Моята функция е да обработвам тези три типа вложени речници. topics_data е Pandas Dataframe и колоната vid е име на колона в topics_data, която съдържа вложен речник, или е None.

def download_vid(topics_data, ydl_opts):
    for i in topics_data['vid']:
        try:
            if i['reddit_video']:
                B = i['reddit_video']['fallback_url']
                with youtube_dl.YoutubeDL(ydl_opts) as ydl:
                    ydl.download([B])

                print(B)
        except:
            pass
    for n, i in enumerate(topics_data['vid']):
        try:
            if i['type'] == 'gfycat.com':
                C = topics_data.loc[n]['vid']['oembed']['thumbnail_url'].split('/')[-1:][0].split('-')[0]
                C = 'https://giant.gfycat.com/'+ C +'.mp4'
                sub = str(topics_data.loc[n]['subreddit']).lower()
                urllib.request.urlretrieve(C,
                                           '/media/iii/Q2/tor/Reddit/Subs/'+sub+'/'+C.split('/')[-1:][0])

                print(C)
        except:
            pass
    for i in topics_data['vid']:
        try:
            if i['oembed']['thumbnail_url']:
                D = topics_data.loc[n]['vid']['oembed']['thumbnail_url'].split('/')[-1:][0].split('-')[0]
                D = 'https://giant.gfycat.com/'+ D +'.mp4'
                sub = str(topics_data.loc[n]['subreddit']).lower()
            urllib.request.urlretrieve(D, '/media/iii/Q2/tor/Reddit/Subs/'+sub+'/'+D.split('/')[-1:][0])
                print(D)
        except:
            pass  

След като написах този код, видях, че изразите if са излишни, защото или ще try и ще успее да анализира topics_data.loc[n]['vid']['oembed'], ако е възможно или не във всеки try блок.
Не се затъвайте в това как е вложеният речник анализиран, защото това всъщност не е моят проблем. Проблемът ми е главно с това как да идентифицирам кой тип вложен речник има итераторът. Предполагам, че всичко това може да се обработи в рамките на един for цикъл вместо три.
Един последен проблем е, че понякога има четвърти, пети или шести тип речник, който не се интересувам от анализиране, защото са твърде редки.

Този последен блок код вероятно не е задължителен, но го добавям само за да направя въпроса пълен. Моята функция, която идентифицира и анализира речниците, също приема параметрите за youtube-dl.

def my_hook(d):
    if d['status'] == 'finished':
        print('Done downloading, now converting ...')

def yt_dl_opts(topics_data):
    ydl_opts = {
        'format': 'bestvideo+bestaudio/37/22/18/best',
        'merge': 'mp4',
        'noplaylist' : True,        
        'progress_hooks': [my_hook],
        'outtmpl' : '/media/iii/Q2/tor/Reddit/Subs/'+ str(topics_data.loc[0]['subreddit']).lower()+'/%(id)s'
    }
    return ydl_opts  

АКТУАЛИЗАЦИЯ
Ето отговора на въпроса с помощта на Нийл. Просто добавям, за да направя Q и A по-ясни за бъдещите поколения.
Всичко все още е обвито в try: except: pass, защото все още има няколко произволни и винаги нови върнати dic структури. Пиша цикъл, за да преброя видео резултатите, които не са None, и да преброя всички видеоклипове, успешно изтеглени с os.walk.

def download_vid(topics_data, ydl_opts):
    y_base = 'https://www.youtube.com/watch?v='
    for n, i in enumerate(topics_data['vid']):
        try:
            if 'type' in i:
                if 'youtube.com' in i[n]['type']:
                    print('This is a Youtube Video')
                    A = i['oembed']['html'].split('embed/')[1].split('?')[0]
                    with youtube_dl.YoutubeDL(ydl_opts) as ydl:
                        ydl.download([A])
                    print(y_base+A)

            if 'reddit_video' in i:
                print('This is a reddit_video Video')
                B = i['reddit_video']['fallback_url']
                with youtube_dl.YoutubeDL(ydl_opts) as ydl:
                    ydl.download([B])
                print(B)

            if 'type' in i:
                if 'gfycat.com' in i[n]['type']:
                    print('This is a type, gfycat Video')
                    C = topics_data.loc[n]['vid']['oembed']['thumbnail_url'].split('/')[-1:][0].split('-')[0]
                    C = 'https://giant.gfycat.com/'+ C +'.mp4'
                    sub = str(topics_data.loc[n]['subreddit']).lower()
                    urllib.request.urlretrieve(C,
                                       '/media/iii/Q2/tor/Reddit/Subs/'+sub+'/'+C.split('/')[-1:][0])
                print(C)

            if 'oembed' in i:
                print('This is a oembed, gfycat Video')
                D = topics_data.loc[n]['vid']['oembed']['thumbnail_url'].split('/')[-1:][0].split('-')[0]
                D = 'https://giant.gfycat.com/'+ D +'.mp4'
                sub = str(topics_data.loc[n]['subreddit']).lower()
                urllib.request.urlretrieve(C, '/media/iii/Q2/tor/Reddit/Subs/'+sub+'/'+D.split('/')[-1:][0])
                print(D)
        except:
            pass

person Renoldus    schedule 05.10.2020    source източник
comment
Нещо като схема ще ви позволи както да анализирате, така и да валидирате наведнъж.   -  person Kyle G    schedule 05.10.2020
comment
Вашата функция download_vid също се е превърнала в божествена функция. Бих написал още три функции, които да обработват действителното изтегляне и заявки и всичко това, и просто да оставя тази функция да се фокусира върху достъпа до данните от заявката. Би било по-малко объркващо   -  person Neil    schedule 05.10.2020


Отговори (1)


Актуализация: Разбрах, че текстът на OP се занимава с неуникално търсене. Добавен е параграф, който описва как да направите това.

Ако установите, че преминавате през списък с речници няколко пъти, за да извършите търсене, преструктурирайте списъка в речник, така че търсенето да е ключ. Например това:

a = [{"id": 1, "value": "foo"}, {"id": 2, "value": "bar"}]
for item in a:
    if item["id"] == 1:
        print(item["value"])

Може да стане това:

a = [{"id": 1, "value": "foo"}, {"id": 2, "value": "bar"}]
a = {item["id"]: item for item in a} # index by lookup field

print(a[1]["value"]) # no loop
... # Now we can continue to loopup by id eg a[2] without a loop

Ако това е неуникално търсене, можете да направите подобно:

indexed = {}
a = [{"category": 1, "value": "foo"}, {"category": 2, "value": "bar"}, {"category": 1, "value": "baz"}]
for item in a: # This loop only has to be executed once
    if indexed.get(item["category"], None) is not None:
        indexed[item["category"]].append(item)
    else:
        indexed[item["category"]] = [item]

# Now we can do:
all_category_1_data = indexed[1]
all_category_2_data = indexed[2]

Ако получите грешка при индексиране, използвайте индексиране на речника по подразбиране, за да се справите по-лесно

if a.get(1, None) is not None:
    print(a[1]["value"])
else:
    print("1 was not in the dictionary")

Няма нищо Pythonic в този IMO, но ако API връща списъци, които трябва да превъртите, това вероятно е просто лошо проектиран API

Актуализация: Добре, ще се опитам да коригирам кода ви:

def download_vid(topics_data, ydl_opts):
    indexed_data = {'reddit': [], 'gfycat': [], 'thumbnail': []}

    for item in topics_data['vid']:
        if item.get('reddit_video', None) is not None:
            indexed_data['reddit'].append(item)
        elif item.get('type', None) == "gfycat.com":
            indexed_data['gfycat'].append(item)
        elif item.get('oembed', None) is not None:
            if item['oembed'].get('thumbnail_url', None) is not None:
                indexed_data['thumbnail'].append(item)

    for k, v in indexed_data.items():
        assert k in ('reddit_video', 'gfycat', 'thumbnail')
        if k == 'reddit_video':
            B = v['reddit_video']['fallback_rul']
            ...
        elif k == 'gfycat':
            C = v['oembed']['thumbnail_url']
            ...
        elif k == 'thumbnail':
            D = v['oembed']['thumbnail_url']
            ...

Само в случай, че не е ясно защо това е по-добре:

  • OP премина през topics_data['vid'] три пъти. Направих го два пъти.

  • По-важното е, че ако се добавят повече теми, пак правя само два пъти. OP ще трябва да зацикли отново.

  • Без обработка на изключения.

  • Всяка група от обекти вече е индексирана. Така че OP може да направи, например indexed_data['gfycat'], за да получи всички тези обекти, ако е необходимо, и това е търсене в хеш таблица, така че е бързо

person Neil    schedule 05.10.2020
comment
А, не трябваше да го правя в първия пример. Но това НЕ Е всичко, което направих. - person Neil; 05.10.2020
comment
Поправено е, че е печатна грешка. вижте сега @KennyOstrom - person Neil; 05.10.2020
comment
Благодаря. (това съобщение ще се самоунищожи) - person Kenny Ostrom; 05.10.2020
comment
Имам проблем с проверката на да или не и преминаването през първия ключ във вложения dic без грешка (ако не е None). if, is not None работи добре, ако е или не е, но все още имам проблеми с идентифицирането на конкретен ключ като oembed (от OP). Когато се опитвам да проверя дали е там в блок, който може да го анализира, получавам грешка, ако oembed не е първият ключ. Оценявам отговора, но вашите дискове имат един и същ първи ключ за всички примери. - person Renoldus; 05.10.2020
comment
Колебая се да редактирам кода ви. Чувствам, че това може да е по-добре във форума за преглед на код. Но и аз не знам какво казваш сега. Ще покажа това във вашия код, ако смятате да останете активни по този въпрос. - person Neil; 05.10.2020
comment
Сега разбирам въпроса ви. По принцип, накратко е, че ако не ми казахте, че тези три json идват от един и същ API, нямаше да се досетя. Те нямат една и съща схема. Което означава, че е необходима малко ръчна намеса и вероятно няма елегантно решение. Решението, което публикувах в редактирания отговор по-горе, е по-добро, но все пак малко грозно. - person Neil; 05.10.2020
comment
хубаво. Точно преди да заредя компютъра си, мислех, че решението е да използвам if и in, за да открия с коя схема имам работа. Почти съм сигурен, че мога да получа всичко с една итерация в списъка с дискове с него. Това наистина е, което API връща. Може би го правят по този начин, за да разубедят хората лесно да изтриват Reddit. За мое щастие youtube-dl обединява аудиото и видеото. Нямаше да мога да разбера как да получа и двете без него. - person Renoldus; 05.10.2020
comment
Погледнато назад, трябваше да задам този въпрос, като направя списък от три прости речника, всеки от които започва с различен ключ и цикъл, опитващ се да идентифицира всеки един. Съгласен съм, че наистина замъглих нещата, като поставих нещата тук, но предполагам, че някак си се получи. - person Renoldus; 05.10.2020