在 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}actor
,fictional: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
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}actor
,fictional: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