Можно ли указать ElementTree сохранить порядок атрибутов?

Я написал довольно простой фильтр на питоне, используя 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)

Разумно или глупо?


Ссылки по теме:


person dmckee --- ex-moderator kitten    schedule 29.04.2010    source источник
comment
нет ли реального решения для этого? etree в python 3.4 не сохраняет атрибуты? или это с какими-то настройками?? Спасибо за помощь!   -  person Gabriel    schedule 08.09.2015
comment
@Gabriel Посмотрите на принятый ответ ...   -  person dmckee --- ex-moderator kitten    schedule 08.09.2015
comment
я думал о решении без патча для обезьян =)? к сожалению, на данный момент нет ничего лучше ... этот вопрос особенно актуален, если XML должен оставаться редактируемым вручную и удобным для чтения, я почти думаю, что собираюсь использовать замены регулярных выражений для изменения xml, отстой, но , макет сохраняется (а также форматирование, такое как отступы и разрывы строк)   -  person Gabriel    schedule 08.09.2015
comment
Если вашей целью является разумное изменение, подумайте о том, чтобы сохранить каноническую копию вашего файла в c14n. формат. Таким образом, вы можете повторно канонизировать любую измененную версию и получить разницу, которая включает только семантически релевантные изменения.   -  person Charles Duffy    schedule 03.08.2017
comment
Это нигде не задокументировано, но, по-видимому, Python 3.8 исправляет это.   -  person teeks99    schedule 11.02.2020


Ответы (12)


С помощью ответа @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())
person SnellyBigoda    schedule 17.06.2015
comment
Ух ты. За годы, прошедшие с тех пор, как я задал этот вопрос, оскорбительный инструмент был реструктурирован, чтобы разрешить постоянные локальные переопределения, так что моя первоначальная потребность исчезла, и я перешел на другие, если не более зеленые, пастбища и даже не использовать фиксированный версия больше. Тем не менее, я уверен, что у кого-то есть такая потребность. - person dmckee --- ex-moderator kitten; 18.06.2015
comment
@dmckee: ты совершенно прав. Этот вопрос все еще актуален и патч не может быть правильным способом решить эту проблему. - person dlewin; 23.07.2015
comment
есть ли решение для python 3.4? Изменилась ли реализация etree, чтобы разрешить это? - person Gabriel; 08.09.2015
comment
Другой модуль, который лучше справляется с этим сценарием. У вас есть на примете какие-то конкретные? - person Luke Taylor; 13.12.2015
comment
Примечание: исправления ET._serialize_xml НЕ достаточно, если вы хотите, чтобы атрибуты корневого узла также сохраняли порядок! Также поместите пропатченный _serialize_xml в ET._serialize['xml'] и вуаля, у вас тоже есть это!! :] - person ewerybody; 11.05.2017
comment
Ответ ниже stackoverflow.com/a/47422944/500902 представляет собой гораздо более простой обезьяний патч для сохранения порядка вывода. Я указываю, что это не устраняет проблемы с круговым обходом (анализ дерева элементов, а затем вывод), но я думаю, что и этот ответ не дает. - person Marvin; 22.11.2017

Неа. 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(). Нет, наверное, это еще хуже, и мне следует лечь спать, прежде чем я придумаю что-нибудь более ужасное, чем это.

person bobince    schedule 30.04.2010
comment
Очень хороший OrderedXmlTreeBuilder в коде выше! Его можно использовать с ltree, и сериализация тоже будет исправлена. Большое спасибо за это. - person Vladimir Kunschikov; 05.08.2016

Лучше всего использовать библиотеку lxml http://lxml.de/ Установка библиотеки lxml и просто переключение библиотеки сотворило со мной волшебство.

#import xml.etree.ElementTree as ET
from lxml import etree as ET
person Dinesh Jeyasankar    schedule 23.01.2018
comment
thdox уже опубликовал это предложение. - person dmckee --- ex-moderator kitten; 23.01.2018
comment
@dmckee: ты прав. Я полностью пропустил этот ответ. - person Dinesh Jeyasankar; 23.01.2018
comment
У меня тоже сработало, большое спасибо за ответ. - person akshaypmurgod; 27.09.2019

Да, с 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 имеет хорошую совместимость API со стандартным xml. etree.ElementTree

person thdox    schedule 01.01.2016
comment
Вы уверены, что lxml сохраняет порядок атрибутов? Документация, кажется, говорит об обратном. - person pepr; 02.01.2016
comment
Из документации я упростил пример и попробовал его с моим python 3.4, а приведенный здесь пример вставлен из моего терминала. По крайней мере, это сработало для меня. Кроме того, в документации, по крайней мере, в предоставленном мной URL-адресе, четко указано, что он сохраняет порядок, а не лексический порядок, а порядок, заданный в этом вопросе stackoverfow. - person thdox; 02.01.2016
comment
Без обид, но вопрос в сохранении порядка атрибутов элемента. В документации lxml (по вашей ссылке) говорится: Атрибуты - это просто неупорядоченные пары "имя-значение"... Я ничего не нашел о сохранении порядка атрибутов элементов из источника XML. Сложность вопроса в том, что у автора есть более строгие требования, чем те, которые гарантирует формат XML, что понятно, но, вероятно, не реализовано в lxml. - person pepr; 02.01.2016
comment
Мое понимание атрибутов - это просто неупорядоченные пары имя-значение... в отличие от xml.etree.ElementTree, который упорядочивает по лексическому порядку, lxml может сохранять нелексический порядок, что-то вроде порядка FIFO здесь. Когда вы говорите, что я ничего не нашел о сохранении порядка атрибутов элементов из источника XML, я бы прочитал файл xml с помощью lxml (обратите внимание на «l»), и при записи я бы явно выбрал нужный порядок, используя приведенный выше пример. - person thdox; 02.01.2016
comment
Задокументировано ли сохранение порядка атрибутов элементов для lxml? Я не нашел его, и я не могу полагаться на какое-либо предположение, основанное на каком-либо наблюдении. - person pepr; 02.01.2016
comment
Это, кажется, работает в моем опыте. Я только что написал сценарий для изменения файла AndroidManifest.xml в файлах .apk, и lxml.etree сохраняет порядок атрибутов, а xml.etree.ElementTree - нет. В качестве дополнительного бонуса он также сохраняет псевдонимы пространств имен (чего не может сделать xml.etree.ElementTree)! Получает от меня высшие оценки..... - person markshep; 24.06.2016

Неправильный вопрос. Должно быть: "Где мне найти гаджет diff, который нормально работает с файлами XML?

Ответ: Google — ваш друг. Первый результат поиска по "xml diff" => это. Есть еще несколько возможных.

person John Machin    schedule 30.04.2010
comment
Всегда рад видеть альтернативное решение. Спасибо. - person dmckee --- ex-moderator kitten; 30.04.2010
comment
В идеальном мире да. Однако иногда мы не можем выбрать все компоненты нашего набора инструментов — например, если вашу систему управления версиями нельзя научить семантически различать XML-файлы, и вы не можете перейти на другую. - person Tim Lesher; 20.12.2010
comment
Как интегрировать инструмент с Github, Stash или любым другим веб-интерфейсом в систему контроля версий? - person avakar; 25.06.2015
comment
Во многих случаях xml-файлы — это просто малоизвестные артефакты в репозитории Git. Тогда более разумно свести к минимуму разницу по умолчанию, чем требовать от всей рабочей группы установки инструмента для обработки умирающего формата файла. Моя ответственность в команде состоит в том, чтобы не испортить различия между всеми остальными участниками. Это не делается, требуя от них установки специального инструмента. Поэтому я не согласен с полезностью оригинальных вопросов. - person Håkon Seljåsen; 27.08.2020

Это было «исправлено» в 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" />'
person teeks99    schedule 11.02.2020
comment
Это не упоминается в Что нового в Python 3.8, но упоминается в документацию по функциям tostring(), tostringlist() и dump() и методу write(). - person mzjn; 12.02.2020
comment
документация для состояний метода ElementTree.write: Изменено в версии 3.8: метод write() теперь сохраняет порядок атрибутов, указанный пользователем. - person Jeyekomon; 19.07.2021

Из раздела 3.1 рекомендаций по XML:

Обратите внимание, что порядок спецификаций атрибутов в начальном теге или теге пустого элемента не имеет значения.

Любая система, основанная на порядке атрибутов в элементе XML, сломается.

person Robert Rossney    schedule 01.05.2010
comment
Речь идет не обязательно о правильности, а о поддержании минимального diff. - person avakar; 25.06.2015

Это частичное решение для случая, когда выдается 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, которые передаются в Element(), после чего порядок теряется. Никакой эквивалентный простой патч невозможен.

person Marvin    schedule 21.11.2017
comment
это работает для меня. и достаточно просто! - person Lei Yang; 06.11.2020

Была ваша проблема. Сначала искал какой-нибудь Python-скрипт для канонизации, но никого не нашел. Потом начал думать о том, чтобы сделать один. Наконец-то проблема xmllint решена.

person 1737973    schedule 18.06.2013
comment
В те дни с тех пор у меня была похожая проблема с rdf (подмножество xml), которую я решаю с помощью внутренних представлений и сортировки этих представлений по алфавиту. - person 1737973; 25.06.2013

Я использовал принятый ответ выше с обоими утверждениями:

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)

Теперь порядок сохраняется.

person TinCupChalice    schedule 30.07.2018
comment
повторное использование узла? Я не смог прокомментировать, поэтому добавил его в качестве дополнения к принятому ответу. Это должно предостеречь любого, кто также хочет скопировать существующий и вставить его с некоторыми измененными значениями обратно в дерево. Если кто-то хочет это сделать, принятый ответ не работает без глубокой копии. - person TinCupChalice; 30.07.2018

Я бы рекомендовал использовать LXML (как и другие). Если вам необходимо сохранить порядок атрибутов для соответствия стандартам c14n v1 или v2 (https://www.w3.org/TR/xml-c14n2/) (т.е. увеличение лексикографического порядка), lxml очень хорошо поддерживает это, передавая метод вывода (см. заголовок C14N в https://lxml.de/api.html)

Например:

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

Запустив скрипт Python в версии Python 3.8, мы можем сохранить порядок атрибутов в XML-файлах.

person Hrithik Diwakar    schedule 18.06.2020