【问题标题】:Can ElementTree be told to preserve the order of attributes?可以告诉 ElementTree 保留属性的顺序吗?
【发布时间】:2011-02-14 01:12:36
【问题描述】:

我使用 ElementTree 在 python 中编写了一个相当简单的过滤器来处理一些 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 3.4中的etree不保留属性?还是有一些设置?感谢您的帮助!
  • @Gabriel 看看接受的答案...
  • 我想到了一个非猴子补丁解决方案 =)?可悲的是,现在看起来没有比这更好的了……如果 XML 应该保持手动编辑并且便于阅读,这个问题尤其重要,我几乎认为我要使用正则表达式替换来修改 xml,很烂但是,然后保留布局(还有缩进和换行等格式)
  • 如果您的目标是合理的差异,请考虑将文件的规范副本保留为 c14n 格式。这样,您可以重新规范化任何修改后的版本并获得仅包含语义相关更改的差异。
  • 它没有在任何地方记录,但显然 python 3.8 解决了这个问题。

标签: python xml elementtree


【解决方案1】:

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

【讨论】:

  • 哇。自从我提出这个问题以来的几年里,违规工具已经重新构建,以允许持久的本地覆盖,这样我原来的需求就消失了,我已经转向不同的,如果不是更环保的牧场,甚至不使用固定的版本了。尽管如此,我确信有人仍然有这个需求。
  • @dmckee:你是完全正确的。 This question is still relevant 和补丁不可能是解决这个问题的正确方法。
  • python 3.4 现在有解决方案吗? etree 实现是否更改以允许这样做?
  • “另一个能更好地处理这种情况的模块”你有什么具体的想法吗?
  • 注意:如果您希望根节点属性也能保留顺序,则修补 ET._serialize_xml 是不够的!还将修补后的_serialize_xml 放入ET._serialize['xml'],瞧,你也明白了!! :]
【解决方案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

子类化或猴子补丁会很烦人,因为它就在一个大方法的中间。

除非你做了一些讨厌的事情,比如子类OrderedDict 并破解items 以返回一个特殊的list 子类,它会忽略对sort() 的调用。不,可能那更糟,我应该在想出比这更可怕的事情之前上床睡觉。

【讨论】:

  • 上面代码中的 OrderedXmlTreeBuilder 非常好!它可以与 ltree 一起使用,并且序列化也将得到修复。非常感谢您。
【解决方案3】:

最好的选择是使用 lxmlhttp://lxml.de/ 安装 lxml 并切换库对我来说很神奇。

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

【讨论】:

  • thdox 已经posted that suggestion
  • @dmckee:你是对的。我完全错过了那个答案。
  • 它也对我有用,非常感谢您的回答。
【解决方案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 兼容性

【讨论】:

  • 你确定lxml保留了属性顺序吗?文档似乎相反。
  • 从文档中,我简化了示例,并用我的 python 3.4 进行了尝试,这里提供的示例是从我的终端粘贴的。至少它对我有用。此外,文档,至少是我提供的 url,清楚地表明它保留了顺序,而不是词法顺序,而是这个 stackoverfow 问题中提出的顺序。
  • 没有冒犯,但问题是关于保留元素属性的顺序。 lxml 的文档(在您的链接上)说:“属性只是无序的名称-值对......”。我没有找到任何关于从 XML 源中保留元素属性顺序的信息。问题的棘手部分是作者的需求比XML格式保证的要求更严格——这可以理解,但lxml可能没有实现。
  • 是否为 lxml 记录了元素属性的顺序?我没有找到它,我不能依赖任何基于观察的猜测。
  • 根据我的经验,这似乎有效。我刚刚编写了一个脚本来更改 .apk 文件中的 AndroidManifest.xml 文件,而 lxml.etree 保留了属性顺序,而 xml.etree.ElementTree 则没有。作为一个额外的好处,它还保留了命名空间别名(xml.etree.ElementTree 无法做到)!从我这里获得最高分.....
【解决方案5】:

这已在 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" />'

【讨论】:

  • 这在What’s New In Python 3.8 中没有提到,但在tostring()tostringlist()dump() 函数和write() 方法的文档中提到。
  • documentationElementTree.write 方法声明:“3.8 版更改:write() 方法现在保留用户指定的属性顺序。”跨度>
【解决方案6】:

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

回答:Google 是您的朋友。搜索“xml diff”的第一个结果 => this。还有更多可能。

【讨论】:

  • 总是很高兴看到替代解决方案。谢谢。
  • 在一个完美的世界里,是的。但是,有时我们无法选择工具集的所有组件——例如,如果您的版本控制系统无法在语义上区分 XML 文件,并且您无法更改为不同的。跨度>
  • 如何将该工具与 Github、Stash 或任何其他 Web 界面集成到版本控制系统?
  • 在许多情况下,xml 文件只是 Git 存储库中晦涩的工件。然后,将默认差异最小化比要求整个工作组安装一个工具来处理濒临死亡的文件格式更明智。我在团队中的职责是不要弄乱所有其他成员的差异。这不是通过要求他们安装特殊工具来完成的。所以我不同意原始问题的有用性。
【解决方案7】:

来自the XML recommendation 的第 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 解析为元素树的问题在于,代码在内部创建了普通的 dicts,这些 dicts 被传递给 Element(),此时顺序丢失。没有等效的简单补丁。

【讨论】:

  • 它适合我。而且够简单!
【解决方案9】:

遇到了你的问题。首先寻找一些Python脚本来规范化,没有找到任何人。然后开始考虑做一个。终于xmllintsolved。

【讨论】:

  • 从那以后,我在 rdf(一个 xml 子集)方面遇到了类似的问题,我用内部视图解决了这个问题,并按字母顺序对视图进行排序。
【解决方案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) 将重新排列内存中的属性。它可能不会导致 alpha 排序的属性字典,但它也可能没有预期的顺序。

to_add = copy.deepcopy(contract)
to_add.attrib['symbol'] = add
to_add.attrib['uniqueId'] = add
contracts.insert(j + 1, to_add)

现在排序仍然存在。

【讨论】:

  • 重用节点?我无法发表评论,因此我将其添加为已接受答案的补充。提醒任何想要复制现有文件并将其插入并更改了一些值的人回到树中。如果有人想这样做,则接受的答案会在没有 deepcopy 的情况下失败。
【解决方案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文件中属性的顺序。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多