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

Kev*_*eng 118 python namespaces find elementtree 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>
Run Code Online (Sandbox Code Playgroud)

示例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>
Run Code Online (Sandbox Code Playgroud)

虽然它可以工作,因为有一个名称空间"{http://www.test.com}",在每个标记前面添加一个名称空间是非常不方便的.

使用"find","findall"等方法时,如何忽略命名空间?

non*_*gon 50

而不是修改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:
    prefix, has_namespace, postfix = el.tag.partition('}')
    if has_namespace:
        el.tag = postfix  # strip all namespaces
root = it.root
Run Code Online (Sandbox Code Playgroud)

这是基于这里的讨论:http: //bugs.python.org/issue18304

  • 好的,这很好而且更先进,但它仍然不是'et.findall('{*} sometag')`.它还会破坏元素树本身,而不仅仅是"此时执行忽略命名空间的搜索,而不重新解析文档等,保留命名空间信息".那么,对于那种情况,您显然需要遍历树,并自行查看,如果节点在删除命名空间后符合您的意愿. (5认同)
  • 这个。这个这个这个。多个名称空间将使我丧命。 (2认同)
  • @TomaszGandor:也许您可以将命名空间添加到单独的属性中。对于简单的标签包含测试(*此文档包含此标签名称吗?*),这个解决方案很棒并且可以短路。 (2认同)

小智 47

如果在解析之前从xml中删除xmlns属性,那么树中的每个标记都不会添加名称空间.

import re

xmlstring = re.sub(' xmlns="[^"]+"', '', xmlstring, count=1)
Run Code Online (Sandbox Code Playgroud)

  • -1在解析之前通过正则表达式操作xml是错误的.虽然它可能在某些情况下有效,但这不应该是最受欢迎的答案,不应该在专业应用程序中使用. (40认同)
  • 这在很多情况下适用于我,但后来我遇到了多个命名空间和命名空间别名.请参阅我的答案,了解处理这些案例的另一种方法 (5认同)
  • 除了使用正则表达式进行 XML 解析作业本质上是不合理的这一事实之外,这对于许多 XML 文档来说也不起作用,因为它忽略了命名空间前缀,而且 XML 语法允许在属性之前使用任意空格。名称(不仅仅是空格)和“=”等号周围。 (2认同)

小智 18

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

import re
def get_namespace(element):
  m = re.match('\{.*\}', element.tag)
  return m.group(0) if m else ''
Run Code Online (Sandbox Code Playgroud)

并在find方法中使用它:

namespace = get_namespace(tree.getroot())
print tree.find('./{0}parent/{0}version'.format(namespace)).text
Run Code Online (Sandbox Code Playgroud)

  • 太多假设只有一个`命名空间' (13认同)

bar*_*rny 14

这是对nonagon的答案的扩展,它也删除了属性的名称空间:

from StringIO import StringIO
import xml.etree.ElementTree as ET

# instead of ET.fromstring(xml)
it = ET.iterparse(StringIO(xml))
for _, el in it:
    if '}' in el.tag:
        el.tag = el.tag.split('}', 1)[1]  # strip all namespaces
    for at in 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
Run Code Online (Sandbox Code Playgroud)


lij*_*jat 10

改进ericspod的答案:

我们可以将它包装在支持with构造的对象中,而不是全局更改解析模式.

from xml.parsers import expat

class DisableXmlNamespaces:
    def __enter__(self):
            self.oldcreate = expat.ParserCreate
            expat.ParserCreate = lambda encoding, sep: self.oldcreate(encoding, None)
    def __exit__(self, type, value, traceback):
            expat.ParserCreate = self.oldcreate
Run Code Online (Sandbox Code Playgroud)

然后可以如下使用

import xml.etree.ElementTree as ET
with DisableXmlNamespaces():
     tree = ET.parse("test.xml")
Run Code Online (Sandbox Code Playgroud)

这种方式的优点在于它不会改变with块之外的无关代码的任何行为.在使用ericspod的版本之后,我在使用expat之后在不相关的库中获得错误之后最终创建了这个.

  • 在Python 3.8(尚未测试其他版本)中,这似乎对我不起作用。查看源代码,它“应该”可以工作,但似乎“xml.etree.ElementTree.XMLParser”的源代码以某种方式进行了优化,并且猴子修补“expat”绝对没有效果。 (2认同)
  • 啊,是的。请参阅@barny的评论:/sf/ask/938874751/#comment96135721_48344935 (2认同)

小智 7

在 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)
Run Code Online (Sandbox Code Playgroud)

文档链接:- https://docs.python.org/3.5/library/xml.etree.elementtree.html#parsing-xml-with-namespaces

  • 这真的回答了OP“在每个标签前面添加命名空间非常不方便”吗? (2认同)

tzp*_*tzp 6

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

ns='http://www.test.com'
el2 = tree.findall("{%s}DEAL_LEVEL/{%s}PAID_OFF" %(ns,ns))
Run Code Online (Sandbox Code Playgroud)

或者,如果您确定PAID_OFF只出现在树中的一层:

el2 = tree.findall(".//{%s}PAID_OFF" % ns)
Run Code Online (Sandbox Code Playgroud)


est*_*est 5

我可能会迟到,但我认为这不是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
Run Code Online (Sandbox Code Playgroud)

这有点悲伤。

解决办法就是先把它去掉。

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)
Run Code Online (Sandbox Code Playgroud)

在 Python 3.6 上测试。

try如果在代码中的某个地方重新加载或导入模块两次时出现一些奇怪的错误,例如,Try语句很有用

  • 超出最大递归深度
  • 属性错误:XMLParser

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