Trova tutte le occorrenze di una chiave in dizionari ed elenchi annidati


88

Ho un dizionario come questo:

{ "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" }] } ] } 

Fondamentalmente un dizionario con elenchi annidati, dizionari e stringhe, di profondità arbitraria.

Qual è il modo migliore per attraversarlo per estrarre i valori di ogni chiave "id"? Voglio ottenere l'equivalente di una query XPath come "// id". Il valore di "id" è sempre una stringa.

Quindi dal mio esempio, l'output di cui ho bisogno è fondamentalmente:

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

L'ordine non è importante.



La maggior parte delle tue soluzioni esplode se passiamo Nonecome input. Ti interessa la robustezza? (dal momento che ora viene usata come domanda canonica)
smci

Risposte:


74

Ho trovato questa domanda / risposta molto interessante, poiché fornisce diverse soluzioni per lo stesso problema. Ho preso tutte queste funzioni e le ho testate con un complesso oggetto dizionario. Ho dovuto togliere due funzioni dal test, perché avevano molti risultati non riusciti e non supportavano la restituzione di elenchi o dettami come valori, cosa che trovo essenziale, poiché una funzione dovrebbe essere preparata per quasi tutti i dati a venire.

Quindi ho pompato le altre funzioni in 100.000 iterazioni attraverso il timeitmodulo e l'output è arrivato al seguente risultato:

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

Tutte le funzioni avevano lo stesso ago per la ricerca ('registrazione') e lo stesso oggetto dizionario, che è costruito in questo modo:

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]]
}

Tutte le funzioni hanno fornito lo stesso risultato, ma le differenze di orario sono notevoli! La funzione gen_dict_extract(k,o)è la mia funzione adattata dalle funzioni qui, in realtà è più o meno come la findfunzione di Alfe, con la differenza principale, che sto controllando se l'oggetto dato ha la funzione iteritems, nel caso in cui le stringhe vengano passate durante la ricorsione:

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

Quindi questa variante è la più veloce e sicura delle funzioni qui. Ed find_all_itemsè incredibilmente lento e lontano dal secondo più lento get_recursivleymentre il resto, tranne dict_extract, è vicino l'uno all'altro. Le funzioni fune funzionano keyHolesolo se stai cercando stringhe.

Aspetto interessante di apprendimento qui :)


1
Se vuoi cercare più chiavi come ho fatto io, semplicemente: (1) cambia in gen_dict_extract(keys, var)(2) metti for key in keys:come riga 2 e fa rientrare il resto (3) cambia il primo rendimento inyield {key: v}
Bruno Bronosky

6
Stai confrontando le mele con le arance. L'esecuzione di una funzione che restituisce un generatore richiede meno tempo rispetto all'esecuzione di una funzione che restituisce un risultato finito. Prova timeit next(functionname(k, o)per tutte le soluzioni del generatore.
kaleissin

6
hasattr(var, 'items')per python3
gobrewers14

1
Hai considerato di rimuovere la if hasattrparte per una versione utilizzando tryper catturare l'eccezione nel caso in cui la chiamata fallisca (vedi pastebin.com/ZXvVtV0g per una possibile implementazione)? Ciò ridurrebbe la ricerca raddoppiata dell'attributo iteritems(una hasattr()volta e una volta per la chiamata) e quindi probabilmente ridurrebbe il runtime (che sembra importante per te). Tuttavia, non ho fatto alcun benchmark.
Alfe

2
Per chiunque visiti questa pagina ora che Python 3 ha preso il sopravvento, ricorda che iteritemsè diventato items.
Mike Williamson

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']

L'unica cosa che vorrei cambiare è for k in dper for k,value in d.items()il successivo utilizzo valueal posto di d[k].
ovgolovin

Grazie, funziona benissimo. Necessaria una modifica molto leggera perché le mie liste possono contenere stringhe e dict (che non ho menzionato), ma per il resto perfette.
Matt Swain

1
Questo si adatta a un caso molto ristretto, devi a te stesso considerare la risposta di "software hexerei" chiamatagen_dict_extract
Bruno Bronosky

Ho ricevuto l'errore "TypeError: l'argomento di tipo" NoneType "non è iterabile"
xiaoshir

2
Questa soluzione non sembra supportare gli elenchi
Alex R

24
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
Questo esempio ha funzionato con ogni dizionario complesso che ho testato. Molto bene.

Questa dovrebbe essere la risposta accettata, può trovare chiavi che si trovano all'interno di dizionari annidati all'interno di elenchi di elenchi ecc.
Anthon

Funziona anche in Python3, a condizione che l'istruzione print alla fine venga modificata. Nessuna delle soluzioni precedenti ha funzionato per una risposta API con elenchi annidati all'interno di dict elencati all'interno di elenchi, ecc., Ma questa ha funzionato magnificamente.
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 ha notato che questo non funzionerà per elenchi nidificati direttamente. Se hai questo nel tuo input, puoi usare questo:

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

Ma penso che la versione originale sia più facile da capire, quindi la lascerò.


1
Anche questo funziona alla grande, ma allo stesso modo incorre in problemi se incontra un elenco che contiene direttamente una stringa (che ho dimenticato di includere nel mio esempio). Penso che l'aggiunta di un isinstancecontrollo per una dictprima delle ultime due righe risolva questo problema.
Matt Swain

1
Grazie per i riconoscimenti, ma sarei più orgoglioso di ottenerli per la pulizia del mio codice che per la sua velocità.
Alfe

1
Il 95% delle volte, sì. Le restanti (rare) occasioni sono quelle in cui qualche limite di tempo potrebbe costringermi a scegliere una versione più veloce rispetto a una più pulita. Ma questo non mi piace. Significa sempre mettere un carico di lavoro sul mio successore che dovrà mantenere quel codice. È un rischio perché il mio successore potrebbe confondersi. Dovrò scrivere molti commenti allora, forse un intero documento che spieghi le mie motivazioni, gli esperimenti sui tempi, i loro risultati ecc. Questo è molto più lavoro per me e per tutti i colleghi per farlo correttamente. Cleaner è molto più semplice.
Alfe

2
@Alfe - grazie per questa risposta. Avevo bisogno di estrarre tutte le occorrenze di una stringa in un dict annidato per un caso d'uso specifico di Elasticsearch e questo codice è stato utile con una piccola modifica - stackoverflow.com/questions/40586020/…
Saurabh Hirani

1
Ciò interrompe completamente gli elenchi contenuti direttamente negli elenchi.
Anthon

5

Un'altra variante, che include il percorso nidificato ai risultati trovati ( nota: questa versione non considera gli elenchi ):

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

Volevo solo ripetere l'eccellente risposta di @ hexerei-software utilizzando yield frome accettando elenchi di primo livello.

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)

Mod eccellente per la risposta di @ hexerei-software: conciso e consente la lista dei dict! Lo sto usando insieme ai suggerimenti di @bruno-bronosky nei suoi commenti da usare for key in keys. Inoltre ho aggiunto al 2 ° isinstanceper (list, tuple)per ancora maggiore varietà. ;)
Cometsong

4

Questa funzione ricerca ricorsivamente un dizionario contenente dizionari ed elenchi annidati. Crea un elenco chiamato fields_found, che contiene il valore per ogni volta che viene trovato il campo. Il "campo" è la chiave che cerco nel dizionario e nei suoi elenchi e dizionari annidati.

def get_recursively (search_dict, field):
    "" "Accetta un dict con elenchi e dict annidati,
    e cerca in tutti i dict una chiave del campo
    fornito.
    "" "
    field_found = []

    per chiave, valore in search_dict.iteritems ():

        if key == field:
            fields_found.append (valore)

        elif isinstance (value, dict):
            risultati = get_recursively (valore, campo)
            per risultato in risultati:
                fields_found.append (risultato)

        elif isinstance (value, list):
            per articolo in valore:
                se isinstance (item, dict):
                    more_results = get_recursively (elemento, campo)
                    per another_result in more_results:
                        fields_found.append (another_result)

    return fields_found

1
Potresti usare fields_found.extend (more_results) invece di eseguire un altro ciclo. Sembrerebbe un po 'più pulito secondo me.
sapit

0

Ecco la mia pugnalata:

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

Ex.:

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

Seguendo la risposta di @hexerei software e il commento di @ bruno-bronosky, se vuoi scorrere un elenco / set di chiavi:

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    

Nota che sto passando un elenco con un singolo elemento ([key]}, invece della chiave della stringa.


0

pip install nested-lookup fa esattamente quello che stai cercando:

document = [ { 'taco' : 42 } , { 'salsa' : [ { 'burrito' : { 'taco' : 69 } } ] } ]

>>> print(nested_lookup('taco', document))
[42, 69]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.