Xpath-подобный запрос для вложенных словарей Python

Есть ли способ определить запрос типа XPath для вложенных словарей Python.

Что-то вроде этого:

foo = {
  'spam':'eggs',
  'morefoo': {
               'bar':'soap',
               'morebar': {'bacon' : 'foobar'}
              }
   }

print( foo.select("/morefoo/morebar") )

>> {'bacon' : 'foobar'}

Мне также нужно было выбрать вложенные списки;)

Это можно легко сделать с помощью решения @jellybean:

def xpath_get(mydict, path):
    elem = mydict
    try:
        for x in path.strip("/").split("/"):
            try:
                x = int(x)
                elem = elem[x]
            except ValueError:
                elem = elem.get(x)
    except:
        pass

    return elem

foo = {
  'spam':'eggs',
  'morefoo': [{
               'bar':'soap',
               'morebar': {
                           'bacon' : {
                                       'bla':'balbla'
                                     }
                           }
              },
              'bla'
              ]
   }

print xpath_get(foo, "/morefoo/0/morebar/bacon")

[EDIT 2016] Этот вопрос и принятый ответ являются древними. Новые ответы могут выполнять работу лучше, чем исходный ответ. Однако я не проверял их, поэтому не буду менять принятый ответ.


person RickyA    schedule 06.09.2011    source источник
comment
Почему бы не использовать foo['morefoo']['morebar'] ?   -  person MarcoS    schedule 06.09.2011
comment
потому что я хочу сделать: def bla(query): data.select(query)   -  person RickyA    schedule 06.09.2011
comment
@MarcoS Было бы интереснее со списками, в которых микроязык пути возвращал бы несколько элементов.   -  person Pavel Šimerda    schedule 14.10.2017
comment
@PavelŠimerda Да, намного интереснее, особенно с запросами с подстановочными знаками (найти все значения по определенному ключу), а затем - также рекурсивными списками вниз или [именованными] кортежами ...   -  person Tomasz Gandor    schedule 26.04.2018
comment
Этот вопрос (в Python) по существу требует рекомендации сторонней библиотеки.   -  person user7610    schedule 01.03.2021


Ответы (10)


Не совсем красиво, но вы можете использовать что-то вроде

def xpath_get(mydict, path):
    elem = mydict
    try:
        for x in path.strip("/").split("/"):
            elem = elem.get(x)
    except:
        pass

    return elem

Конечно, это не поддерживает такие вещи xpath, как индексы ... не говоря уже об указанной ловушке / ключа unutbu.

person Johannes Charra    schedule 06.09.2011
comment
В 2011 году, возможно, вариантов было не так много, как сегодня, но в 2014 году, я думаю, решать проблему таким образом не слишком элегантно и его следует избегать. - person nikolay; 26.09.2014
comment
@nikolay это просто предположение или есть решения, которые решают эту проблему более красиво? - person Nils Werner; 16.12.2015

Одна из лучших библиотек, которую мне удалось выделить, которая, к тому же, очень активно развивается, — это проект, извлеченный из boto: JMESPath. У него очень мощный синтаксис для выполнения действий, для выражения которых обычно требуются страницы кода.

Вот некоторые примеры:

search('foo | bar', {"foo": {"bar": "baz"}}) -> "baz"
search('foo[*].bar | [0]', {
    "foo": [{"bar": ["first1", "second1"]},
            {"bar": ["first2", "second2"]}]}) -> ["first1", "second1"]
search('foo | [0]', {"foo": [0, 1, 2]}) -> [0]
person nikolay    schedule 26.09.2014
comment
но это не позволяет изменять dict :( - person Gaetan; 22.11.2018

Сейчас есть более простой способ сделать это.

http://github.com/akesterson/dpath-python

$ easy_install dpath
>>> dpath.util.search(YOUR_DICTIONARY, "morefoo/morebar")

... сделано. Или, если вам не нравится возвращать результаты в представление (объединенный словарь, сохраняющий пути), вместо этого выведите их:

$ easy_install dpath
>>> for (path, value) in dpath.util.search(YOUR_DICTIONARY, "morefoo/morebar", yielded=True)

... и готово. 'value' будет содержать {'bacon': 'foobar'} в этом случае.

person Andrew Kesterson    schedule 12.05.2013
comment
Повторяющийся оператор не выполняется — в операторе for нет тела. - person Mittenchops; 02.07.2013

Существует более новая библиотека jsonpath-rw, поддерживающая JSONPATH, но для словарей и массивов Python, как вы пожелаете.

Таким образом, ваш 1-й пример становится:

from jsonpath_rw import parse

print( parse('$.morefoo.morebar').find(foo) )

И 2-й:

print( parse("$.morefoo[0].morebar.bacon").find(foo) )

PS: альтернативная более простая библиотека, также поддерживающая словари, — python-json-pointer с более похожим на XPath синтаксисом.

person ankostis    schedule 01.01.2014
comment
Обратите внимание, что jsonpath использует eval, а jsonpath-rw выглядит неподдерживаемым (также говорится, что некоторые функции отсутствуют, но я не пробовал). - person Sam Brightman; 19.08.2017

словарь › jmespath

Вы можете использовать JMESPath, который является языком запросов для JSON и имеет реализация Python.

import jmespath # pip install jmespath

data = {'root': {'section': {'item1': 'value1', 'item2': 'value2'}}}

jmespath.search('root.section.item2', data)
Out[42]: 'value2'

Синтаксис запроса jmespath и живые примеры: http://jmespath.org/tutorial.html

словарь › xml › xpath

Другим вариантом может быть преобразование ваших словарей в XML с помощью чего-то вроде dicttoxml, а затем использование обычных выражений XPath, например. через lxml или любую другую библиотеку, которую вы предпочитаете.

from dicttoxml import dicttoxml  # pip install dicttoxml
from lxml import etree  # pip install lxml

data = {'root': {'section': {'item1': 'value1', 'item2': 'value2'}}}
xml_data = dicttoxml(data, attr_type=False)
Out[43]: b'<?xml version="1.0" encoding="UTF-8" ?><root><root><section><item1>value1</item1><item2>value2</item2></section></root></root>'

tree = etree.fromstring(xml_data)
tree.xpath('//item2/text()')
Out[44]: ['value2']

Json указатель

Еще одним вариантом является Json Pointer, который представляет собой спецификация IETF с реализацией Python:

Из руководства по jsonpointer-python:

from jsonpointer import resolve_pointer

obj = {"foo": {"anArray": [ {"prop": 44}], "another prop": {"baz": "A string" }}}

resolve_pointer(obj, '') == obj
# True

resolve_pointer(obj, '/foo/another%20prop/baz') == obj['foo']['another prop']['baz']
# True

>>> resolve_pointer(obj, '/foo/anArray/0') == obj['foo']['anArray'][0]
# True

person ccpizza    schedule 08.07.2018
comment
проверяя это, так как я бы не хотел менять бэкэнд API, а проходить через выходной json - person swdev; 11.06.2020

Если вам нравится краткость:

def xpath(root, path, sch='/'):
    return reduce(lambda acc, nxt: acc[nxt],
                  [int(x) if x.isdigit() else x for x in path.split(sch)],
                  root)

Конечно, если у вас есть только словари, то это проще:

def xpath(root, path, sch='/'):
    return reduce(lambda acc, nxt: acc[nxt],
                  path.split(sch),
                  root)

Удачи в поиске ошибок в спецификации пути ;-)

person d1zzyg    schedule 01.02.2018
comment
Это позволит избежать преобразования вещей в целые числа, если узел является dict: def xpath(root, path, sep='/'): return reduce(lambda node, key: node[key if hasattr(node, 'keys') else int (ключ)], path.split(sep), root) - person samwyse; 28.03.2018
comment
Классное решение. Однако для Python 3 нужно from functools import reduce. - person Adrian W; 11.07.2018
comment
Мне нравится эта краткость — синтаксический анализатор должен выдавать ошибку key not found, когда спецификация пути неверна, поэтому отладка не должна быть очень болезненной. - person michaPau; 20.04.2019
comment
отличное решение, но ломается, когда у вас есть ключ словаря как целое число, например. в 1_ - person onesiumus; 06.11.2019
comment
Конечно, невозможно отличить, когда вводить ключ в список или ключ в словарь, не вводя больше синтаксиса в логику xquery. - person onesiumus; 06.11.2019

Другая альтернатива (помимо предложенной jellybean) это:

def querydict(d, q):
  keys = q.split('/')
  nd = d
  for k in keys:
    if k == '':
      continue
    if k in nd:
      nd = nd[k]
    else:
      return None
  return nd

foo = {
  'spam':'eggs',
  'morefoo': {
               'bar':'soap',
               'morebar': {'bacon' : 'foobar'}
              }
   }
print querydict(foo, "/morefoo/morebar")
person MarcoS    schedule 06.09.2011
comment
это должно быть решением - person bfmcneill; 24.11.2020

Потребуется дополнительная работа над тем, как будет работать XPath-подобный селектор. '/' является действительным ключом словаря, так как же

foo={'/':{'/':'eggs'},'//':'ham'}

обрабатываться?

foo.select("///")

было бы неоднозначно.

person unutbu    schedule 06.09.2011
comment
Да, для этого вам понадобится парсер. Но то, что я прошу, это метод xpath like. Меня устраивает morefoo.morebar. - person RickyA; 06.09.2011
comment
@RickyA: '.' также является ключом словаря значений. Такая же проблема будет. foo.select('...') будет двусмысленным. - person unutbu; 06.09.2011

Есть ли у вас какая-либо причина запрашивать его так же, как шаблон XPath? Как предположил комментатор вашего вопроса, это просто словарь, поэтому вы можете получить доступ к элементам в виде гнезда. Кроме того, учитывая, что данные представлены в формате JSON, вы можете использовать модуль simplejson для их загрузки и доступа к элементам.

Существует проект JSONPATH, который пытается помочь людям делать противоположное тому, что вы собираетесь делать ( учитывая XPATH, как сделать его легко доступным через объекты python), что кажется более полезным.

person Senthil Kumaran    schedule 06.09.2011
comment
Причина в том, что я хочу разделить данные и запрос. Я хочу быть гибким в части запроса. Если я обращаюсь к нему вложенным способом, запрос жестко запрограммирован в программе. - person RickyA; 06.09.2011
comment
@RickyA, в другом комментарии вы говорите, что morefoo.morebar в порядке. Вы проверили проект JSONPATH (скачайте и посмотрите исходники и тесты). - person Senthil Kumaran; 06.09.2011
comment
Я взглянул на JSONPATH, но мой ввод не text/json. Это вложенные словари. - person RickyA; 06.09.2011
comment
Например, вопрос @RickyA очень ценен при использовании mongodb. Если вы хотите перебирать вложенные ключи в документе BSON, это необходимо. - person Mittenchops; 02.07.2013

def Dict(var, *arg, **kwarg):
  """ Return the value of an (imbricated) dictionnary, if all fields exist else return "" unless "default=new_value" specified as end argument
      Avoid TypeError: argument of type 'NoneType' is not iterable
      Ex: Dict(variable_dict, 'field1', 'field2', default = 0)
  """
  for key in arg:
    if isinstance(var, dict) and key and key in var:  var = var[key]
    else:  return kwarg['default'] if kwarg and 'default' in kwarg else ""   # Allow Dict(var, tvdbid).isdigit() for example
  return kwarg['default'] if var in (None, '', 'N/A', 'null') and kwarg and 'default' in kwarg else "" if var in (None, '', 'N/A', 'null') else var

foo = {
  'spam':'eggs',
  'morefoo': {
               'bar':'soap',
               'morebar': {'bacon' : 'foobar'}
              }
   }
print Dict(foo, 'morefoo', 'morebar')
print Dict(foo, 'morefoo', 'morebar', default=None)

Имейте функцию SaveDict(value, var, *arg), которая может даже добавляться к спискам в dict...

person ZeroQI    schedule 23.09.2018