Знайти всі входження ключа в вкладені словники та списки


87

У мене є такий словник:

{ "id" : "abcde",
  "key1" : "blah",
  "key2" : "blah blah",
  "nestedlist" : [ 
    { "id" : "qwerty",
      "nestednestedlist" : [ 
        { "id" : "xyz",
          "keyA" : "blah blah blah" },
        { "id" : "fghi",
          "keyZ" : "blah blah blah" }],
      "anothernestednestedlist" : [ 
        { "id" : "asdf",
          "keyQ" : "blah blah" },
        { "id" : "yuiop",
          "keyW" : "blah" }] } ] } 

В основному це словник із вкладеними списками, словниками та рядками довільної глибини.

Який найкращий спосіб пройти це шляхом вилучення значень кожного ключа "id"? Я хочу досягти еквівалента запиту XPath на зразок "// id". Значення "id" завжди є рядком.

Отже, з мого прикладу результат, який мені потрібен, в основному:

["abcde", "qwerty", "xyz", "fghi", "asdf", "yuiop"]

Порядок не важливий.



Більшість ваших рішень підірвуться, якщо ми передамо їх Noneяк вхідні дані. Ви дбаєте про міцність? (оскільки це зараз використовується як канонічне питання)
smci

Відповіді:


74

Я знайшов це запитання дуже цікавим, оскільки воно пропонує кілька різних рішень для однієї і тієї ж проблеми. Я взяв усі ці функції і протестував їх за допомогою складного об'єкта словника. Мені довелося вилучити дві функції з тесту, оскільки їм довелося багато невдалих результатів, і вони не підтримували повернення списків або диктів як значень, що я вважаю суттєвим, оскільки функція повинна бути підготовлена ​​майже до будь-яких даних.

Отже, я прокачав інші функції через 100 000 ітерацій через timeitмодуль, і результат прийшов до такого результату:

0.11 usec/pass on gen_dict_extract(k,o)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
6.03 usec/pass on find_all_items(k,o)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0.15 usec/pass on findkeys(k,o)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1.79 usec/pass on get_recursively(k,o)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0.14 usec/pass on find(k,o)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
0.36 usec/pass on dict_extract(k,o)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Усі функції мали однакову стрілку для пошуку ('ведення журналу') та однаковий об'єкт словника, який побудований так:

o = { 'temparature': '50', 
      'logging': {
        'handlers': {
          'console': {
            'formatter': 'simple', 
            'class': 'logging.StreamHandler', 
            'stream': 'ext://sys.stdout', 
            'level': 'DEBUG'
          }
        },
        'loggers': {
          'simpleExample': {
            'handlers': ['console'], 
            'propagate': 'no', 
            'level': 'INFO'
          },
         'root': {
           'handlers': ['console'], 
           'level': 'DEBUG'
         }
       }, 
       'version': '1', 
       'formatters': {
         'simple': {
           'datefmt': "'%Y-%m-%d %H:%M:%S'", 
           'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
         }
       }
     }, 
     'treatment': {'second': 5, 'last': 4, 'first': 4},   
     'treatment_plan': [[4, 5, 4], [4, 5, 4], [5, 5, 5]]
}

Усі функції давали однаковий результат, але різниця в часі надзвичайна! Функція gen_dict_extract(k,o)- це моя функція, адаптована з функцій тут, насправді вона майже схожа на findфункцію від Alfe, з основною відмінністю, що я перевіряю, чи має даний об'єкт функцію iteritems, на випадок, якщо рядки передаються під час рекурсії:

def gen_dict_extract(key, var):
    if hasattr(var,'iteritems'):
        for k, v in var.iteritems():
            if k == key:
                yield v
            if isinstance(v, dict):
                for result in gen_dict_extract(key, v):
                    yield result
            elif isinstance(v, list):
                for d in v:
                    for result in gen_dict_extract(key, d):
                        yield result

Отже, цей варіант є найшвидшим і найбезпечнішим із функцій тут. І find_all_itemsнеймовірно повільний і далеко другий найповільніший, get_recursivleyтоді як решта, крім dict_extract, знаходиться близько один до одного. Функції funі keyHoleтільки працювати , якщо ви шукаєте рядки.

Цікавий аспект навчання тут :)


1
Якщо ви хочете шукати кілька ключів, як це робив я, просто: (1) змініть на gen_dict_extract(keys, var)(2) поставте for key in keys:як рядок 2 і відступите до решти (3) змініть перший вихід наyield {key: v}
Бруно Броноскі

6
Ви порівнюєте яблука з апельсинами. Запуск функції, яка повертає генератор, займає менше часу, ніж запуск функції, яка повертає готовий результат. Спробуйте timeit на next(functionname(k, o)всіх рішеннях генератора.
kaleissin

6
hasattr(var, 'items')для python3
gobrewers14

1
Ви розглядали можливість вилучити if hasattrдеталь для версії, використовуючи tryдля вилучення виняток у випадку невдалого виклику (див. Pastebin.com/ZXvVtV0g щодо можливої ​​реалізації)? Це призведе до зменшення подвоєного пошуку атрибута iteritems(один раз hasattr()і один раз для виклику) і, отже, скоротить час виконання (що здається вам важливим). Однак не зробив жодних орієнтирів.
Alfe

2
Для тих, хто відвідує цю сторінку зараз, коли Python 3 взяв на себе владу, пам’ятайте, що iteritemsце стало items.
Майк Вільямсон,

46
d = { "id" : "abcde",
    "key1" : "blah",
    "key2" : "blah blah",
    "nestedlist" : [ 
    { "id" : "qwerty",
        "nestednestedlist" : [ 
        { "id" : "xyz", "keyA" : "blah blah blah" },
        { "id" : "fghi", "keyZ" : "blah blah blah" }],
        "anothernestednestedlist" : [ 
        { "id" : "asdf", "keyQ" : "blah blah" },
        { "id" : "yuiop", "keyW" : "blah" }] } ] } 


def fun(d):
    if 'id' in d:
        yield d['id']
    for k in d:
        if isinstance(d[k], list):
            for i in d[k]:
                for j in fun(i):
                    yield j

>>> list(fun(d))
['abcde', 'qwerty', 'xyz', 'fghi', 'asdf', 'yuiop']

Єдине, що я б змінив, це for k in dна for k,value in d.items()наступне використання valueзамість d[k].
ovgolovin

Дякую, це чудово працює. Потрібні дуже незначні зміни, оскільки мої списки можуть містити рядки, а також дикти (про що я не згадував), але в іншому випадку ідеальні.
Метт Суейн,

1
Це відповідає дуже вузькому випадку, ви зобов'язані собі розглянути відповідь від "програмного забезпечення hexerei"gen_dict_extract
Бруно Броноскі,

Я отримав помилку "TypeError: аргумент типу 'NoneType' не є ітерабельним"
xiaoshir,

2
Здається, це рішення не підтримує списки
Alex R

23
d = { "id" : "abcde",
    "key1" : "blah",
    "key2" : "blah blah",
    "nestedlist" : [
    { "id" : "qwerty",
        "nestednestedlist" : [
        { "id" : "xyz", "keyA" : "blah blah blah" },
        { "id" : "fghi", "keyZ" : "blah blah blah" }],
        "anothernestednestedlist" : [
        { "id" : "asdf", "keyQ" : "blah blah" },
        { "id" : "yuiop", "keyW" : "blah" }] } ] }


def findkeys(node, kv):
    if isinstance(node, list):
        for i in node:
            for x in findkeys(i, kv):
               yield x
    elif isinstance(node, dict):
        if kv in node:
            yield node[kv]
        for j in node.values():
            for x in findkeys(j, kv):
                yield x

print(list(findkeys(d, 'id')))

1
Цей приклад працював із кожним складним словником, який я тестував. Молодці.

Це повинна бути прийнята відповідь, вона може знаходити ключі, що знаходяться в словниках, вкладених у список списків тощо
Антон

Це працює і в Python3, за умови, що оператор print у кінці змінений. Жодне з наведених вище рішень не працювало для відповіді API зі списками, вкладеними всередину диктів, перерахованих всередині списків тощо, але цей працював чудово.
Andy Forceno,

21
def find(key, value):
  for k, v in value.iteritems():
    if k == key:
      yield v
    elif isinstance(v, dict):
      for result in find(key, v):
        yield result
    elif isinstance(v, list):
      for d in v:
        for result in find(key, d):
          yield result

EDIT: @Anthon помітив, що це не буде працювати для безпосередньо вкладених списків. Якщо у вас є це у вашому введенні, ви можете використовувати це:

def find(key, value):
  for k, v in (value.iteritems() if isinstance(value, dict) else
               enumerate(value) if isinstance(value, list) else []):
    if k == key:
      yield v
    elif isinstance(v, (dict, list)):
      for result in find(key, v):
        yield result

Але я думаю, що оригінальну версію легше зрозуміти, тому я залишу її.


1
Це також чудово працює, але так само натрапляє на проблеми, якщо стикається зі списком, який безпосередньо містить рядок (який я забув включити у свій приклад). Думаю, це додає isinstanceперевірка на а dictперед двома останніми рядками.
Метт Суейн,

1
Дякую за похвали, але я би з гордістю отримав їх за чистоту мого коду, ніж за його швидкість.
Alfe,

1
95% випадків, так. Решта (рідкісні) випадки - це випадки, коли деяке обмеження часу може змусити мене вибрати більш швидку версію порівняно з більш чистою. Але мені це не подобається. Це завжди означає покласти багато праці на мого наступника, який повинен буде підтримувати цей код. Це ризик, тому що мій наступник може заплутатися. Тоді мені доведеться написати багато коментарів, можливо, цілий документ, що пояснює мої спонукання, терміни експериментів, їх результати тощо. Це набагато більше праці для мене та всіх колег, щоб зробити це належним чином. Прибиральник набагато простіший.
Alfe

2
@Alfe - дякую за цю відповідь. Мені довелося витягти всі випадки появи рядка у вкладеному дикті для конкретного випадку використання Elasticsearch, і цей код був корисний із незначною модифікацією - stackoverflow.com/questions/40586020/…
Саураб Хірані

1
Це повністю розбивається на списки, що безпосередньо містяться у списках.
Антон

5

Інший варіант, який включає вкладений шлях до знайдених результатів ( примітка: ця версія не враховує списки ):

def find_all_items(obj, key, keys=None):
    """
    Example of use:
    d = {'a': 1, 'b': 2, 'c': {'a': 3, 'd': 4, 'e': {'a': 9, 'b': 3}, 'j': {'c': 4}}}
    for k, v in find_all_items(d, 'a'):
        print "* {} = {} *".format('->'.join(k), v)    
    """
    ret = []
    if not keys:
        keys = []
    if key in obj:
        out_keys = keys + [key]
        ret.append((out_keys, obj[key]))
    for k, v in obj.items():
        if isinstance(v, dict):
            found_items = find_all_items(v, key, keys=(keys+[k]))
            ret += found_items
    return ret

5

Я просто хотів повторити відмінну відповідь @ hexerei-software, використовуючи yield fromта приймаючи списки найвищого рівня.

def gen_dict_extract(var, key):
    if isinstance(var, dict):
        for k, v in var.items():
            if k == key:
                yield v
            if isinstance(v, (dict, list)):
                yield from gen_dict_extract(v, key)
    elif isinstance(var, list):
        for d in var:
            yield from gen_dict_extract(d, key)

Відмінний мод на відповідь @ hexerei-software: стислий і дозволяє складати списки диктів! Я використовую це разом із пропозиціями @ bruno-bronosky у своїх коментарях for key in keys. Крім того, я додав до 2 isinstanceв (list, tuple)протягом ще додаткової різновиди. ;)
Cometsong

4

Ця функція здійснює рекурсивний пошук у словнику, що містить вкладені словники та списки. Він створює список із назвою fields_found, який містить значення кожного разу, коли поле знайдено. "Поле" - це ключ, який я шукаю у словнику та його вкладених списках та словниках.

def get_recursively (search_dict, field):
    "" "Робить дикт із вкладеними списками та диктами,
    і шукає у всіх диктофонах ключ поля
    за умови.
    "" "
    знайдене поле = []

    для ключа, значення в search_dict.iteritems ():

        якщо ключ == поле:
            fields_found.append (значення)

        elif isinstance (значення, dict):
            результати = get_recursively (значення, поле)
            для результату в результатах:
                fields_found.append (результат)

        elif isinstance (значення, список):
            для товару у вартості:
                if isinstance (пункт, дикт):
                    more_results = отримати_рекурсивно (елемент, поле)
                    для іншого_результату в more_results:
                        fields_found.append (інший_результат)

    повернути поля_знайдено

1
Ви можете використовувати fields_found.extend (more_results) замість запуску іншого циклу. На мою думку, це виглядало б трохи чистіше.
сапіт

0

Ось мій удар:

def keyHole(k2b,o):
  # print "Checking for %s in "%k2b,o
  if isinstance(o, dict):
    for k, v in o.iteritems():
      if k == k2b and not hasattr(v, '__iter__'): yield v
      else:
        for r in  keyHole(k2b,v): yield r
  elif hasattr(o, '__iter__'):
    for r in [ keyHole(k2b,i) for i in o ]:
      for r2 in r: yield r2
  return

Приклад:

>>> findMe = {'Me':{'a':2,'Me':'bop'},'z':{'Me':4}}
>>> keyHole('Me',findMe)
<generator object keyHole at 0x105eccb90>
>>> [ x for x in keyHole('Me',findMe) ]
['bop', 4]

0

Слідом за відповіддю програмного забезпечення @hexerei та коментарем @ bruno-bronosky, якщо ви хочете переглядати список / набір ключів:

def gen_dict_extract(var, keys):
   for key in keys:
      if hasattr(var, 'items'):
         for k, v in var.items():
            if k == key:
               yield v
            if isinstance(v, dict):
               for result in gen_dict_extract([key], v):
                  yield result
            elif isinstance(v, list):
               for d in v:
                  for result in gen_dict_extract([key], d):
                     yield result    

Зверніть увагу, що я передаю список із одним елементом ([key]}, замість рядкового ключа.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.