¿Se puede decir a ElementTree que mantenga el orden de los atributos?

He escrito un filtro bastante simple en python utilizando ElementTree para mezclar los contextos de algunos archivos xml. Y funciona, más o menos.

Pero reordena los atributos de varias tags, y me gustaría que no lo hiciera.

¿Alguien sabe un interruptor que puedo lanzar para que se mantenga en el orden especificado?

Contexto para esto

Estoy trabajando con y en una herramienta de física de partículas que tiene un sistema de configuración complejo, pero curiosamente limitado, basado en archivos xml. Entre las muchas cosas que se configuran de esa manera están las rutas a varios archivos de datos estáticos. Estas rutas están codificadas en el xml existente y no hay instalaciones para configurarlas o variarlas en función de las variables de entorno, y en nuestra instalación local están necesariamente en un lugar diferente.

Esto no es un desastre porque la herramienta combinada de control de comstackción y fuente que estamos usando nos permite ocultar ciertos archivos con copias locales. Pero incluso aunque los campos de datos son estáticos, el xml no lo es, por lo que he escrito un script para corregir las rutas, pero con la diferencia de cambio de atributo entre las versiones local y maestra es más difícil de leer que lo necesario.


Esta es la primera vez que doy un giro a ElementTree (y solo a mi quinto o sexto proyecto de python), así que quizás lo estoy haciendo mal.

Abstraído por simplicidad, el código se ve así:

tree = elementtree.ElementTree.parse(inputfile) i = tree.getiterator() for e in i: e.text = filter(e.text) tree.write(outputfile) 

¿Razonable o tonto?


Enlaces relacionados:

  • ¿Cómo puedo obtener el orden de una lista de atributos de elementos utilizando Python xml.sax?
  • Mantener el orden de los atributos al modificar con minidom

Con la ayuda de la respuesta de @ bobince y estas dos ( establecer el orden de los atributos , anular los métodos del módulo )

Me las arreglé para conseguir que este mono parcheado estuviera sucio y sugeriría usar otro módulo que maneje mejor este escenario, pero cuando no es posible:

 # ======================================================================= # 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("" % ET._encode(text, encoding)) elif tag is ET.ProcessingInstruction: write("" % 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("") 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) # ======================================================================= 

Luego en tu código:

 tree = ET.parse(pathToFile, OrderedXMLTreeBuilder()) 

No ElementTree usa un diccionario para almacenar valores de atributos, por lo que es inherentemente desordenado.

Incluso DOM no le garantiza el orden de los atributos, y DOM expone muchos más detalles del conjunto de información XML que ElementTree. (Hay algunos DOM que lo ofrecen como una característica, pero no es estándar).

¿Se puede arreglar? Tal vez. Aquí hay una puñalada que reemplaza el diccionario cuando se analiza con uno ordenado ( 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('') >>> tree = ElementTree.ElementTree() >>> root = tree.parse(xmlf, OrderedXMLTreeBuilder()) >>> root.attrib OrderedDict([('b', 'c'), ('d', 'e'), ('f', 'g'), ('j', 'k'), ('h', 'i')]) 

Parece potencialmente prometedor.

 >>> s = StringIO.StringIO() >>> tree.write(s) >>> s.getvalue() '' 

Bah, el serializador los emite en orden canónico.

Esto parece la línea a la que hay que culpar, en ElementTree._write :

  items.sort() # lexical order 

Subclasificar o parchar a los monos será molesto ya que está en medio de un gran método.

A menos que hiciera algo desagradable como la subclase OrderedDict y OrderedDict items para devolver una subclase especial de list que ignora las llamadas a sort() . No, probablemente eso sea aún peor y debería irme a la cama antes de que se me ocurra algo más horrible que eso.

Pregunta equivocada. Debería estar: “¿Dónde encuentro un gadget diff que funcione con sensatez con los archivos XML?

Respuesta: Google es tu amigo. Primer resultado de búsqueda en “xml diff” => this . Hay algunos más posibles.

Si con lxml

 >>> from lxml import etree >>> root = etree.Element("root", interesting="totally") >>> etree.tostring(root) b'' >>> print(root.get("hello")) None >>> root.set("hello", "Huhu") >>> print(root.get("hello")) Huhu >>> etree.tostring(root) b'' 

Aquí hay un enlace directo a la documentación, desde el cual el ejemplo anterior está ligeramente adaptado.

También tenga en cuenta que lxml tiene, por diseño, una buena compatibilidad API con el estándar xml.etree.ElementTree

De la sección 3.1 de la recomendación XML :

Tenga en cuenta que el orden de las especificaciones de atributos en una etiqueta de inicio o etiqueta de elemento vacío no es significativo.

Cualquier sistema que se base en el orden de los atributos en un elemento XML se romperá.

He tenido tu problema. Primero busqué un script en Python para canonizar, no encontré a nadie. Entonces empecé a pensar en hacer uno. Finalmente xmllint resuelto.

Esta es una solución parcial, en el caso de que se emita xml y se desee un orden predecible. No resuelve ida y vuelta analizando y escribiendo. Tanto 2.7 como 3.x usan sorted() para forzar un orden de atributo. Por lo tanto, este código, junto con el uso de un OrderedDictionary para mantener los atributos, conservará el orden para que la salida xml coincida con el orden utilizado para crear los Elementos.

 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... #  # Parse again, won't be ordered because Elements are created # without ordered dict print ET.tostring(ET.fromstring(x)) #  

El problema con el análisis de XML en un árbol de elementos es que el código crea internamente dict simples que se pasan a Element (), en cuyo punto se pierde la orden. No es posible un parche simple equivalente.

La mejor opción es usar la biblioteca lxml http://lxml.de/. Instalar el lxml y simplemente cambiar la biblioteca me sirvió de magia.

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

Usé la respuesta aceptada arriba, con ambas afirmaciones:

 ET._serialize_xml = _serialize_xml ET._serialize['xml'] = _serialize_xml 

Si bien esto solucionó el orden en cada nodo, el orden de los atributos en los nuevos nodos insertados desde copias de los nodos existentes no pudo conservarse sin una copia en profundidad. Cuidado con la reutilización de nodos para crear otros … En mi caso, tenía un elemento con varios atributos, así que quería reutilizarlos:

 to_add = ET.fromstring(ET.tostring(contract)) to_add.attrib['symbol'] = add to_add.attrib['uniqueId'] = add contracts.insert(j + 1, to_add) 

El fromstring(tostring) reordena los atributos en la memoria. Puede que no resulte en el dictado de atributos ordenados por alfa, pero también puede que no tenga el orden esperado.

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

Ahora el ordenamiento persiste.