Каков самый питонический способ работы со списком вложенных словарей?

Каков самый Pythonic способ идентифицировать различные типы вложенных словарей, которые возвращает API, чтобы можно было применить правильный тип синтаксического анализа?

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

Еще в нескольких абзацах я опишу проблему, но вы, возможно, сможете погрузиться в примеры словарей и мой код ниже и увидеть мою проблему, заключающуюся в невозможности определить один из трех типов словарей за один проход. Вложенные словари не имеют одинаковых структур, и мой код полон trys и, как мне кажется, избыточных for циклов.

У меня есть функция для обработки трех типов вложенных словарей. topics_data (используется ниже) — это кадр данных Pandas, а столбец vid — это имя столбца в topics_data, которое содержит вложенный словарь. Иногда объект в ячейке vid равен None, если сообщение, которое я читаю, не является видеопостом.

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

Ниже приведены примеры трех различных типов вложенных словарей, которые я получаю, и уродливого кода, который я сейчас настроил для их обработки. Код у меня работает, но он уродлив. Пожалуйста, копайте и смотрите.

Вложенные словари...

Вложенный словарь 1

{'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}}

Вложенный словарь 3

{'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, а столбец 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  

ОБНОВЛЕНИЕ
Вот ответ на вопрос с помощью Нила. Просто добавлено, чтобы сделать вопросы и ответы более понятными для будущих поколений.
Все по-прежнему заключено в 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
Что-то вроде schema позволит вам одновременно анализировать и проверять.   -  person Kyle G    schedule 05.10.2020
comment
Ваша функция download_vid также стала чем-то вроде божественной функции. Я бы написал еще три функции для обработки фактической загрузки и запросов и всего такого, и просто позволил бы этой функции сосредоточиться на доступе к данным из запроса. Было бы меньше путаницы   -  person Neil    schedule 05.10.2020


Ответы (1)


Обновление: Реализован текст ОП, связанный с неуникальным поиском. Добавил абзац, чтобы описать, как это сделать.

Если вы обнаружите, что просматриваете список словарей несколько раз, чтобы выполнить поиск, реструктурируйте список в словарь, чтобы поиск был ключевым. Например это:

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")

В этом IMO нет ничего Pythonic, хотя, если 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 трижды просматривал темы_data['vid']. Я сделал это дважды.

  • Что еще более важно, если добавляются дополнительные темы, я все равно делаю это только дважды. ОП придется снова зацикливаться.

  • Нет обработки исключений.

  • Каждая группа объектов теперь проиндексирована. Таким образом, 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
У меня возникают проблемы с проверкой да или нет и передачей первого ключа во вложенном диске без ошибки (если это не 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