可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I've written a fairly simple filter in python using ElementTree to munge the contexts of some xml files. And it works, more or less.
But it reorders the attributes of various tags, and I'd like it to not do that.
Does anyone know a switch I can throw to make it keep them in specified order?
Context for this
I'm working with and on a particle physics tool that has a complex, but oddly limited configuration system based on xml files. Among the many things setup that way are the paths to various static data files. These paths are hardcoded into the existing xml and there are no facilities for setting or varying them based on environment variables, and in our local installation they are necessarily in a different place.
This isn't a disaster because the combined source- and build-control tool we're using allows us to shadow certain files with local copies. But even thought the data fields are static the xml isn't, so I've written a script for fixing the paths, but with the attribute rearrangement diffs between the local and master versions are harder to read than necessary.
This is my first time taking ElementTree for a spin (and only my fifth or sixth python project) so maybe I'm just doing it wrong.
Abstracted for simplicity the code looks like this:
tree = elementtree.ElementTree.parse(inputfile)
i = tree.getiterator()
for e in i:
e.text = filter(e.text)
tree.write(outputfile)
Reasonable or dumb?
Related links:
- How can I get the order of an element attribute list using Python xml.sax?
- Preserve order of attributes when modifying with minidom
回答1:
With help from @bobince's answer and these two (setting attribute order, overriding module methods)
I managed to get this monkey patched it's dirty and I'd suggest using another module that better handles this scenario but when that isn't a possibility:
# =======================================================================
# 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)
# =======================================================================
Then in your code:
tree = ET.parse(pathToFile, OrderedXMLTreeBuilder())
回答2:
Nope. ElementTree uses a dictionary to store attribute values, so it's inherently unordered.
Even DOM doesn't guarantee you attribute ordering, and DOM exposes a lot more detail of the XML infoset than ElementTree does. (There are some DOMs that do offer it as a feature, but it's not standard.)
Can it be fixed? Maybe. Here's a stab at it that replaces the dictionary when parsing with an ordered one (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')])
Looks potentially promising.
>>> s = StringIO.StringIO()
>>> tree.write(s)
>>> s.getvalue()
'<a b="c" d="e" f="g" h="i" j="k" />'
Bah, the serialiser outputs them in canonical order.
This looks like the line to blame, in ElementTree._write
:
items.sort() # lexical order
Subclassing or monkey-patching that is going to be annoying as it's right in the middle of a big method.
Unless you did something nasty like subclass OrderedDict
and hack items
to return a special subclass of list
that ignores calls to sort()
. Nah, probably that's even worse and I should go to bed before I come up with anything more horrible than that.
回答3:
Best Option is to use the lxml library http://lxml.de/
Installing the lxml and just switching the library did the magic to me.
#import xml.etree.ElementTree as ET
from lxml import etree as ET
回答4:
Wrong question. Should be: "Where do I find a diff
gadget that works sensibly with XML files?
Answer: Google is your friend. First result for search on "xml diff" => this. There are a few more possibles.
回答5:
Yes, with 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"/>'
Here is direct link to documentation, from which the above example is slightly adapted.
Also note that lxml has, by design, some good API compatiblity with standard xml.etree.ElementTree
回答6:
From section 3.1 of the XML recommendation:
Note that the order of attribute specifications in a start-tag or empty-element tag is not significant.
Any system that relies on the order of attributes in an XML element is going to break.
回答7:
Have had your problem. Firstly looked for some Python script to canonize, didnt found anyone. Then started thinking about making one. Finally xmllint
solved.
回答8:
This is a partial solution, for the case where xml is being emitted and a predictable order is desired. It does not solve round trip parsing and writing. Both 2.7 and 3.x use sorted()
to force an attribute ordering. So, this code, in conjunction with use of an OrderedDictionary to hold the attributes will preserve the order for xml output to match the order used to create the Elements.
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>
The problem with parsing XML into an element tree is that the code internally creates plain dict
s which are passed in to Element(), at which point the order is lost. No equivalent simple patch is possible.
回答9:
I used the accepted answer above, with both statements:
ET._serialize_xml = _serialize_xml
ET._serialize['xml'] = _serialize_xml
While this fixed the ordering in every node, attribute ordering on new nodes inserted from copies of existing nodes failed to preserve without a deepcopy. Watch out for reusing nodes to create others...
In my case I had an element with several attributes, so I wanted to reuse them:
to_add = ET.fromstring(ET.tostring(contract))
to_add.attrib['symbol'] = add
to_add.attrib['uniqueId'] = add
contracts.insert(j + 1, to_add)
The fromstring(tostring)
will reorder the attributes in memory. It may not result in the alpha sorted dict of attributes, but it also may not have the expected ordering.
to_add = copy.deepcopy(contract)
to_add.attrib['symbol'] = add
to_add.attrib['uniqueId'] = add
contracts.insert(j + 1, to_add)
Now the ordering persists.