Temukan semua kemunculan kunci dalam kamus dan daftar bertingkat

88

Saya punya kamus seperti ini:

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

Pada dasarnya kamus dengan daftar bersarang, kamus, dan string, dengan kedalaman yang berubah-ubah.

Apa cara terbaik untuk melintasi ini untuk mengekstrak nilai dari setiap kunci "id"? Saya ingin mencapai yang setara dengan kueri XPath seperti "// id". Nilai "id" selalu berupa string.

Jadi dari contoh saya, output yang saya butuhkan pada dasarnya adalah:

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

Keteraturan tidak penting.

Matt Swain
sumber
Sebagian besar solusi Anda akan meledak jika kami lolos Nonesebagai masukan. Apakah Anda peduli dengan ketahanan? (karena ini sekarang digunakan sebagai pertanyaan kanonik)
smci

Jawaban:

74

Menurut saya Tanya Jawab ini sangat menarik, karena memberikan beberapa solusi berbeda untuk masalah yang sama. Saya mengambil semua fungsi ini dan mengujinya dengan objek kamus yang kompleks. Saya harus mengambil dua fungsi dari pengujian, karena mereka memiliki banyak hasil yang gagal dan mereka tidak mendukung daftar yang dikembalikan atau dicts sebagai nilai, yang menurut saya penting, karena suatu fungsi harus disiapkan untuk hampir semua data yang akan datang.

Jadi saya memompa fungsi lain dalam 100.000 iterasi melalui timeitmodul dan keluarannya menghasilkan hasil sebagai berikut:

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

Semua fungsi memiliki jarum yang sama untuk mencari ('logging') dan objek kamus yang sama, yang dibuat seperti ini:

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

Semua fungsi memberikan hasil yang sama, tetapi perbedaan waktu sangat dramatis! Fungsinya gen_dict_extract(k,o)adalah fungsi saya yang diadaptasi dari fungsi-fungsi di sini, sebenarnya sangat mirip dengan findfungsi dari Alfe, dengan perbedaan utama, bahwa saya memeriksa apakah objek yang diberikan memiliki fungsi iteritems, jika string dilewatkan selama rekursi:

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

Jadi varian ini adalah fungsi tercepat dan teraman di sini. Dan find_all_itemssangat lambat dan paling lambat kedua get_recursivleysementara sisanya, kecuali dict_extract, dekat satu sama lain. Fungsi fundan keyHolehanya berfungsi jika Anda mencari string.

Aspek pembelajaran yang menarik di sini :)

perangkat lunak hexerei
sumber
1
Jika Anda ingin mencari beberapa kunci seperti yang saya lakukan, cukup: (1) ubah ke gen_dict_extract(keys, var)(2) letakkan for key in keys:sebagai baris 2 & masukkan sisanya (3) ubah hasil pertama menjadiyield {key: v}
Bruno Bronosky
6
Anda membandingkan apel dengan jeruk. Menjalankan fungsi yang mengembalikan generator membutuhkan waktu lebih sedikit daripada menjalankan fungsi yang mengembalikan hasil akhir. Coba timeit on next(functionname(k, o)untuk semua solusi generator.
kaleissin
6
hasattr(var, 'items')untuk python3
gobrewers14
1
Apakah Anda mempertimbangkan untuk menghapus if hasattrbagian untuk versi yang digunakan tryuntuk menangkap pengecualian jika panggilan gagal (lihat pastebin.com/ZXvVtV0g untuk kemungkinan implementasi)? Itu akan mengurangi pencarian atribut dua kali lipat iteritems(sekali untuk hasattr()dan sekali untuk panggilan) dan dengan demikian mungkin mengurangi runtime (yang tampaknya penting bagi Anda). Namun, tidak membuat tolok ukur apa pun.
Alfe
2
Bagi siapa pun yang mengunjungi halaman ini sekarang setelah Python 3 mengambil alih, ingatlah bahwa itu iteritemstelah terjadi 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
sumber
Satu-satunya hal yang saya akan berubah adalah for k in duntuk for k,value in d.items()dengan penggunaan selanjutnya valuebukan d[k].
ovgolovin
Terima kasih, ini berfungsi dengan baik. Diperlukan sedikit modifikasi karena daftar saya dapat berisi string dan juga dicts (yang tidak saya sebutkan), tetapi sebaliknya sempurna.
Matt Swain
1
Ini cocok untuk kasus yang sangat sempit, Anda berhutang pada diri Anda sendiri untuk mempertimbangkan jawaban dari "perangkat lunak hexerei" yang disebutgen_dict_extract
Bruno Bronosky
Saya mendapat pesan kesalahan "TypeError: argumen tipe 'NoneType' tidak dapat
diulangi
2
Solusi ini sepertinya tidak mendukung daftar
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')))
arainchi
sumber
1
Contoh ini bekerja dengan setiap kamus kompleks yang saya uji. Sudah selesai dilakukan dengan baik.
Ini harus menjadi jawaban yang diterima, ini dapat menemukan kunci yang ada dalam kamus yang bersarang dalam daftar daftar dll.
Anthon
Ini berfungsi di Python3 juga, selama pernyataan cetak di bagian akhir diubah. Tak satu pun dari solusi di atas ini berfungsi untuk respons API dengan daftar bersarang di dalam dicts yang tercantum di dalam daftar, dll, tetapi yang ini bekerja dengan baik.
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 memperhatikan bahwa ini tidak akan berfungsi untuk daftar bersarang secara langsung. Jika Anda memiliki ini dalam masukan Anda, Anda dapat menggunakan ini:

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

Tapi menurut saya versi aslinya lebih mudah dimengerti, jadi saya akan tinggalkan.

Alfe
sumber
1
Ini berfungsi dengan baik juga, tetapi juga mengalami masalah jika menemukan daftar yang secara langsung berisi string (yang saya lupa sertakan dalam contoh saya). Saya pikir menambahkan tanda isinstancecentang dictsebelum dua baris terakhir menyelesaikan ini.
Matt Swain
1
Terima kasih atas penghargaannya, tetapi saya akan lebih bangga mendapatkannya karena kebersihan kode saya daripada kecepatannya.
Alfe
1
95% dari waktu, ya. Kesempatan (jarang) yang tersisa adalah saat di mana beberapa batasan waktu mungkin memaksa saya untuk memilih versi yang lebih cepat daripada yang lebih bersih. Tapi saya tidak suka ini. Itu selalu berarti meletakkan beban pekerjaan ke penerus saya yang harus mempertahankan kode itu. Ini berisiko karena penerus saya mungkin akan bingung. Saya harus menulis banyak komentar, mungkin seluruh dokumen yang menjelaskan motivasi saya, eksperimen waktu, hasil mereka, dll. Itu jauh lebih banyak pekerjaan bagi saya dan semua rekan kerja untuk menyelesaikannya dengan benar. Pembersih jauh lebih sederhana.
Alfe
2
@Alfe - terima kasih atas jawaban ini. Saya harus mengekstrak semua kemunculan string dalam dikt bersarang untuk kasus penggunaan tertentu Elasticsearch dan kode ini berguna dengan sedikit modifikasi - stackoverflow.com/questions/40586020/…
Saurabh Hirani
1
Ini benar - benar merusak daftar yang langsung terkandung dalam daftar.
Anthon
5

Variasi lain, yang mencakup jalur bersarang ke hasil yang ditemukan ( catatan: versi ini tidak mempertimbangkan daftar ):

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

Saya hanya ingin mengulangi jawaban yang sangat baik @ hexerei-software menggunakan yield fromdan menerima daftar tingkat atas.

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
sumber
Mod yang sangat baik untuk jawaban @ hexerei-software: ringkas dan memungkinkan daftar-petunjuk! Saya menggunakan ini bersama dengan saran @ bruno-bronosky dalam komentarnya untuk digunakan for key in keys. Juga saya ditambahkan ke 2 isinstanceuntuk (list, tuple)bahkan lebih beragam. ;)
Cometsong
4

Fungsi ini secara rekursif mencari kamus yang berisi kamus dan daftar bertingkat. Itu membangun daftar yang disebut field_found, yang berisi nilai untuk setiap kali field ditemukan. 'Bidang' adalah kunci yang saya cari di kamus dan daftar bersarang serta kamusnya.

def get_recursively (search_dict, field):
    "" "Mengambil dikt dengan daftar dan dict bersarang,
    dan mencari semua penis untuk kunci lapangan
    disediakan.
    "" "
    field_found = []

    untuk kunci, nilai di search_dict.iteritems ():

        jika kunci == bidang:
            field_found.append (nilai)

        elif isinstance (nilai, dikt):
            hasil = get_recursively (nilai, bidang)
            untuk hasil dalam hasil:
                field_found.append (hasil)

        elif isinstance (nilai, daftar):
            untuk item dalam nilai:
                if isinstance (item, dict):
                    more_results = get_recursively (item, field)
                    untuk another_result in more_results:
                        field_found.append (hasil_lain)

    mengembalikan field_found
Becca Petrin
sumber
1
Anda bisa menggunakan field_found.extend (more_results) daripada menjalankan loop lain. Akan terlihat sedikit lebih bersih menurut saya.
sapit
0

Inilah tusukan saya:

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

Menindaklanjuti jawaban software @hexerei dan komentar @ bruno-bronosky, jika Anda ingin mengulang daftar / set kunci:

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    

Perhatikan bahwa saya meneruskan daftar dengan satu elemen ([key]}, bukan kunci string.

João Henriques
sumber
0

pip install nested-lookup melakukan apa yang Anda cari:

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

>>> print(nested_lookup('taco', document))
[42, 69]
VengaVenga
sumber