Python 元素树模块:使用方法时如何忽略 XML 文件的命名空间以查找匹配的元素 "find" , "findall"



我想使用 findall 的方法在 ElementTree 模块中定位源 xml 文件的某些元素。

但是,源 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}" 有效,但在每个标签前面添加一个命名空间非常不方便。

使用findfindall、...等函数时如何忽略命名空间?

与其修改 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

这是基于此处的讨论。

如果在

解析 xml 之前从 xml 中删除 xmlns 属性,则树中的每个标记都不会预置命名空间。

import re
xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)

到目前为止,答案明确地将命名空间值放在脚本中。对于更通用的解决方案,我宁愿从 xml 中提取命名空间:

import re
def get_namespace(element):
  m = re.match('{.*}', element.tag)
  return m.group(0) if m else ''

并在查找方法中使用它:

namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text

以下是@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的永久污损,但如果这是可以接受的,因为没有非唯一的标签名称,并且因为您不会编写需要原始命名空间的文件,那么这可以使访问它变得更加容易

改进 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。

您也可以使用优雅的字符串格式构造:

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)

在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

我可能会迟到,但我认为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上测试。

尝试try语句很有用,以防万一您在代码中的某个地方重新加载或导入模块两次,您遇到一些奇怪的错误,例如

  • 超出最大递归深度
  • AttributeError: XMLParser

顺便说一句,该死的etree源代码看起来真的很混乱。

如果您使用的是ElementTree而不是cElementTree您可以通过替换ParserCreate()来强制 Expat 忽略命名空间处理:

from xml.parsers import expat
oldcreate = expat.ParserCreate
expat.ParserCreate = lambda encoding, sep: oldcreate(encoding, None)

ElementTree尝试通过调用 ParserCreate() Expat 来使用 Expat,但没有提供不提供命名空间分隔符字符串的选项,上面的代码将导致它被忽略,但请注意这可能会破坏其他内容。

让我们将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

使用此功能,我们:

  1. 创建一个迭代器来获取命名空间和解析的树对象

  2. 遍历创建的迭代器以获得我们可以得到的命名空间字典稍后通过每个find()findall()呼叫,如妈妈0.

  3. 返回已分析树的根元素对象和命名空间。

我认为这是最好的方法,因为既没有对源 XML 进行操作,也没有涉及任何结果解析xml.etree.ElementTree输出。

我还想归功于 balmy 的回答,它提供了这个难题的重要组成部分(您可以从迭代器中获取解析的根)。在此之前,我实际上在我的应用程序中遍历了两次XML树(一次用于获取命名空间,第二次用于根)。

从 xml.etree.ElementTree 3.8 版本开始,可以使用通配符命名空间查询节点。

{命名空间}* 选择给定命名空间中的所有标签,{}spam 选择在任何(或无)命名空间中名为垃圾邮件的标记,并且 {} 仅选择标记不在命名空间中。

所以它会是:

tree.findall('.//{*} DEAL_LEVEL')
忽略

根节点中的默认命名空间,将修补的根节点启动提供给分析器,然后继续分析原始 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"n".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"""
<?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

只是偶然地进入了这里的答案: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

最新更新