在 Python 中使用 XML 命名空间时,如何使我的代码更具可读性和 DRYer?

How can I make my code more readable and DRYer when working with XML namespaces in Python?

Python 的内置 xml.etree 包支持解析带有命名空间的 XML 文件,但命名空间前缀会扩展为括号中的完整 URI。所以在官方文档中的示例文件中:

<actors xmlns:fictional="http://characters.example.com"
    xmlns="http://people.example.com">
    <actor>
        <name>John Cleese</name>
        <fictional:character>Lancelot</fictional:character>
        <fictional:character>Archie Leach</fictional:character>
    </actor>
    ...

actor 标签扩展为 {http://people.example.com}actorfictional:character 扩展为 {http://characters.example.com}character

我可以看到这如何使一切都非常明确并减少歧义(文件可以具有相同的名称空间但不同的前缀等)但是使用起来非常麻烦。 Element.find() 方法和其他方法允许将 dict 映射前缀传递给名称空间 URI,因此我仍然可以执行 element.find('fictional:character', nsmap),但据我所知,标签属性没有任何相似之处。这会导致烦人的事情,比如 element.attrib['{{{}}}attrname'.format(nsmap['prefix'])].

流行的 lxml 包提供了相同的 API 和一些扩展,其中之一是对从文档继承的元素的 nsmap 属性 .然而,none 的方法似乎实际上利用了它,所以我仍然必须做 element.find('fictional:character', element.nsmap),这只是不必要的重复,每次都要输入。它也仍然不适用于属性。

幸运的是 lxml 支持子类化 BaseElement,所以我只做了一个带有 p(前缀)属性 的 属性 具有相同的 API 但是使用元素的 nsmap 自动使用命名空间前缀(编辑: 可能最好分配代码中定义的自定义 nsmap)。所以我只做 element.p.find('fictional:character')element.p.attrib['prefix:attrname'],这样重复性要低得多,而且我认为可读性更高。

我只是觉得我真的遗漏了一些东西,真的感觉这应该已经是 lxml 的一个特性,如果不是内置的 etree 包的话。我是不是做错了什么?

是否可以去掉命名空间映射?

是否需要将其作为参数传递到每个函数调用中?一个选项是在 属性 中设置要在 XML 文档中使用的前缀。

在将 XML 文档传递给第 3 方函数之前,这很好。该函数也想使用前缀,因此它将 属性 设置为其他内容,因为它不知道您将其设置为什么。

您取回 XML 文档后,它已被修改,因此您的前缀不再有效。

总而言之:不,它不安全,因此它很好。

这种设计不仅存在于Python中,它也存在于.NET中。 SelectNodes() [MSDN] can be used if you don't need prefixes. But as soon as there's a prefix present, it'll throw an exception. Therefore, you have to use the overloaded SelectNodes() [MSDN] 使用 XmlNamespaceManager 作为参数。

XPath 作为解决方案

我建议学习XPath (lxml specific link),在那里你可以使用前缀。由于这可能是特定于版本的,让我说我 运行 这段代码使用 Python 2.7 x64 和 lxml 3.6.0(我不太熟悉 Python,所以这可能不是最干净的代码,但它可以很好地作为演示):

from lxml import etree as ET
from pprint import pprint
data = """<?xml version="1.0"?>
<d:data xmlns:d="dns">
    <country name="Liechtenstein">
        <rank>1</rank>
        <year>2008</year>
        <gdppc>141100</gdppc>
        <neighbor d:name="Austria" direction="E"/>
        <neighbor name="Switzerland" direction="W"/>
    </country>
    <country name="Singapore">
        <rank>4</rank>
        <year>2011</year>
        <gdppc>59900</gdppc>
        <neighbor name="Malaysia" direction="N"/>
    </country>
</d:data>"""
root = ET.fromstring(data)
my_namespaces = {'x':'dns'}
xp=root.xpath("/x:data/country/neighbor/@x:name", namespaces=my_namespaces)
pprint(xp)
xp=root.xpath("//@x:name", namespaces=my_namespaces)
pprint(xp)
xp=root.xpath("/x:data/country/neighbor/@name", namespaces=my_namespaces)
pprint(xp)

输出为

C:\Python27x64\python.exe E:/xpath.py
['Austria']
['Austria']
['Switzerland', 'Malaysia']

Process finished with exit code 0

注意 XPath 如何很好地解决了从命名空间 table 中的 x 前缀到 XML 文档中的 d 前缀的映射。

这消除了真正可怕的阅读 element.attrib['{{{}}}attrname'.format(nsmap['prefix'])]

简短(且不完整)的 XPath 介绍

到select一个元素,写/element,可选择使用前缀。

xp=root.xpath("/x:data", namespaces=my_namespaces)

要select一个属性,写/@attribute,可以选择使用前缀。

#See example above

要向下导航,请连接多个元素。如果您不知道中间的项目,请使用 //。要向上移动,请使用 /..。如果后面没有 /...

,则属性必须放在最后
xp=root.xpath("/x:data/country/neighbor/@x:name/..", namespaces=my_namespaces)

要使用条件,请将其写在方括号中。 /element[@attribute] 表示:select 所有具有该属性的元素。 /element[@attribute='value'] 表示:select 所有具有该属性且该属性具有特定值的元素。 /element[./subelement] 表示:select 具有特定名称子元素的所有元素。可选择在任何地方使用前缀。

xp=root.xpath("/x:data/country[./neighbor[@name='Switzerland']]/@name", namespaces=my_namespaces)

还有很多东西有待发现,比如 text(),兄弟 selection 甚至函数的各种方式。

关于'why'

原来的问题标题是

Why does working with XML namespaces seem so difficult in Python?

对于一些用户来说,他们只是不理解这个概念。如果用户理解这个概念,开发者可能不理解。也许这只是众多选择中的一个,而决定就是朝这个方向发展。在这种情况下,唯一能够在 "why" 部分给出答案的人就是开发者本人。

参考资料

如果您需要避免在 Python 中使用 ElementTree 重复 nsmap 参数,请考虑使用 XSLT 转换您的 XML 以删除命名空间和 return 本地元素名称。 Python 的 lxml 可以 运行 XSLT 1.0 脚本。

作为信息,XSLT 是一种特殊用途的声明性语言(与 XPath 同族,但与整个文档交互)专门用于转换 XML 源。事实上,XSLT 脚本是格式正确的 XML 文档!删除名称空间是满足最终用户需求的一项经常使用的任务。

考虑以下 XML 和 XSLT 作为字符串嵌入(但每个都可以从文件中解析)。转换后,您可以在转换后的新树对象上 运行 .findall()iter().xpath(),而无需定义名称空间前缀:

脚本

import lxml.etree as ET

# LOAD XML AND XSL STRINGS
xmlStr = '''
         <actors xmlns:fictional="http://characters.example.com"
                 xmlns="http://people.example.com">
             <actor>
                 <name>John Cleese</name>
                 <fictional:character>Lancelot</fictional:character>
                 <fictional:character>Archie Leach</fictional:character>
             </actor>
         </actors>
         '''
dom = ET.fromstring(xmlStr)

xslStr = '''
        <xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
        <xsl:output version="1.0" encoding="UTF-8" indent="yes" />
        <xsl:strip-space elements="*"/>

          <xsl:template match="@*|node()">
            <xsl:element name="{local-name()}">
              <xsl:apply-templates select="@*|node()"/>
            </xsl:element>
          </xsl:template>

          <xsl:template match="text()">
            <xsl:copy/>
          </xsl:template>

        </xsl:transform>
        '''
xslt = ET.fromstring(xslStr)

# TRANSFORM XML
transform = ET.XSLT(xslt)
newdom = transform(dom)

# OUTPUT AND PARSE
print(str(newdom))

for i in newdom.findall('//character'):
    print(i.text)

for i in newdom.iter('character'):
    print(i.text)

for i in newdom.xpath('//character'):
    print(i.text)

输出

<?xml version="1.0"?>
<actors>
  <actor>
    <name>John Cleese</name>
    <character>Lancelot</character>
    <character>Archie Leach</character>
  </actor>
</actors>

Lancelot
Archie Leach
Lancelot
Archie Leach
Lancelot
Archie Leach