是否可以指示 ElementTree 保留属性的顺序?
- 2025-03-21 09:06:00
- admin 原创
- 39
问题描述:
我用 Python 编写了一个相当简单的过滤器,使用 ElementTree 来整理一些 xml 文件的上下文。它或多或少能起作用。
但是它会重新排序各种标签的属性,而我希望它不要这样做。
有人知道我可以按下一个开关让它们保持指定的顺序吗?
背景
我正在使用并开发一个粒子物理工具,它有一个基于 xml 文件的复杂但功能有限的配置系统。通过这种方式设置的众多内容包括各种静态数据文件的路径。这些路径被硬编码到现有的 xml 中,没有基于环境变量设置或更改它们的工具,并且在我们的本地安装中,它们必然位于不同的地方。
这不是什么灾难,因为我们使用的源代码和构建控制工具组合允许我们使用本地副本来隐藏某些文件。但即使数据字段是静态的,xml 也不是,所以我编写了一个脚本来修复路径,但由于属性重新排列,本地版本和主版本之间的差异比必要的更难读取。
这是我第一次使用 ElementTree(也是我的第五或第六个 Python 项目)所以也许我做错了。
为了简单起见,代码抽象如下:
tree = elementtree.ElementTree.parse(inputfile)
i = tree.getiterator()
for e in i:
e.text = filter(e.text)
tree.write(outputfile)
合理还是愚蠢?
相关链接:
如何使用 Python xml.sax 获取元素属性列表的顺序?
使用 minidom 修改时保留属性的顺序
解决方案 1:
在@bobince 的答案和这两个(设置属性顺序、覆盖模块方法)的帮助下
我设法修补了这只猴子,但它很脏,我建议使用另一个模块来更好地处理这种情况,但是当这是不可能的时:
# =======================================================================
# Monkey patch ElementTree
import xml.etree.ElementTree as ET
def _serialize_xml(write, elem, encoding, qnames, namespaces):
tag = elem.tag
text = elem.text
if tag is ET.Comment:
write("<!--%s-->" % ET._encode(text, encoding))
elif tag is ET.ProcessingInstruction:
write("<?%s?>" % ET._encode(text, encoding))
else:
tag = qnames[tag]
if tag is None:
if text:
write(ET._escape_cdata(text, encoding))
for e in elem:
_serialize_xml(write, e, encoding, qnames, None)
else:
write("<" + tag)
items = elem.items()
if items or namespaces:
if namespaces:
for v, k in sorted(namespaces.items(),
key=lambda x: x[1]): # sort on prefix
if k:
k = ":" + k
write(" xmlns%s=\"%s\"" % (
k.encode(encoding),
ET._escape_attrib(v, encoding)
))
#for k, v in sorted(items): # lexical order
for k, v in items: # Monkey patch
if isinstance(k, ET.QName):
k = k.text
if isinstance(v, ET.QName):
v = qnames[v.text]
else:
v = ET._escape_attrib(v, encoding)
write(" %s=\"%s\"" % (qnames[k], v))
if text or len(elem):
write(">")
if text:
write(ET._escape_cdata(text, encoding))
for e in elem:
_serialize_xml(write, e, encoding, qnames, None)
write("</" + tag + ">")
else:
write(" />")
if elem.tail:
write(ET._escape_cdata(elem.tail, encoding))
ET._serialize_xml = _serialize_xml
from collections import OrderedDict
class OrderedXMLTreeBuilder(ET.XMLTreeBuilder):
def _start_list(self, tag, attrib_in):
fixname = self._fixname
tag = fixname(tag)
attrib = OrderedDict()
if attrib_in:
for i in range(0, len(attrib_in), 2):
attrib[fixname(attrib_in[i])] = self._fixtext(attrib_in[i+1])
return self._target.start(tag, attrib)
# =======================================================================
然后在你的代码中:
tree = ET.parse(pathToFile, OrderedXMLTreeBuilder())
解决方案 2:
不是。ElementTree 使用字典来存储属性值,因此它本质上是无序的。
即使 DOM 也不能保证属性排序,而且 DOM 比 ElementTree 公开了更多 XML 信息集的细节。(有些 DOM 确实提供了此功能,但这不是标准。)
可以修复吗?也许可以。下面是尝试在解析时用有序字典替换字典的方法(collections.OrderedDict()
)。
from xml.etree import ElementTree
from collections import OrderedDict
import StringIO
class OrderedXMLTreeBuilder(ElementTree.XMLTreeBuilder):
def _start_list(self, tag, attrib_in):
fixname = self._fixname
tag = fixname(tag)
attrib = OrderedDict()
if attrib_in:
for i in range(0, len(attrib_in), 2):
attrib[fixname(attrib_in[i])] = self._fixtext(attrib_in[i+1])
return self._target.start(tag, attrib)
>>> xmlf = StringIO.StringIO('<a b="c" d="e" f="g" j="k" h="i"/>')
>>> tree = ElementTree.ElementTree()
>>> root = tree.parse(xmlf, OrderedXMLTreeBuilder())
>>> root.attrib
OrderedDict([('b', 'c'), ('d', 'e'), ('f', 'g'), ('j', 'k'), ('h', 'i')])
看上去很有前景。
>>> s = StringIO.StringIO()
>>> tree.write(s)
>>> s.getvalue()
'<a b="c" d="e" f="g" h="i" j="k" />'
嗯,序列化器按照规范顺序输出它们。
这看起来像是应该受到指责的行ElementTree._write
:
items.sort() # lexical order
子类化或 monkey-patching 将会很烦人,因为它就在一个大方法的中间。
除非你做了一些肮脏的事情,比如子类化OrderedDict
和 hack items
,返回一个list
忽略对的特殊子类sort()
。不,那可能更糟糕,我应该睡觉,以免想出比这更可怕的事情。
解决方案 3:
最好的选择是使用lxml库http://lxml.de/
安装 lxml 并切换库对我来说就产生了神奇的效果。
#import xml.etree.ElementTree as ET
from lxml import etree as ET
解决方案 4:
是的,使用lxml
>>> from lxml import etree
>>> root = etree.Element("root", interesting="totally")
>>> etree.tostring(root)
b'<root interesting="totally"/>'
>>> print(root.get("hello"))
None
>>> root.set("hello", "Huhu")
>>> print(root.get("hello"))
Huhu
>>> etree.tostring(root)
b'<root interesting="totally" hello="Huhu"/>'
这是文档的直接链接,上面的示例稍作了改编。
还要注意的是,lxml 在设计上与标准xml.etree.ElementTree具有良好的 API 兼容性
解决方案 5:
此问题已在 Python 3.8 中“修复”。我找不到任何关于此问题的说明,但现在可以正常工作了。
D: mpetree_order>type etree_order.py
import xml.etree.ElementTree as ET
a = ET.Element('a', {"aaa": "1", "ccc": "3", "bbb": "2"})
print(ET.tostring(a))
D: mpetree_order>C:Python37-64python.exe etree_order.py
b'<a aaa="1" bbb="2" ccc="3" />'
D: mpetree_order>c:Python38-64python.exe etree_order.py
b'<a aaa="1" ccc="3" bbb="2" />'
解决方案 6:
问题错误。应该是:“我在哪里可以找到一个diff
可以合理地处理 XML 文件的小工具?”
答案:Google 是你的朋友。搜索“xml diff”=> this 的第一个结果。还有其他一些可能。
解决方案 7:
来自XML 建议书第 3.1 节:
请注意,开始标记或空元素标记中的属性规范的顺序并不重要。
任何依赖 XML 元素中属性顺序的系统都会崩溃。
解决方案 8:
对于发出 xml 并需要可预测顺序的情况,这是一个部分解决方案。它不能解决往返解析和写入问题。2.7 和 3.x 都用于sorted()
强制属性排序。因此,此代码与使用 OrderedDictionary 保存属性相结合,将保留 xml 输出的顺序,以匹配用于创建元素的顺序。
from collections import OrderedDict
from xml.etree import ElementTree as ET
# Make sorted() a no-op for the ElementTree module
ET.sorted = lambda x: x
try:
# python3 use a cPython implementation by default, prevent that
ET.Element = ET._Element_Py
# similarly, override SubElement method if desired
def SubElement(parent, tag, attrib=OrderedDict(), **extra):
attrib = attrib.copy()
attrib.update(extra)
element = parent.makeelement(tag, attrib)
parent.append(element)
return element
ET.SubElement = SubElement
except AttributeError:
pass # nothing else for python2, ElementTree is pure python
# Make an element with a particular "meaningful" ordering
t = ET.ElementTree(ET.Element('component',
OrderedDict([('grp','foo'),('name','bar'),
('class','exec'),('arch','x86')])))
# Add a child element
ET.SubElement(t.getroot(),'depend',
OrderedDict([('grp','foo'),('name','util1'),('class','lib')]))
x = ET.tostring(n)
print (x)
# Order maintained...
# <component grp="foo" name="bar" class="exec" arch="x86"><depend grp="foo" name="util1" class="lib" /></component>
# Parse again, won't be ordered because Elements are created
# without ordered dict
print ET.tostring(ET.fromstring(x))
# <component arch="x86" name="bar" grp="foo" class="exec"><depend name="util1" grp="foo" class="lib" /></component>
将 XML 解析为元素树的问题在于,代码会在内部创建普通的dict
s 并将其传递给 Element(),此时顺序就会丢失。没有等效的简单补丁。
解决方案 9:
遇到过你的问题。首先寻找一些 Python 脚本来规范化,没有找到。然后开始考虑编写一个。终于xmllint
解决了。
解决方案 10:
我使用了上面接受的答案,其中包含两个陈述:
ET._serialize_xml = _serialize_xml
ET._serialize['xml'] = _serialize_xml
虽然这修复了每个节点的顺序,但如果没有深度复制,从现有节点的副本插入的新节点上的属性顺序将无法保留。注意重复使用节点来创建其他节点... 在我的例子中,我有一个具有多个属性的元素,所以我想重复使用它们:
to_add = ET.fromstring(ET.tostring(contract))
to_add.attrib['symbol'] = add
to_add.attrib['uniqueId'] = add
contracts.insert(j + 1, to_add)
将fromstring(tostring)
在内存中对属性进行重新排序。这可能不会产生按字母顺序排序的属性字典,但也可能不具有预期的顺序。
to_add = copy.deepcopy(contract)
to_add.attrib['symbol'] = add
to_add.attrib['uniqueId'] = add
contracts.insert(j + 1, to_add)
现在,该顺序仍然存在。
解决方案 11:
我建议使用 LXML(其他人也建议使用)。如果您需要保留属性的顺序以遵守 c14n v1 或 v2 标准(https://www.w3.org/TR/xml-c14n2/ )(即按字典顺序递增),lxml 通过传递输出方法很好地支持了这一点(请参阅https://lxml.de/api.html的标题 C14N )
例如:
from lxml import etree as ET
element = ET.Element('Test', B='beta', Z='omega', A='alpha')
val = ET.tostring(element, method="c14n")
print(val)
解决方案 12:
通过运行python 3.8版本的python脚本,我们可以保留xml文件中属性的顺序。
扫码咨询,免费领取项目管理大礼包!