Tìm tất cả các lần xuất hiện của một khóa trong từ điển và danh sách lồng nhau


87

Tôi có một từ điển như thế này:

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

Về cơ bản là một từ điển với danh sách, từ điển và chuỗi được lồng vào nhau, có độ sâu tùy ý.

Cách tốt nhất để duyệt qua điều này để trích xuất các giá trị của mọi khóa "id" là gì? Tôi muốn đạt được tương đương với truy vấn XPath như "// id". Giá trị của "id" luôn là một chuỗi.

Vì vậy, từ ví dụ của tôi, đầu ra tôi cần về cơ bản là:

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

Thứ tự không quan trọng.



Hầu hết các giải pháp của bạn sẽ nổ tung nếu chúng tôi chuyển qua Nonelàm đầu vào. Bạn có quan tâm đến sự mạnh mẽ? (vì câu này hiện đang được dùng làm câu hỏi chuẩn)
smci

Câu trả lời:


74

Tôi thấy phần Q / A này rất thú vị, vì nó cung cấp một số giải pháp khác nhau cho cùng một vấn đề. Tôi đã lấy tất cả các chức năng này và thử nghiệm chúng với một đối tượng từ điển phức tạp. Tôi đã phải loại bỏ hai chức năng ra khỏi thử nghiệm, vì chúng phải nhận nhiều kết quả không thành công và chúng không hỗ trợ trả về danh sách hoặc phân số dưới dạng giá trị, điều mà tôi thấy cần thiết, vì một hàm nên được chuẩn bị cho hầu hết mọi dữ liệu sắp tới.

Vì vậy, tôi đã bơm các chức năng khác trong 100.000 lần lặp qua timeitmô-đun và kết quả đầu ra như sau:

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

Tất cả các hàm đều có cùng một kim để tìm kiếm ('ghi nhật ký') và cùng một đối tượng từ điển, được cấu tạo như thế này:

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

Tất cả các chức năng đều cho kết quả giống nhau, nhưng sự khác biệt về thời gian là rất lớn! Hàm gen_dict_extract(k,o)là hàm của tôi được điều chỉnh từ các hàm ở đây, thực ra nó khá giống với findhàm từ Alfe, với sự khác biệt chính là tôi đang kiểm tra xem đối tượng đã cho có hàm iteritems hay không, trong trường hợp các chuỗi được truyền trong quá trình đệ quy:

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

Vì vậy, biến thể này là nhanh nhất và an toàn nhất trong các chức năng ở đây. Và find_all_itemscực kỳ chậm và xa thứ hai chậm nhất get_recursivleytrong khi phần còn lại, ngoại trừ dict_extract, là gần nhau. Các chức năng funkeyHolechỉ hoạt động nếu bạn đang tìm kiếm chuỗi.

Khía cạnh học tập thú vị ở đây :)


1
Nếu bạn muốn tìm kiếm nhiều phím như tôi đã làm, chỉ cần: (1) thay đổi gen_dict_extract(keys, var)(2) đặt for key in keys:như dòng 2 & thụt phần còn lại (3) thay đổi sản lượng đầu tiênyield {key: v}
Bruno Bronosky

6
Bạn đang so sánh táo với cam. Chạy một hàm trả về trình tạo sẽ mất ít thời gian hơn so với chạy một hàm trả về kết quả đã hoàn thành. Hãy thử bật thời gian next(functionname(k, o)cho tất cả các giải pháp máy phát điện.
kaleissin

6
hasattr(var, 'items')cho python3
gobrewers 14

1
Bạn có cân nhắc loại bỏ if hasattrphần này cho một phiên bản sử dụng tryđể bắt ngoại lệ trong trường hợp cuộc gọi không thành công (xem pastebin.com/ZXvVtV0g để biết cách triển khai khả thi)? Điều đó sẽ làm giảm số lần tra cứu thuộc tính iteritems(một lần cho hasattr()và một lần cho cuộc gọi) và do đó có thể giảm thời gian chạy (điều này có vẻ quan trọng đối với bạn). Tuy nhiên, không thực hiện bất kỳ điểm chuẩn nào.
Alfe

2
Đối với bất kỳ ai truy cập trang này bây giờ mà Python 3 đã tiếp quản, hãy nhớ rằng điều đó iteritemsđã trở thành 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']

Điều duy nhất tôi muốn thay đổi là for k in dđể for k,value in d.items()với việc sử dụng tiếp theo của valuethay vì d[k].
ovgolovin

Cảm ơn, điều này làm việc tuyệt vời. Yêu cầu sửa đổi rất nhẹ vì danh sách của tôi có thể chứa chuỗi cũng như các phần (mà tôi không đề cập đến), nhưng nếu không thì hoàn hảo.
Matt Swain

1
Phù hợp Đây là một trường hợp rất hẹp, bạn nợ cho chính mình để xem xét các câu trả lời từ "phần mềm hexerei" gọigen_dict_extract
Bruno Bronosky

Tôi đã nhận lỗi "Lỗi Loại: đối số kiểu 'NoneType' không phải là iterable"
xiaoshir

2
Giải pháp này dường như không hỗ trợ danh sách
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
Ví dụ này hoạt động với mọi từ điển phức tạp mà tôi đã thử nghiệm. Làm tốt.

Đây phải là câu trả lời được chấp nhận, nó có thể tìm thấy các khóa trong từ điển được lồng trong danh sách các danh sách, v.v.
Anthon

Điều này cũng hoạt động trong Python3, miễn là câu lệnh in ở cuối được sửa đổi. Không có giải pháp nào ở trên hoạt động đối với phản hồi API với các danh sách được lồng bên trong các phần được liệt kê bên trong danh sách, v.v. nhưng giải pháp này hoạt động rất tốt.
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

CHỈNH SỬA: @Anthon nhận thấy rằng điều này sẽ không hoạt động đối với các danh sách lồng nhau trực tiếp. Nếu bạn có điều này trong đầu vào của mình, bạn có thể sử dụng điều này:

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

Nhưng tôi nghĩ phiên bản gốc dễ hiểu hơn nên tôi sẽ bỏ nó đi.


1
Điều này cũng hoạt động tốt, nhưng tương tự như vậy sẽ gặp vấn đề nếu nó gặp phải một danh sách chứa trực tiếp một chuỗi (mà tôi đã quên đưa vào ví dụ của mình). Tôi nghĩ rằng việc thêm vào một isinstanceséc cho một dicttrước khi hai dòng cuối cùng giải quyết điều này.
Matt Swain

1
Cảm ơn vì những lời khen tặng, nhưng tôi tự hào nhận được chúng vì sự sạch sẽ của mã của tôi hơn là tốc độ của nó.
Alfe

1
95% thời gian, có. Những trường hợp (hiếm) còn lại là những trường hợp mà một số giới hạn về thời gian có thể buộc tôi phải chọn phiên bản nhanh hơn so với phiên bản sạch hơn. Nhưng tôi không thích điều này. Nó luôn luôn có nghĩa là đặt một lượng công việc lên người kế nhiệm của tôi, người sẽ phải duy trì mã đó. Đó là một rủi ro vì người kế nhiệm của tôi có thể bị nhầm lẫn. Sau đó, tôi sẽ phải viết rất nhiều nhận xét, có thể là toàn bộ tài liệu giải thích động cơ của tôi, thời gian thí nghiệm, kết quả của chúng, v.v. Đó là cách nhiều việc hơn đối với tôi và tất cả các đồng nghiệp để hoàn thành nó đúng cách. Cleaner là cách đơn giản hơn.
Alfe

2
@Alfe - cảm ơn vì câu trả lời này. Tôi đã có một nhu cầu để trích xuất tất cả các lần xuất hiện của một chuỗi trong một dict lồng nhau cho một trường hợp cụ thể sử dụng Elasticsearch và mã này là hữu ích với một sửa đổi nhỏ - stackoverflow.com/questions/40586020/...
Saurabh Hirani

1
Điều này hoàn toàn phá vỡ trên các danh sách được chứa trực tiếp trong danh sách.
Anthon

5

Một biến thể khác, bao gồm đường dẫn lồng nhau đến các kết quả được tìm thấy ( lưu ý: phiên bản này không xem xét danh sách ):

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

Tôi chỉ muốn lặp lại câu trả lời tuyệt vời của @ hexerei-software bằng cách sử dụng yield fromvà chấp nhận các danh sách cấp cao nhất.

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)

Bản mod tuyệt vời cho câu trả lời của @ hexerei-software: cô đọng và cho phép danh sách các phần! Tôi đang sử dụng điều này cùng với các đề xuất của @ bruno-bronosky trong nhận xét của anh ấy để sử dụng for key in keys. Ngoài ra tôi thêm vào thứ 2 isinstanceđể (list, tuple)cho thậm chí hơn nhiều. ;)
Cometsong

4

Hàm này tìm kiếm đệ quy một từ điển có chứa các từ điển và danh sách lồng nhau. Nó xây dựng một danh sách được gọi là fields_found, chứa giá trị cho mỗi lần trường được tìm thấy. 'Trường' là từ khóa tôi đang tìm kiếm trong từ điển và các danh sách và từ điển lồng nhau của nó.

def get_recursently (search_dict, field):
    "" "Thực hiện một câu lệnh với các danh sách và dấu tích lồng nhau,
    và tìm kiếm tất cả các dấu hiệu cho một khóa của trường
    đã cung cấp.
    "" "
    fields_found = []

    cho khóa, giá trị trong search_dict.iteritems ():

        trường if key ==:
            fields_found.append (giá trị)

        elif isinstance (value, dict):
            results = get_recursently (giá trị, trường)
            để biết kết quả:
                fields_found.append (kết quả)

        elif isinstance (giá trị, danh sách):
            cho mặt hàng có giá trị:
                if isinstance (item, dict):
                    more_results = get_recursently (mục, trường)
                    cho another_result trong more_results:
                        fields_found.append (another_result)

    return fields_found

1
Bạn có thể sử dụng fields_found.extend (more_results) thay vì chạy một vòng lặp khác. Theo ý kiến ​​của tôi sẽ trông sạch sẽ hơn một chút.
sapit

0

Đây là cú đâm của tôi vào nó:

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

Ví dụ.:

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

Theo dõi câu trả lời của phần mềm @hexerei và nhận xét của @ bruno-bronosky, nếu bạn muốn lặp lại danh sách / bộ khóa:

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    

Lưu ý rằng tôi đang chuyển một danh sách có một phần tử ([key]}, thay vì khóa chuỗi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.