如何在lxml中使用find/findall和XML命名空间?

38 投票
4 回答
39374 浏览
提问于 2025-04-16 07:15

我正在尝试解析一个OpenOffice ODS电子表格中的内容。ODS格式其实就是一个压缩文件,里面包含了多个文档。电子表格的内容存储在一个叫做'content.xml'的文件里。

import zipfile
from lxml import etree

zf = zipfile.ZipFile('spreadsheet.ods')
root = etree.parse(zf.open('content.xml'))

电子表格的内容在一个单元格里:

table = root.find('.//{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table')

我们也可以直接获取行的信息:

rows = root.findall('.//{urn:oasis:names:tc:opendocument:xmlns:table:1.0}table-row')

每个元素都知道它们的命名空间:

>>> table.nsmap['table']
'urn:oasis:names:tc:opendocument:xmlns:table:1.0'

我该如何在find/findall中直接使用这些命名空间呢?

显而易见的解决方案并不奏效。

尝试从表格中获取行:

>>> root.findall('.//table:table')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "lxml.etree.pyx", line 1792, in lxml.etree._ElementTree.findall (src/lxml/lxml.etree.c:41770)
  File "lxml.etree.pyx", line 1297, in lxml.etree._Element.findall (src/lxml/lxml.etree.c:37027)
  File "/usr/lib/python2.6/dist-packages/lxml/_elementpath.py", line 225, in findall
    return list(iterfind(elem, path))
  File "/usr/lib/python2.6/dist-packages/lxml/_elementpath.py", line 200, in iterfind
    selector = _build_path_iterator(path)
  File "/usr/lib/python2.6/dist-packages/lxml/_elementpath.py", line 184, in _build_path_iterator
    selector.append(ops[token[0]](_next, token))
KeyError: ':'

4 个回答

11

这里有一种方法可以获取XML文档中的所有命名空间(假设没有前缀冲突)。

我在解析XML文档时使用这个方法,前提是我事先知道命名空间的URL,只需要知道前缀就可以了。

        doc = etree.XML(XML_string)

        # Getting all the name spaces.
        nsmap = {}
        for ns in doc.xpath('//namespace::*'):
            if ns[0]: # Removes the None namespace, neither needed nor supported.
                nsmap[ns[0]] = ns[1]
        doc.xpath('//prefix:element', namespaces=nsmap)
17

首先需要注意的是,命名空间是在元素级别定义的,而不是在文档级别

通常情况下,所有的命名空间都会在文档的根元素中声明(这里是office:document-content),这样我们就不用逐个解析里面的内容来收集所有的xmlns范围了。

然后,一个元素的命名空间映射包括:

  • 一个默认的命名空间,前缀是None(并不总是如此)
  • 所有祖先的命名空间,除非被覆盖。

如果像ChrisR提到的那样,默认命名空间不被支持,你可以使用字典推导来将其过滤掉,这样写起来会更简洁。

在xpath和ElementPath中,你会看到稍微不同的语法。


下面是你可以用来获取第一个表格所有行的代码(测试版本:lxml=3.4.2):

import zipfile
from lxml import etree

# Open and parse the document
zf = zipfile.ZipFile('spreadsheet.ods')
tree = etree.parse(zf.open('content.xml'))

# Get the root element
root = tree.getroot()

# get its namespace map, excluding default namespace
nsmap = {k:v for k,v in root.nsmap.iteritems() if k}

# use defined prefixes to access elements
table = tree.find('.//table:table', nsmap)
rows = table.findall('table:table-row', nsmap)

# or, if xpath is needed:
table = tree.xpath('//table:table', namespaces=nsmap)[0]
rows = table.xpath('table:table-row', namespaces=nsmap)
32

如果 root.nsmap 里有 table 这个命名空间前缀,那么你可以这样做:

root.xpath('.//table:table', namespaces=root.nsmap)

findall(path) 可以接受 {namespace}name 这种写法,而不是 namespace:name。所以,在把 path 传给 findall() 之前,应该先用命名空间字典把它处理成 {namespace}name 这种格式。

撰写回答