嵌套的Python字典的XPath查询

55
有没有办法为嵌套的Python字典定义XPath类型的查询?
就像这样:
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")

[编辑于2016年] 这个问题和被接受的答案都已经过时了。新的答案可能比原始答案更好地完成任务。然而,我没有测试它们,所以不会改变被接受的答案。

为什么不使用 foo['morefoo']['morebar'] - MarcoS
5
因为我想要做的是:def bla(query): data.select(query) - RickyA
@MarcoS 如果列表中的路径微语言返回多个项,那将更有趣。 - Pavel Šimerda
@PavelŠimerda 是的,更有趣,特别是使用通配符查询(查找特定键下的所有值),然后 - 还可以递归下降列表或[命名]元组... - Tomasz Gandor
这个问题(在Python中)本质上是要求推荐一个第三方库。 - user7610
11个回答

22

我能找到的最好的库之一,而且还在积极开发中,是从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]

但是这并不允许修改字典 :( - Gaetan

19

现在有一种更简单的方法来做这件事。

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

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

...完成。或者,如果你不喜欢将结果以视图(合并字典保留路径)的形式返回,请改为使用yield。

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

...完成后,此时'value'将包含{'bacon': 'foobar'}。


迭代语句不会运行——for语句中没有主体。 - Mittenchops

18

不是非常美观,但你可以使用类似这样的东西

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

    return elem

当然,这不支持像索引这样的XPath内容……更不用说unutbu指出的/键陷阱了。


2011年可能没有像今天这样多的选择,但在2014年,我认为用这种方式解决问题不够优雅,应该避免。 - nikolay
11
@nikolay这只是您的猜测吗?还是有更好的解决方案可以更好地解决这个问题? - Nils Werner

14

有一个较新的jsonpath-rw库,支持JSONPATH语法,但适用于Python的字典和数组,正如您所希望的。

因此,您的第一个示例变为:

from jsonpath_rw import parse

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

并且第二个:

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

PS: 还有一个支持字典的替代简化库是 python-json-pointer,它具有更类似 XPath 的语法。



1
请注意,jsonpath使用eval,而jsonpath-rw看起来没有维护(它还说缺少一些功能,但我还没有尝试过)。 - Sam Brightman

10

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

dict > xml > xpath

另一个选择是使用类似dicttoxml这样的工具将您的字典转换为XML,然后使用常规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 Pointer

另一个选项是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


检查这个,因为我不想改变后端API,而是遍历输出的JSON。 - swdev
将字典转换为XML,然后使用路径似乎不是一个好的做法。 - Miroslav Karpíšek

5
如果你喜欢简洁明了的话:
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)

祝你好运,尽管在你的路径规范中找到任何错误都很困难;-)


如果节点是字典,则避免将其转换为整数:def xpath(root, path, sep='/'): return reduce(lambda node, key: node[key if hasattr(node, 'keys') else int(key)], path.split(sep), root) - samwyse
酷炫的解决方案。对于 Python 3,需要 from functools import reduce - Adrian W
我喜欢这种简洁 - 当路径规范错误时,解析器应该给出“键未找到”错误,因此调试起来不应该很痛苦。 - michaPau
很好的解决方案,但当你的字典键是整数时会出现问题,例如在 d1 = {'a': {'1': {'c': {'d': {'e': 2}}}}, 'c': {'e': {}}} 中。 - onesiumus
当然,在不引入更多语法到XQuery逻辑中的情况下,无法区分何时要键入列表或键入字典。 - onesiumus

2

需要更多的工作来确定类似XPath的选择器如何工作。 '/'是一个有效的字典键,那么该怎么办?

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

如何处理?

foo.select("///")

会产生歧义。


是的,你需要一个解析器。但我所要求的是类似于xpath的方法。“morefoo.morebar”对我来说也可以。 - RickyA
3
@RickyA:'.' 也可以作为字典键的值。同样的问题仍然存在。foo.select('...') 将会是模棱两可的。 - unutbu

2

除了 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")

这是解决方案。 - bfmcneill

1

你为什么要像XPath模式一样查询它呢?正如评论者建议的那样,它只是一个字典,所以你可以以嵌套的方式访问元素。此外,考虑到数据是以JSON格式存在的,你可以使用simplejson模块来加载它并访问其中的元素。

有这个项目JSONPATH,它试图帮助人们做与你打算做的相反的事情(给定一个XPATH,如何使其易于通过Python对象访问),这似乎更有用。


1
原因是我想将数据和查询分开。我希望在查询部分具有灵活性。如果我以嵌套的方式访问它,查询将被硬编码到程序中。 - RickyA
@RickyA,在另一条评论中,你说morefoo.morebar没问题。你有检查过JSONPATH项目吗(下载并查看源代码和测试)? - Senthil Kumaran
我确实看了一下JSONPATH,但我的输入不是text/json格式的,而是嵌套字典。 - RickyA
@RickyA的问题在使用MongoDB时非常有价值,比如说,如果你想要迭代BSON文档中的嵌套键,那么这是必要的。 - Mittenchops

0
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) 函数,甚至可以追加列表到字典中...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接