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

Jar*_*edL 9 python xml lxml elementtree xml-namespaces

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

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它们从文档继承的元素的属性.然而,没有一种方法似乎实际上使用它,所以我仍然必须做element.find('fictional:character', element.nsmap),这只是不必要的重复输入每次.它仍然不适用于属性.

幸运的是lxml支持子类化BaseElement,所以我只使用p(for prefix)属性创建一个具有相同API的属性,但使用元素自动使用名称空间前缀nsmap(编辑:可能最好nsmap在代码中分配自定义).所以我只是这样做,element.p.find('fictional:character')或者element.p.attrib['prefix:attrname']更少重复,我认为更具可读性.

我只是觉得我真的错过了一些东西,它真的感觉这应该真的已经是一个功能,lxml如果不是内置etree包.我在某种程度上做错了吗?

Tho*_*ler 7

是否有可能摆脱命名空间映射?

你需要将它作为参数传递给每个函数调用吗?一个选项是设置要在属性中的XML文档中使用的前缀.

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

一旦您获得XML文档,它就被修改,因此您的前缀不再起作用.

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

这种设计不仅存在于Python中,它还存在于.NET中.在SelectNodes()[MSDN]可以,如果你不需要前缀使用.但只要有前缀,它就会抛出异常.因此,您必须使用使用XmlNamespaceManager作为参数的重载SelectNodes()[MSDN].

XPath作为解决方案

我建议学习XPath(lxml特定链接),你可以使用前缀.由于这可能是版本特定的,让我说我使用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)
Run Code Online (Sandbox Code Playgroud)

输出是

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

Process finished with exit code 0
Run Code Online (Sandbox Code Playgroud)

注意XPath如何解决从x命名空间表中的d前缀到XML文档中的前缀的映射.

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

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

要选择元素,请/element选择使用前缀.

xp=root.xpath("/x:data", namespaces=my_namespaces)
Run Code Online (Sandbox Code Playgroud)

要选择属性,请编写/@attribute,可选择使用前缀.

#See example above
Run Code Online (Sandbox Code Playgroud)

要向下导航,请连接几个元素.//如果您不知道介于两者之间的项目,请使用.要向上移动,请使用/...如果不遵循,属性必须是最后的/...

xp=root.xpath("/x:data/country/neighbor/@x:name/..", namespaces=my_namespaces)
Run Code Online (Sandbox Code Playgroud)

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

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

还有更多要发现的东西,比如text()兄弟选择的各种方式甚至功能.

关于'为什么'

原来的问题标题是

为什么使用XML命名空间在Python中看起来如此困难?

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

参考