如何在 lxml 中将 xml 命名空间与 find/findall 一起使用?
-
25-09-2019 - |
题
我正在尝试解析 OpenOffice ODS 电子表格中的内容。ods 格式本质上只是一个包含许多文档的 zip 文件。电子表格的内容存储在“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: ':'
解决方案
如果root.nsmap
包含table
命名空间前缀,那么你可以:
root.xpath('.//table:table', namespaces=root.nsmap)
findall(path)
接受{namespace}name
语法,而不是namespace:name
。因此path
应该使用的命名空间的字典到{namespace}name
形式将它传递给findall()
之前进行预处理。
其他提示
下面是一种方式来获得的XML文档中的所有命名空间(并假定没有前缀冲突)。
我用这个解析XML文档,我知道事先的命名空间的网址是什么的时候,只有前缀。
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)
也许第一件事要注意的是名称空间是定义的 元素级别, , 不是 文档级别。
但是,最常见的是,所有名称空间都在文档的root元素中声明(office:document-content
在这里),这节省了我们解析所有内容以收集内部 xmlns
范围。
那么 nsmap 元素包括:
- 默认命名空间,带有
None
前缀(并不总是) - 所有祖先命名空间,除非被覆盖。
如果正如克里斯(Chrisr)提到的那样,不支持默认名称空间,则可以使用 听写理解 以更紧凑的表达方式过滤掉。
xpath 和的语法略有不同元素路径.
因此,这是您可以使用的代码来获取所有第一个表的行(已测试: 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)
如果有XML文件中没有
xmlns
定义Etree不会找到命名空间的元素。例如:
import lxml.etree as etree
xml_doc = '<ns:root><ns:child></ns:child></ns:root>'
tree = etree.fromstring(xml_doc)
# finds nothing:
tree.find('.//ns:root', {'ns': 'foo'})
tree.find('.//{foo}root', {'ns': 'foo'})
tree.find('.//ns:root')
tree.find('.//ns:root')
有时候这是你给出的数据。所以,你可以做什么时,有没有命名空间?
我的解决办法:添加一个
import lxml.etree as etree
xml_doc = '<ns:root><ns:child></ns:child></ns:root>'
xml_doc_with_ns = '<ROOT xmlns:ns="foo">%s</ROOT>' % xml_doc
tree = etree.fromstring(xml_doc_with_ns)
# finds what you're looking for:
tree.find('.//{foo}root')
不隶属于 StackOverflow