如何在Python中以无命名空间的方式通过XPath查找XML元素?

19 投票
2 回答
22429 浏览
提问于 2025-04-16 15:13

因为我第二次遇到这个烦人的问题,所以我觉得问一下会有帮助。

有时候我需要从XML文档中获取元素,但这样做的方法总是很麻烦。

我想知道有没有一个Python库可以满足我的需求,能够优雅地构造我的XPath,自动注册命名空间前缀,或者在内置的XML实现或lxml中有个隐藏的选项可以完全去掉命名空间。如果你已经知道我想要什么,那我就不多解释了 :)

示例文档:

<root xmlns="http://really-long-namespace.uri"
  xmlns:other="http://with-ambivalent.end/#">
    <other:elem/>
</root>

我能做什么

ElementTree API是我知道的唯一一个内置的支持XPath查询的工具。但它要求我使用“UNames”。看起来像这样:/{http://really-long-namespace.uri}root/{http://with-ambivalent.end/#}elem

如你所见,这些名字非常冗长。我可以通过以下方式缩短它们:

default_ns = "http://really-long-namespace.uri"
other_ns   = "http://with-ambivalent.end/#"
doc.find("/{{{0}}}root/{{{1}}}elem".format(default_ns, other_ns))

但这样做既{{{难看}}}又不稳定,因为http…end/#http…end#http…end/http…end,我怎么知道会用哪个变体呢?

另外,lxml支持命名空间前缀,但它既不使用文档中的前缀,也没有提供处理默认命名空间的自动方法。我仍然需要从每个命名空间中获取一个元素才能从文档中提取它。命名空间属性不会被保留,所以也无法自动从中获取。

还有一种与命名空间无关的XPath查询方式,但它既冗长又难看,并且在内置实现中不可用:/*[local-name() = 'root']/*[local-name() = 'elem']

我想做什么

我想找到一个库、选项或通用的XPath变换函数,以便通过输入以下内容来实现上面的例子……

  1. 没有命名空间:/root/elem
  2. 文档中的命名空间前缀:/root/other:elem

……再加上可能的一些声明,表明我确实想使用文档的前缀或去掉命名空间。

进一步说明:虽然我现在的用例很简单,但将来我会需要使用更复杂的情况。

谢谢你的阅读!


解决方案

用户samplebias让我注意到py-dom-xpath;正是我在寻找的东西。我的实际代码现在看起来是这样的:

#parse the document into a DOM tree
rdf_tree = xml.dom.minidom.parse("install.rdf")
#read the default namespace and prefix from the root node
context = xpath.XPathContext(rdf_tree)

name    = context.findvalue("//em:id", rdf_tree)
version = context.findvalue("//em:version", rdf_tree)

#<Description/> inherits the default RDF namespace
resource_nodes = context.find("//Description/following-sibling::*", rdf_tree)

与文档一致,简单,支持命名空间;完美。

2 个回答

2

首先,关于“你想做什么”:

  1. 没有命名空间的情况:/root/elem -> 这里应该没什么问题。
  2. 来自文档的命名空间前缀:/root/other:elem -> 这就有点麻烦了,你不能仅仅使用“来自文档的命名空间前缀”。即使在同一个文档中:
    • 带命名空间的元素不一定都有前缀。
    • 相同的前缀不一定总是对应同一个命名空间的URI。
    • 相同的命名空间URI不一定总是有相同的前缀。

顺便说一下:如果你想获取某个元素的前缀映射,可以在lxml中尝试使用elem.nsmap。另外,lxml.etree中的iterparse和iterwalk方法可以用来“通知”你命名空间的声明。

14

*[local-name() = "elem"] 这种写法应该是可以用的,不过为了让事情简单一些,你可以创建一个函数来帮助你更方便地写出部分或完整的“通配符命名空间”XPath表达式。

我在Ubuntu 10.04上使用python-lxml 2.2.4,下面的脚本对我来说是有效的。你需要根据自己的需求来调整每个元素的默认命名空间的设置,同时处理你想要合并到表达式中的其他XPath语法:

import lxml.etree

def xpath_ns(tree, expr):
    "Parse a simple expression and prepend namespace wildcards where unspecified."
    qual = lambda n: n if not n or ':' in n else '*[local-name() = "%s"]' % n
    expr = '/'.join(qual(n) for n in expr.split('/'))
    nsmap = dict((k, v) for k, v in tree.nsmap.items() if k)
    return tree.xpath(expr, namespaces=nsmap)

doc = '''<root xmlns="http://really-long-namespace.uri"
    xmlns:other="http://with-ambivalent.end/#">
    <other:elem/>
</root>'''

tree = lxml.etree.fromstring(doc)
print xpath_ns(tree, '/root')
print xpath_ns(tree, '/root/elem')
print xpath_ns(tree, '/root/other:elem')

输出结果:

[<Element {http://really-long-namespace.uri}root at 23099f0>]
[<Element {http://with-ambivalent.end/#}elem at 2309a48>]
[<Element {http://with-ambivalent.end/#}elem at 2309a48>]

更新:如果你发现确实需要解析XPath,可以看看像py-dom-xpath这样的项目,它是一个纯Python实现的(大部分)XPath 1.0。至少这会让你对解析XPath的复杂性有一些了解。

撰写回答