Python ElementTree 模块:使用方法“find”、“findall”时如何忽略 XML 文件的命名空间来定位匹配元素
- 2025-01-16 08:38:00
- admin 原创
- 109
问题描述:
我想使用 的方法findall
来定位模块中源 xml 文件的某些元素ElementTree
。
但是源 xml 文件(test.xml)有命名空间,我截断部分 xml 文件作为示例:
<?xml version="1.0" encoding="iso-8859-1"?>
<XML_HEADER xmlns="http://www.test.com">
<TYPE>Updates</TYPE>
<DATE>9/26/2012 10:30:34 AM</DATE>
<COPYRIGHT_NOTICE>All Rights Reserved.</COPYRIGHT_NOTICE>
<LICENSE>newlicense.htm</LICENSE>
<DEAL_LEVEL>
<PAID_OFF>N</PAID_OFF>
</DEAL_LEVEL>
</XML_HEADER>
示例 Python 代码如下:
from xml.etree import ElementTree as ET
tree = ET.parse(r"test.xml")
el1 = tree.findall("DEAL_LEVEL/PAID_OFF") # Return None
el2 = tree.findall("{http://www.test.com}DEAL_LEVEL/{http://www.test.com}PAID_OFF") # Return <Element '{http://www.test.com}DEAL_LEVEL/PAID_OFF' at 0xb78b90>
虽然使用"{http://www.test.com}"
有效,但是在每个标签前面添加命名空间非常不方便。
如何在使用诸如、、……之类的函数时忽略命名find
空间findall
?
解决方案 1:
最好不要修改 XML 文档本身,而是先解析它,然后修改结果中的标签。这样您就可以处理多个命名空间和命名空间别名:
from io import StringIO # for Python 2 import from StringIO instead
import xml.etree.ElementTree as ET
# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
_, _, el.tag = el.tag.rpartition('}') # strip ns
root = it.root
这是基于此处的讨论。
解决方案 2:
如果在解析 xml 之前从 xml 中删除 xmlns 属性,那么树中每个标签前面就不会添加命名空间。
import re
xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)
解决方案 3:
到目前为止,答案都明确地将命名空间值放在脚本中。对于更通用的解决方案,我宁愿从 xml 中提取命名空间:
import re
def get_namespace(element):
m = re.match('{.*}', element.tag)
return m.group(0) if m else ''
并在 find 方法中使用它:
namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text
解决方案 4:
这是@nonagon 答案的扩展(从标签中删除命名空间),也可以从属性中删除命名空间:
import io
import xml.etree.ElementTree as ET
# instead of ET.fromstring(xml)
it = ET.iterparse(io.StringIO(xml))
for _, el in it:
if '}' in el.tag:
el.tag = el.tag.split('}', 1)[1] # strip all namespaces
for at in list(el.attrib.keys()): # strip namespaces of attributes too
if '}' in at:
newat = at.split('}', 1)[1]
el.attrib[newat] = el.attrib[at]
del el.attrib[at]
root = it.root
显然,这是对 XML 的永久性破坏,但如果这是可以接受的,因为没有非唯一的标记名称,并且因为你不会编写需要原始命名空间的文件,那么这可以使访问它变得容易得多
解决方案 5:
改进ericspod 的答案:
我们不需要全局改变解析模式,而是可以将其包装在支持 with 构造的对象中。
from xml.parsers import expat
class DisableXmlNamespaces:
def __enter__(self):
self.old_parser_create = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: self.old_parser_create(encoding, None)
def __exit__(self, type, value, traceback):
expat.ParserCreate = self.oldcreate
然后可以按如下方式使用
import xml.etree.ElementTree as ET
with DisableXmlNamespaces():
tree = ET.parse("test.xml")
这种方式的优点在于它不会改变 with 块之外的无关代码的任何行为。在使用 ericspod 的版本(碰巧也使用了 expat)后,我在无关库中遇到错误,最终创建了这个。
解决方案 6:
在 Python 3.5 中,你可以将命名空间作为参数传递find()
。例如,
ns= {'xml_test':'http://www.test.com'}
tree = ET.parse(r"test.xml")
el1 = tree.findall("xml_test:DEAL_LEVEL/xml_test:PAID_OFF",ns)
文档链接:-https://docs.python.org/3.5/library/xml.etree.elementtree.html#parsing-xml-with-namespaces
解决方案 7:
您也可以使用优雅的字符串格式化构造:
ns='http://www.test.com'
el2 = tree.findall("{%s}DEAL_LEVEL/{%s}PAID_OFF" %(ns,ns))
或者,如果您确定PAID_OFF仅出现在树的一个级别中:
el2 = tree.findall(".//{%s}PAID_OFF" % ns)
解决方案 8:
自Python 3.8 中的 xml.etree.ElementTree开始,您可以使用通配符命名空间查询节点。
{namespace}*
选择给定命名空间中的所有标签,{*}spam
选择任何(或无)命名空间中名为 spam 的标签,并且{}*
仅选择不在命名空间中的标签。
因此它将是:
tree.findall('.//{*}DEAL_LEVEL')
解决方案 9:
我可能会迟到,但我不认为re.sub
这是一个好的解决方案。
然而,重写xml.parsers.expat
不适用于 Python 3.x 版本,
罪魁祸首是xml/etree/ElementTree.py
参见源代码底部
# Import the C accelerators
try:
# Element is going to be shadowed by the C implementation. We need to keep
# the Python version of it accessible for some "creative" by external code
# (see tests)
_Element_Py = Element
# Element, SubElement, ParseError, TreeBuilder, XMLParser
from _elementtree import *
except ImportError:
pass
这有点令人伤心。
解决办法是先将其除去。
import _elementtree
try:
del _elementtree.XMLParser
except AttributeError:
# in case deleted twice
pass
else:
from xml.parsers import expat # NOQA: F811
oldcreate = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)
在 Python 3.6 上测试。
Trytry
语句很有用,如果你在代码中的某处重新加载或导入模块两次,你会得到一些奇怪的错误,例如
超出最大递归深度
属性错误:XMLParser
顺便说一句,etree 源代码看起来真的很乱。
解决方案 10:
如果你正在使用ElementTree
而不是,cElementTree
你可以通过替换来强制 Expat 忽略命名空间处理ParserCreate()
:
from xml.parsers import expat
oldcreate = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)
ElementTree
尝试通过调用来使用 Expat,ParserCreate()
但没有提供不提供命名空间分隔符字符串的选项,上述代码将导致它被忽略,但请注意这可能会破坏其他东西。
解决方案 11:
让我们将nonagon 的回答与mzjn 对相关问题的回答结合起来:
def parse_xml(xml_path: Path) -> Tuple[ET.Element, Dict[str, str]]:
xml_iter = ET.iterparse(xml_path, events=["start-ns"])
xml_namespaces = dict(prefix_namespace_pair for _, prefix_namespace_pair in xml_iter)
return xml_iter.root, xml_namespaces
使用此功能我们:
创建一个迭代器来获取命名空间和已解析的树对象。
遍历创建的迭代器以获取命名空间字典,我们稍后可以传入每个字典
find()
或者按照 iMom0 的建议findall()
进行调用。返回解析树的根元素对象和命名空间。
我认为这是最好的方法,因为它不需要对源 XML 或生成的解析xml.etree.ElementTree
输出进行任何操作。
我还想感谢balmy 的回答,他提供了这个难题的一个关键部分(您可以从迭代器中获取解析后的根)。在此之前,我实际上在我的应用程序中遍历了 XML 树两次(一次是为了获取命名空间,第二次是为了获取根)。
解决方案 12:
忽略根节点中的默认命名空间,将修补的根节点起始提供给解析器,然后继续解析原始 XML 流。
例如,而不是<XML_HEADER xmlns="http://www.test.com">
,而是提供<XML_HEADER>
给解析器。
限制:只能忽略默认命名空间。当文档包含像这样的命名空间前缀节点时<some-ns:some-name>
,lxml 将抛出lxml.etree.XMLSyntaxError: Namespace prefix some-ns on some-name is not defined
。
限制:目前,这会忽略来自的原始编码<?xml encoding="..."?>
。
#! /usr/bin/env python3
import lxml.etree
import io
def parse_xml_stream(xml_stream, ignore_default_ns=True):
"""
ignore_default_ns:
ignore the default namespace of the root node.
by default, lxml.etree.iterparse
returns the namespace in every element.tag.
with ignore_default_ns=True,
element.tag returns only the element's localname,
without the namespace.
example:
xml_string:
<html xmlns="http://www.w3.org/1999/xhtml">
<div>hello</div>
</html>
with ignore_default_ns=False:
element.tag = "{http://www.w3.org/1999/xhtml}div"
with ignore_default_ns=True:
element.tag = "div"
see also:
Python ElementTree module: How to ignore the namespace of XML files
https://stackoverflow.com/a/76601149/10440128
"""
# save the original read method
xml_stream_read = xml_stream.read
if ignore_default_ns:
def xml_stream_read_track(_size):
# ignore size, always return 1 byte
# so we can track node positions
return xml_stream_read(1)
xml_stream.read = xml_stream_read_track
def get_parser(stream):
return lxml.etree.iterparse(
stream,
events=('start', 'end'),
remove_blank_text=True,
huge_tree=True,
)
if ignore_default_ns:
# parser 1
parser = get_parser(xml_stream)
# parse start of root node
event, element = next(parser)
#print(xml_stream.tell(), event, element)
# get name of root node
root_name = element.tag.split("}")[-1]
#print("root name", root_name)
#print("root pos", xml_stream.tell()) # end of start-tag
# attributes with namespaces
#print("root attrib", element.attrib)
# patched document header without namespaces
xml_stream_nons = io.BytesIO(b"
".join([
#b"""<?xml version="1.0" encoding="utf-8"?>""",
b"<" + root_name.encode("utf8") + b"><dummy/>",
]))
xml_stream.read = xml_stream_nons.read
# parser 2
parser = get_parser(xml_stream)
# parse start of root node
# note: if you only need "end" events,
# then wait for end of dummy node
event, element = next(parser)
print(event, element.tag)
assert event == "start"
if ignore_default_ns:
assert element.tag == root_name
# parse start of dummy node
event, element = next(parser)
#print(event, element.tag)
assert event == "start"
assert element.tag == "dummy"
# parse end of dummy node
event, element = next(parser)
#print(event, element.tag)
assert event == "end"
assert element.tag == "dummy"
# restore the original read method
xml_stream.read = xml_stream_read
# now all elements come without namespace
# so element.tag is the element's localname
#print("---")
# TODO handle events
#for i in range(5):
# event, element = next(parser)
# print(event, element)
for event, element in parser:
print(event, element.tag)
# xml with namespace in root node
xml_bytes = b"""\n<?xml version="1.0" encoding="utf-8"?>
<doc version="1" xmlns="http://www.test.com">
<node/>
<!--
limitation: this breaks the parser.
lxml.etree.XMLSyntaxError:
Namespace prefix some-ns on some-name is not defined
<some-ns:some-name/>
-->
</doc>
"""
print("# keep default namespace")
parse_xml_stream(io.BytesIO(xml_bytes), False)
print()
print("# ignore default namespace")
parse_xml_stream(io.BytesIO(xml_bytes))
输出print(event, element.tag)
:
# keep default namespace
start {http://www.test.com}doc
start {http://www.test.com}node
end {http://www.test.com}node
end {http://www.test.com}doc
# ignore default namespace
start doc
start node
end node
end doc
解决方案 13:
只是偶然掉进了这里的答案:XSD 条件类型分配默认类型混淆?这不是主题问题的确切答案,但如果命名空间不重要,则可能适用。
<?xml version="1.0" encoding="UTF-8"?>
<persons xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="test.xsd">
<person version="1">
<firstname>toto</firstname>
<lastname>tutu</lastname>
</person>
</persons>
另请参阅: https: //www.w3.org/TR/xmlschema-1/#xsi_schemaLocation
对我来说很管用。我在应用程序中调用 XML 验证过程。但我还想在编辑 XML 时快速查看 PyCharm 中的验证高亮显示和自动完成功能。此noNamespaceSchemaLocation
属性满足了我的需求。
重新检查
from xml.etree import ElementTree as ET
tree = ET.parse("test.xml")
el1 = tree.findall("person/firstname")
print(el1[0].text)
el2 = tree.find("person/lastname")
print(el2.text)
回归者
>python test.py
toto
tutu