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&qu...

问题描述:

我想使用 的方法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

使用此功能我们:

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

  2. 遍历创建的迭代器以获取命名空间字典,我们稍后可以传入每个字典find()或者按照 iMom0 的建议findall()进行调用。

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

我认为这是最好的方法,因为它不需要对源 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
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用