Encontre todas as ocorrências de uma chave em listas e dicionários aninhados

87

Eu tenho um dicionário como este:

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

Basicamente, um dicionário com listas, dicionários e strings aninhados de profundidade arbitrária.

Qual é a melhor maneira de fazer isso para extrair os valores de cada chave "id"? Desejo obter o equivalente a uma consulta XPath como "// id". O valor de "id" é sempre uma string.

Portanto, a partir do meu exemplo, a saída de que preciso é basicamente:

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

A ordem não é importante.

Matt Swain
fonte
Muitas de suas soluções explodem se passarmos Nonecomo entrada. Você se preocupa com robustez? (já que agora está sendo usada como pergunta canônica)
smci

Respostas:

74

Achei esta Q / A muito interessante, pois fornece várias soluções diferentes para o mesmo problema. Peguei todas essas funções e testei-as com um objeto de dicionário complexo. Tive que retirar duas funções do teste, porque eles tiveram muitos resultados reprovados e não suportavam o retorno de listas ou dictos como valores, o que considero essencial, uma vez que uma função deve ser preparada para quase todos os dados que virão.

Então eu bombeei as outras funções em 100.000 iterações através do timeit módulo e a saída veio para o seguinte resultado:

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

Todas as funções tinham o mesmo ponteiro para pesquisar ('logging') e o mesmo objeto de dicionário, que é construído assim:

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

Todas as funções forneceram o mesmo resultado, mas as diferenças de tempo são dramáticas! A função gen_dict_extract(k,o)é a minha função adaptada das funções aqui, na verdade é muito parecida com ofind função de Alfe, com a principal diferença, que estou verificando se o objeto fornecido tem função de iteritens, no caso de strings serem passadas durante a recursão:

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

Portanto, esta variante é a mais rápida e segura das funções aqui. E find_all_itemsé incrivelmente lento e distante do segundo mais lento get_recursivleyenquanto o resto, exceto dict_extract, está perto um do outro. As funções fune keyHolesó funcionam se você estiver procurando por strings.

Aspecto de aprendizagem interessante aqui :)

software hexerei
fonte
1
Se você quiser pesquisar várias chaves como eu fiz, apenas: (1) mude para gen_dict_extract(keys, var)(2) coloque for key in keys:como linha 2 e indente o resto (3) mude o primeiro rendimento parayield {key: v}
Bruno Bronosky
6
Você está comparando maçãs com laranjas. Executar uma função que retorna um gerador leva menos tempo do que executar uma função que retorna um resultado final. Experimente o timeit on next(functionname(k, o)para todas as soluções de gerador.
kaleissin
6
hasattr(var, 'items')para python3
gobrewers14
1
Você considerou retirar a if hasattrparte de uma versão usando trypara capturar a exceção no caso de falha na chamada (consulte pastebin.com/ZXvVtV0g para uma possível implementação)? Isso reduziria a pesquisa dobrada do atributo iteritems(uma hasattr()vez para a chamada e uma vez para a chamada) e, portanto, provavelmente reduziria o tempo de execução (o que parece importante para você). Não fez nenhum benchmark, no entanto.
Alfe
2
Para quem visita esta página agora que o Python 3 assumiu o controle, lembre-se de que iteritemsse tornou 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']
kev
fonte
A única coisa que eu mudaria é for k in dpara for k,value in d.items()com o uso posterior de valueem vez de d[k].
ovgolovin,
Obrigado, isso funciona muito bem. Exigiu uma modificação muito pequena porque minhas listas podem conter strings, bem como dicts (que eu não mencionei), mas de outra forma perfeita.
Matt Swain
1
Isso se encaixa em um caso muito restrito, você deve considerar a resposta do "software hexerei" chamadogen_dict_extract
Bruno Bronosky
Recebi o erro "TypeError: o argumento do tipo 'NoneType' não é iterável"
xiaoshir
2
Esta solução parece não suportar listas
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')))
arainchi
fonte
1
Este exemplo funcionou com todos os dicionários complexos que testei. Bem feito.
Esta deve ser a resposta aceita, ele pode encontrar as chaves que estão dentro de dicionários que estão aninhados em uma lista de listas etc.
Anthon
Isso funciona no Python3 também, contanto que a instrução print no final seja modificada. Nenhuma das soluções acima funcionou para uma resposta da API com listas aninhadas dentro de dicts listados dentro de listas, etc, mas esta funcionou perfeitamente.
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 notou que isso não funcionará para listas aninhadas diretamente. Se você tiver isso em sua entrada, você pode usar isto:

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

Mas acho que a versão original é mais fácil de entender, então vou deixá-la.

Alfe
fonte
1
Isso também funciona muito bem, mas também apresenta problemas se encontrar uma lista que contenha diretamente uma string (que esqueci de incluir no meu exemplo). Acho que adicionar uma isinstanceverificação de um dictantes das duas últimas linhas resolve isso.
Matt Swain
1
Obrigado pelos elogios, mas ficaria mais orgulhoso se conseguisse pela limpeza de meu código do que por sua velocidade.
Alfe
1
95% do tempo, sim. As demais (raras) ocasiões são aquelas em que alguma limitação de tempo pode me forçar a escolher uma versão mais rápida em vez de uma mais limpa. Mas eu não gosto disso. Isso sempre significa colocar muito trabalho no meu sucessor, que terá que manter esse código. É um risco porque meu sucessor pode ficar confuso. Terei que escrever muitos comentários então, talvez um documento inteiro explicando minhas motivações, experimentos de tempo, seus resultados, etc. Isso é muito mais trabalho para mim e todos os colegas para fazer isso corretamente. Cleaner é muito mais simples.
Alfe
2
@Alfe - obrigado por esta resposta. Tive a necessidade de extrair todas as ocorrências de uma string em um dicionário aninhado para um caso de uso específico de Elasticsearch e este código foi útil com uma pequena modificação - stackoverflow.com/questions/40586020/…
Saurabh Hirani
1
Isso quebra completamente as listas contidas diretamente nas listas.
Anthon de
5

Outra variação, que inclui o caminho aninhado para os resultados encontrados ( nota: esta versão não considera listas ):

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
Dolan Antenucci
fonte
5

Eu só queria repetir a excelente resposta do @hexerei-software usando yield frome aceitando listas de nível superior.

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)
Chris
fonte
Excelente mod para a resposta do @hexerei-software: sucinto e permite lista de dictos! Estou usando isso junto com as sugestões de @bruno-bronosky em seus comentários de uso for key in keys. Também acrescentei à 2ª isinstancea (list, tuple)mesmo para obter mais variedade. ;)
Cometsong de
4

Esta função pesquisa recursivamente um dicionário contendo dicionários e listas aninhados. Ele cria uma lista chamada fields_found, que contém o valor de cada vez que o campo é encontrado. O 'campo' é a chave que procuro no dicionário e em suas listas e dicionários aninhados.

def get_recursively (search_dict, field):
    "" "Obtém um dict com listas e dicts aninhados,
    e procura em todos os dicts por uma chave do campo
    forneceu.
    "" "
    fields_found = []

    para chave, valor em search_dict.iteritems ():

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

        elif isinstance (valor, dict):
            resultados = get_recursively (value, field)
            para resultado em resultados:
                fields_found.append (resultado)

        elif isinstance (valor, lista):
            para item em valor:
                if isinstance (item, dict):
                    more_results = get_recursively (item, field)
                    para outro_resultado em more_results:
                        fields_found.append (another_result)

    return fields_found
Becca Petrin
fonte
1
Você poderia usar fields_found.extend (more_results) em vez de executar outro loop. Ficaria um pouco mais limpo na minha opinião.
sapit de
0

Aqui está a minha tentativa:

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]
AX Labs
fonte
0

Acompanhando a resposta do software @hexerei e o comentário de @bruno-bronosky, se você quiser iterar em uma lista / conjunto de chaves:

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    

Observe que estou passando uma lista com um único elemento ([chave]}, em vez da chave da string.

João Henriques
fonte