ElementTree可以保持属性的顺序吗?

33 投票
12 回答
35613 浏览
提问于 2025-04-15 22:11

我用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 个回答

13

最好的选择是使用 lxml 这个库,详细信息可以查看这里:http://lxml.de/。安装 lxml 库,然后只需切换到这个库,就能解决我的问题。

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

不行。ElementTree使用字典来存储属性值,所以它本身是没有顺序的。

就连DOM也不能保证属性的顺序,而且DOM提供的XML信息比ElementTree要详细得多。(有些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

想要对这个进行子类化或者猴子补丁会很麻烦,因为它就在一个大方法的中间。

除非你做一些奇怪的事情,比如子类化 OrderedDict,然后修改 items 让它返回一个特殊的 list 子类,这个子类会忽略对 sort() 的调用。算了,可能这样做更糟,我还是去睡觉吧,别再想出更可怕的主意了。

25

在@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())

撰写回答