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


136

我想使用“ 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}”,但是在每个标签前面添加一个命名空间非常不方便。

使用“ find”,“ findall”等方法时,如何忽略名称空间?


18
tree.findall("xmlns:DEAL_LEVEL/xmlns:PAID_OFF", namespaces={'xmlns': 'http://www.test.com'})方便就够了吗?
iMom0 2012年

非常感谢。我尝试您的方法,它可以工作。比我的要方便,但还是有点尴尬。您是否知道ElementTree模块中没有其他合适的方法来解决此问题,还是根本没有这样的方法?
KevinLeng 2012年

或尝试tree.findall("{0}DEAL_LEVEL/{0}PAID_OFF".format('{http://www.test.com}'))
Warf

在Python 3.8中,通配符可用于名称空间。stackoverflow.com/a/62117710/407651
mzjn

Answers:


62

最好不要解析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

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

更新: rpartition而不是partition确保你得到的标签名postfix,即使没有命名空间。因此,您可以将其压缩:

for _, el in it:
    _, _, el.tag = el.tag.rpartition('}') # strip ns

2
这个。这个这个这个。多个名称空间将使我丧命。
2014年

8
好的,这很好而且更高级,但事实并非如此et.findall('{*}sometag')。而且,这也正在扭曲元素树本身,而不仅仅是“这次只是在忽略名称空间的情况下执行搜索,而无需重新解析文档等,而保留名称空间信息”。好吧,对于这种情况,您显然需要遍历树,并亲自查看节点在删除名称空间后是否符合您的要求。
Tomasz Gandor 2014年

1
这是通过剥离字符串来实现的,但是当我使用write(...)保存XML文件时,名称空间就消失了,而XML的请求就消失了xmlns =“ bla ”消失了。请咨询
TraceKira '16

@TomaszGandor:您可以将名称空间添加到单独的属性中。对于简单的标签包含测试(此文档中是否包含该标签名称?),这种解决方案非常有用,并且可以短路。
马丁·彼得斯

@TraceKira:此技术会从解析的文档中删除名称空间,您不能使用它来创建带有名称空间的新XML字符串。可以将名称空间值存储在一个额外的属性中(并在将XML树变回字符串之前放回名称空间),或者从原始源重新解析以对基于剥离树的更改应用更改。
马丁·彼得斯

48

如果您在解析前从xml中删除xmlns属性,则树中的每个标记都将没有命名空间。

import re

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

5
这对我来说在很多情况下都是可行的,但是后来我遇到了多个名称空间和名称空间别名。请参阅我的答案以获取处理这些情况的另一种方法。
nonagon 2014年

47
-1在解析之前通过正则表达式操作xml是错误的。尽管在某些情况下可能会起作用,但这不应是投票最多的答案,也不应该在专业应用程序中使用。
迈克,

1
除了使用正则表达式进行XML解析工作本质上是健全的事实外,对于许多XML文档而言,这是行不通的,因为它忽略了名称空间前缀,并且XML语法允许在属性名称之前使用任意空格(不仅仅是空格)和=等号周围。
马丁·彼得斯

是的,它既快速又肮脏,但是对于简单的用例,它绝对是最优雅的解决方案,谢谢!
6

18

到目前为止,答案明确地将名称空间值放在脚本中。对于更通用的解决方案,我宁愿从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

15
太多的假设只是一个namespace
Kashyap 2014年

这没有考虑到嵌套标记可以使用不同的名称空间。
马丁·彼得斯

15

这是对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 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

UPDATE:已添加,list()以便迭代器可以工作(Python 3所需)


14

改善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

然后可以按如下方式使用

import xml.etree.ElementTree as ET
with DisableXmlNamespaces():
     tree = ET.parse("test.xml")

这种方式的优点在于,它不会更改with块之外无关代码的任何行为。我使用了ericspod的版本(在此同时也使用了expat)在不相关的库中出现错误之后,最终创建了该代码。


这既甜又健康!拯救了我的一天!+1
AndreasT '18

在Python 3.8(未经其他版本测试)中,这似乎对我不起作用。从源代码上看,它应该可以工作,但似乎源代码已xml.etree.ElementTree.XMLParser在某种程度上进行了优化,而猴子补丁expat绝对没有任何作用。
Reinderien

嗯是的 见@巴尼的评论:stackoverflow.com/questions/13412496/...
Reinderien

5

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

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)

2

如果不使用ElementTree,则cElementTree可以通过替换来强制Expat忽略名称空间处理ParserCreate()

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

ElementTree尝试通过调用来使用Expat,ParserCreate()但没有提供不提供名称空间分隔符字符串的选项,以上代码将导致其被忽略,但被警告可能会破坏其他情况。


与当前的其他答案相比,这是一种更好的方法,因为它不依赖于字符串处理
lijat

3
在蟒蛇3.7.2(也可能是早期的教)AFAICT它已经不再可能避免使用cElementTree,所以这种解决方法可能无法:-(
巴尼

1
不推荐使用cElemTree,但是使用C加速器完成的类型存在阴影。C代码没有调用expat,所以这个解决方案被打破了。
ericspod

@barny仍然有可能,ElementTree.fromstring(s, parser=None)我正在尝试将解析器传递给它。
est

2

我为此可能会迟到,但我认为这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如果在代码的某处重新加载或导入模块两次而遇到一些奇怪的错误,例如try 语句,则很有用

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

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


1

让我们结合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()调用iMom0的名称空间。

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

我认为这是最好的方法,因为无论源XML还是解析后的xml.etree.ElementTree输出都不会受到任何操纵。

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


找到了如何使用它,但对我不起作用,我仍然在输出中看到名称空间
taiko

1
查看iMom0对OP问题的评论。使用此函数,您既可以获取已解析的对象,又可以使用find()和查询该对象findall()。您只需parse_xml()使用名称空间的dict来提供这些方法,并在查询中使用名称空间的前缀。例如:et_element.findall(".//some_ns_prefix:some_xml_tag", namespaces=xml_namespaces)
z33k
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.