ค้นหาคีย์ที่เกิดขึ้นทั้งหมดในพจนานุกรมและรายการที่ซ้อนกัน


88

ฉันมีพจนานุกรมแบบนี้:

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

ฉันพบว่า Q / A นี้น่าสนใจมากเนื่องจากมีวิธีแก้ปัญหาที่แตกต่างกันหลายประการสำหรับปัญหาเดียวกัน ฉันใช้ฟังก์ชันเหล่านี้ทั้งหมดและทดสอบกับวัตถุพจนานุกรมที่ซับซ้อน ฉันต้องนำฟังก์ชันสองฟังก์ชันออกจากการทดสอบเนื่องจากต้องมีผลลัพธ์ที่ล้มเหลวจำนวนมากและพวกเขาไม่สนับสนุนการส่งคืนรายการหรือเขียนตามคำบอกเป็นค่าซึ่งฉันพบว่าจำเป็นเนื่องจากควรเตรียมฟังก์ชันสำหรับข้อมูลเกือบทุกชนิดที่จะมา

ดังนั้นฉันจึงสูบฟังก์ชั่นอื่น ๆ ในการวนซ้ำ 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}
Bruno Bronosky

6
คุณกำลังเปรียบเทียบแอปเปิ้ลกับส้ม การเรียกใช้ฟังก์ชันที่ส่งคืนเครื่องกำเนิดไฟฟ้าใช้เวลาน้อยกว่าการเรียกใช้ฟังก์ชันที่ส่งคืนผลลัพธ์ที่เสร็จสมบูรณ์ ลองใช้next(functionname(k, o)เวลาสำหรับโซลูชันเครื่องกำเนิดไฟฟ้าทั้งหมด
kaleissin

6
hasattr(var, 'items')สำหรับ python3
gobrewers14

1
คุณพิจารณาที่จะตัดif hasattrส่วนของเวอร์ชันที่ใช้tryเพื่อตรวจจับข้อยกเว้นในกรณีที่การโทรล้มเหลว (ดูpastebin.com/ZXvVtV0gสำหรับการนำไปใช้งานที่เป็นไปได้) นั่นจะช่วยลดการค้นหาแอตทริบิวต์เป็นสองเท่าiteritems(หนึ่งhasattr()ครั้งสำหรับการโทร) และอาจลดรันไทม์ (ซึ่งดูเหมือนว่าสำคัญสำหรับคุณ) ไม่ได้ทำการวัดประสิทธิภาพใด ๆ
Alfe

2
สำหรับทุกคนที่เข้ามาเยี่ยมชมหน้านี้ในขณะนี้ว่างูหลาม 3 ได้เอาไปจำไว้ว่าได้กลายเป็นiteritems 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']

สิ่งเดียวที่ฉันจะเปลี่ยนเป็นfor k in dเพื่อfor k,value in d.items()กับการใช้งานที่ตามมาของแทนvalue d[k]
ovgolovin

ขอบคุณมันใช้งานได้ดี จำเป็นต้องมีการปรับเปลี่ยนเล็กน้อยเนื่องจากรายการของฉันอาจมีสตริงและคำสั่ง (ซึ่งฉันไม่ได้กล่าวถึง) แต่ก็สมบูรณ์แบบ
Matt Swain

1
นี่เหมาะกับกรณีที่แคบมากคุณเป็นหนี้ตัวเองที่จะต้องพิจารณาคำตอบจาก "ซอฟต์แวร์ hexerei" ที่เรียกว่าgen_dict_extract
Bruno Bronosky

ฉันได้รับข้อผิดพลาด "TypeError: อาร์กิวเมนต์ประเภท 'NoneType' ไม่สามารถทำซ้ำได้"
xiaoshir

2
โซลูชันนี้ดูเหมือนจะไม่รองรับรายการ
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
ตัวอย่างนี้ใช้ได้กับทุกพจนานุกรมที่ซับซ้อนที่ฉันทดสอบ ทำได้ดี.

นี่ควรเป็นคำตอบที่ได้รับการยอมรับมันสามารถค้นหาคีย์ที่อยู่ในพจนานุกรมที่ซ้อนอยู่ในรายการต่างๆเป็นต้น
Anthon

สิ่งนี้ใช้ได้ใน Python3 เช่นกันตราบเท่าที่มีการแก้ไขคำสั่งพิมพ์ในตอนท้าย ไม่มีวิธีแก้ปัญหาใด ๆ ข้างต้นนี้ที่ใช้ได้กับการตอบสนองของ 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

แก้ไข: @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ก่อนสองบรรทัดสุดท้ายจะช่วยแก้ปัญหานี้ได้
Matt Swain

1
ขอบคุณสำหรับรางวัล แต่ฉันจะเตรียมให้พวกเขาเพื่อความสะอาดของรหัสของฉันมากกว่าความเร็ว
Alfe

1
95% ของเวลาใช่ โอกาสที่เหลือ (หายาก) คือโอกาสที่การ จำกัด เวลาอาจบังคับให้ฉันเลือกเวอร์ชันที่เร็วกว่ามากกว่าโอกาสที่สะอาดกว่า แต่ฉันไม่ชอบสิ่งนี้ มันหมายถึงการใส่ภาระงานให้กับผู้สืบทอดของฉันซึ่งจะต้องรักษารหัสนั้นไว้เสมอ เป็นความเสี่ยงเนื่องจากผู้สืบทอดของฉันอาจสับสน ฉันจะต้องเขียนความคิดเห็นเป็นจำนวนมากจากนั้นอาจจะเป็นเอกสารทั้งหมดที่อธิบายถึงแรงจูงใจของฉันการทดลองเรื่องเวลาผลลัพธ์ของพวกเขา ฯลฯ นั่นเป็นวิธีที่ช่วยให้ฉันและเพื่อนร่วมงานทุกคนทำงานได้อย่างถูกต้องมากขึ้น Cleaner เป็นวิธีที่ง่ายกว่า
Alfe

2
@Alfe - ขอบคุณสำหรับคำตอบนี้ ฉันจำเป็นต้องแยกการเกิดขึ้นทั้งหมดของสตริงในคำสั่งที่ซ้อนกันสำหรับกรณีการใช้งานเฉพาะของ Elasticsearch และรหัสนี้มีประโยชน์เมื่อมีการปรับเปลี่ยนเล็กน้อย - stackoverflow.com/questions/40586020/…
Saurabh Hirani

1
สิ่งนี้ทำลายรายการที่อยู่ในรายการโดยตรงอย่างสิ้นเชิง
Anthon

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)

คำตอบของ mod ที่ยอดเยี่ยมสำหรับ @ hexerei-software: รวบรัดและอนุญาตให้ list-of-dicts! ฉันใช้นี้พร้อมกับข้อเสนอแนะ @ bruno-bronosky for key in keysในความคิดเห็นของเขากับการใช้งาน นอกจากนี้ฉันยังเพิ่มเป็นลำดับที่ 2 isinstanceเพื่อ(list, tuple)ให้มีความหลากหลายมากยิ่งขึ้น ;)
ดาวหาง

4

ฟังก์ชันนี้จะค้นหาพจนานุกรมที่มีพจนานุกรมและรายการซ้อนกันแบบวนซ้ำ สร้างรายการที่เรียกว่า fields_found ซึ่งมีค่าสำหรับทุกครั้งที่พบเขตข้อมูล 'ฟิลด์' คือกุญแจสำคัญที่ฉันกำลังมองหาในพจนานุกรมรวมถึงรายการและพจนานุกรมที่ซ้อนกัน

def get_recursively (search_dict ฟิลด์):
    "" "ใช้คำสั่งที่มีรายการและคำสั่งซ้อนกัน
    และค้นหาคำสั่งทั้งหมดเพื่อหาคีย์ของฟิลด์
    ให้.
    "" "
    fields_found = []

    สำหรับคีย์ค่าใน search_dict.iteritems ():

        ถ้าคีย์ == ฟิลด์:
            fields_found.append (ค่า)

        elif isinstance (ค่า, dict):
            ผลลัพธ์ = get_recursively (ค่าฟิลด์)
            เพื่อผลลัพธ์ในผลลัพธ์:
                fields_found.append (ผลลัพธ์)

        elif isinstance (ค่ารายการ):
            สำหรับสินค้ามูลค่า:
                ถ้า isinstance (item, dict):
                    more_results = get_recursively (รายการฟิลด์)
                    สำหรับ another_result ใน more_results:
                        fields_found.append (another_result)

    ส่งคืน fields_found

1
คุณสามารถใช้ fields_found.extend (more_results) แทนการรันลูปอื่น จะดูสะอาดกว่าในความคิดของฉัน
sapit

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    

โปรดทราบว่าฉันกำลังส่งรายการที่มีองค์ประกอบเดียว ([คีย์]} แทนคีย์สตริง


0

pip install nested-lookup ทำในสิ่งที่คุณกำลังมองหา:

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

>>> print(nested_lookup('taco', document))
[42, 69]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.