是否可以告诉ElementTree保留属性的顺序?

时间:2010-04-29 23:48:51

标签: python xml elementtree

我在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)

合理还是愚蠢?


相关链接:

12 个答案:

答案 0 :(得分:21)

在@ bobince的答案和这两个答案的帮助下(setting attribute orderoverriding module methods

我设法让这只猴子修好了,我建议使用另一个更好地处理这种情况的模块但是当这不可能时:

# =======================================================================
# 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())

答案 1 :(得分:18)

不。 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" />'

Bah,序列化器以规范顺序输出它们。

ElementTree._write

中,这看起来像是责备
            items.sort() # lexical order

子类化或猴子修补会让人讨厌,因为它正处于一个大方法的中间。

除非你做了类似子类OrderedDict和hack items的讨厌,否则返回list的特殊子类,忽略对sort()的调用。不,可能情况更糟,在我提出比这更糟糕的事情之前我应该​​上床睡觉。

答案 2 :(得分:4)

错误的问题。应该是:“我在哪里可以找到合理使用XML文件的diff小工具?

答案:谷歌是你的朋友。搜索“xml diff”=&gt;的第一个结果this。还有一些可能性。

答案 3 :(得分: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"/>'

以下是文档的直接link,上面的示例稍作修改。

另请注意,lxml在设计上具有一些与标准xml.etree.ElementTree

的良好API兼容性

答案 4 :(得分:3)

来自the XML recommendation的第3.1节:

  

请注意,start-tag或empty-element标记中的属性规范顺序并不重要。

任何依赖于XML元素中属性顺序的系统都会破坏。

答案 5 :(得分:3)

最佳选择是使用 lxml http://lxml.de/ 安装lxml并只是切换库给我带来了魔力。

#import xml.etree.ElementTree as ET
from lxml import etree as ET

答案 6 :(得分:3)

此问题已在python 3.8中“修复”。我在任何地方都找不到关于它的任何注释,但现在可以使用。

D:\tmp\etree_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:\tmp\etree_order>C:\Python37-64\python.exe etree_order.py
b'<a aaa="1" bbb="2" ccc="3" />'

D:\tmp\etree_order>c:\Python38-64\python.exe etree_order.py
b'<a aaa="1" ccc="3" bbb="2" />'

答案 7 :(得分:2)

遇到了你的问题。首先寻找一些canonize的Python脚本,没有找到任何人。然后开始考虑制作一个。最后xmllint解决了。

答案 8 :(得分:2)

对于发出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解析为元素树的问题在于代码在内部创建了传递给Element()的普通dict,此时订单将丢失。没有相应的简单补丁。

答案 9 :(得分:0)

我在两个陈述中都使用了上面接受的答案:

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)

现在顺序仍然存在。

答案 10 :(得分:0)

我建议使用 LXML(其他人也有)。如果您需要保留属性的顺序以符合 c14n v1 或 v2 标准 (https://www.w3.org/TR/xml-c14n2/)(即增加字典顺序),lxml 通过传递一个输出方法很好地支持这一点(参见标题 C14N of {{3 }})

例如:

from lxml import etree as ET 
element = ET.Element('Test', B='beta', Z='omega', A='alpha') 
val = ET.tostring(element, method="c14n") 
print(val)

答案 11 :(得分:-2)

通过在python 3.8版本中运行python脚本,我们可以保留xml文件中属性的顺序。