En se basant sur une autre SO question , comment vérifier si deux extraits de code XML bien formés sont sémantiquement égaux. Tout ce dont j'ai besoin est "égal" ou non, car je l'utilise pour les tests unitaires.
Dans le système que je veux, ils seraient égaux (notez l'ordre de 'début' et 'fin'):
<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats start="1275955200" end="1276041599">
</Stats>
# Reordered start and end
<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats end="1276041599" start="1275955200" >
</Stats>
J'ai lmxl et d'autres outils à ma disposition, et une fonction simple qui permet uniquement de réorganiser les attributs fonctionnerait également!
Extrait de travail basé sur la réponse de IanB:
from formencode.doctest_xml_compare import xml_compare
# have to strip these or fromstring carps
xml1 = """ <?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats start="1275955200" end="1276041599"></Stats>"""
xml2 = """ <?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats end="1276041599" start="1275955200"></Stats>"""
xml3 = """ <?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats start="1275955200"></Stats>"""
from lxml import etree
tree1 = etree.fromstring(xml1.strip())
tree2 = etree.fromstring(xml2.strip())
tree3 = etree.fromstring(xml3.strip())
import sys
reporter = lambda x: sys.stdout.write(x + "\n")
assert xml_compare(tree1,tree2,reporter)
assert xml_compare(tree1,tree3,reporter) is False
Vous pouvez utiliser formencode.doctest_xml_compare - la fonction xml_compare compare deux arbres ElementTree ou lxml.
L’ordre des éléments peut être significatif en XML. C’est peut-être pour cette raison que la plupart des autres méthodes suggérées comparent de manière inégale si l’ordre est différent ... même si les éléments ont les mêmes attributs et le même contenu textuel.
Mais je voulais aussi une comparaison insensible à l'ordre, alors je suis venu avec ceci:
from lxml import etree
import xmltodict # pip install xmltodict
def normalise_dict(d):
"""
Recursively convert dict-like object (eg OrderedDict) into plain dict.
Sorts list values.
"""
out = {}
for k, v in dict(d).iteritems():
if hasattr(v, 'iteritems'):
out[k] = normalise_dict(v)
Elif isinstance(v, list):
out[k] = []
for item in sorted(v):
if hasattr(item, 'iteritems'):
out[k].append(normalise_dict(item))
else:
out[k].append(item)
else:
out[k] = v
return out
def xml_compare(a, b):
"""
Compares two XML documents (as string or etree)
Does not care about element order
"""
if not isinstance(a, basestring):
a = etree.tostring(a)
if not isinstance(b, basestring):
b = etree.tostring(b)
a = normalise_dict(xmltodict.parse(a))
b = normalise_dict(xmltodict.parse(b))
return a == b
J'ai eu le même problème: je voulais comparer deux documents qui avaient les mêmes attributs mais dans des ordres différents.
Il semble que la canonisation XML (C14N) en lxml fonctionne bien pour cela, mais je ne suis certainement pas un expert en XML. Je suis curieux de savoir si quelqu'un d'autre peut signaler les inconvénients de cette approche.
parser = etree.XMLParser(remove_blank_text=True)
xml1 = etree.fromstring(xml_string1, parser)
xml2 = etree.fromstring(xml_string2, parser)
print "xml1 == xml2: " + str(xml1 == xml2)
ppxml1 = etree.tostring(xml1, pretty_print=True)
ppxml2 = etree.tostring(xml2, pretty_print=True)
print "pretty(xml1) == pretty(xml2): " + str(ppxml1 == ppxml2)
xml_string_io1 = StringIO()
xml1.getroottree().write_c14n(xml_string_io1)
cxml1 = xml_string_io1.getvalue()
xml_string_io2 = StringIO()
xml2.getroottree().write_c14n(xml_string_io2)
cxml2 = xml_string_io2.getvalue()
print "canonicalize(xml1) == canonicalize(xml2): " + str(cxml1 == cxml2)
Courir cela me donne:
$ python test.py
xml1 == xml2: false
pretty(xml1) == pretty(xml2): false
canonicalize(xml1) == canonicalize(xml2): true
Voici une solution simple: convertissez XML en dictionnaires (avec xmltodict ) et comparez les dictionnaires
import json
import xmltodict
class XmlDiff(object):
def __init__(self, xml1, xml2):
self.dict1 = json.loads(json.dumps((xmltodict.parse(xml1))))
self.dict2 = json.loads(json.dumps((xmltodict.parse(xml2))))
def equal(self):
return self.dict1 == self.dict2
test de l'unité
import unittest
class XMLDiffTestCase(unittest.TestCase):
def test_xml_equal(self):
xml1 = """<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats start="1275955200" end="1276041599">
</Stats>"""
xml2 = """<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats end="1276041599" start="1275955200" >
</Stats>"""
self.assertTrue(XmlDiff(xml1, xml2).equal())
def test_xml_not_equal(self):
xml1 = """<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats start="1275955200">
</Stats>"""
xml2 = """<?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats end="1276041599" start="1275955200" >
</Stats>"""
self.assertFalse(XmlDiff(xml1, xml2).equal())
ou en méthode python simple:
import json
import xmltodict
def xml_equal(a, b):
"""
Compares two XML documents (as string or etree)
Does not care about element order
"""
return json.loads(json.dumps((xmltodict.parse(a)))) == json.loads(json.dumps((xmltodict.parse(b))))
En réfléchissant à ce problème, j'ai proposé la solution suivante qui rend les éléments XML comparables et triables:
import xml.etree.ElementTree as ET
def cmpElement(x, y):
# compare type
r = cmp(type(x), type(y))
if r: return r
# compare tag
r = cmp(x.tag, y.tag)
if r: return r
# compare tag attributes
r = cmp(x.attrib, y.attrib)
if r: return r
# compare stripped text content
xtext = (x.text and x.text.strip()) or None
ytext = (y.text and y.text.strip()) or None
r = cmp(xtext, ytext)
if r: return r
# compare sorted children
if len(x) or len(y):
return cmp(sorted(x.getchildren()), sorted(y.getchildren()))
return 0
ET._ElementInterface.__lt__ = lambda self, other: cmpElement(self, other) == -1
ET._ElementInterface.__gt__ = lambda self, other: cmpElement(self, other) == 1
ET._ElementInterface.__le__ = lambda self, other: cmpElement(self, other) <= 0
ET._ElementInterface.__ge__ = lambda self, other: cmpElement(self, other) >= 0
ET._ElementInterface.__eq__ = lambda self, other: cmpElement(self, other) == 0
ET._ElementInterface.__ne__ = lambda self, other: cmpElement(self, other) != 0
Si vous utilisez une approche DOM, vous pouvez parcourir les deux arbres simultanément tout en comparant les nœuds (type de nœud, texte, attributs) au fur et à mesure.
Une solution récursive sera la plus élégante - il suffit de court-circuiter la comparaison supplémentaire une fois qu'une paire de nœuds ne sont pas "égaux" ou une fois que vous avez détecté une feuille dans un arbre lorsqu'il s'agit d'une branche dans un autre, etc.
Adaptation Excellente réponse d'Anentropic à Python 3 (en gros, changez iteritems()
en items()
et basestring
en string
):
from lxml import etree
import xmltodict # pip install xmltodict
def normalise_dict(d):
"""
Recursively convert dict-like object (eg OrderedDict) into plain dict.
Sorts list values.
"""
out = {}
for k, v in dict(d).items():
if hasattr(v, 'iteritems'):
out[k] = normalise_dict(v)
Elif isinstance(v, list):
out[k] = []
for item in sorted(v):
if hasattr(item, 'iteritems'):
out[k].append(normalise_dict(item))
else:
out[k].append(item)
else:
out[k] = v
return out
def xml_compare(a, b):
"""
Compares two XML documents (as string or etree)
Does not care about element order
"""
if not isinstance(a, str):
a = etree.tostring(a)
if not isinstance(b, str):
b = etree.tostring(b)
a = normalise_dict(xmltodict.parse(a))
b = normalise_dict(xmltodict.parse(b))
return a == b
Qu'en est-il de l'extrait de code suivant? Peut être facilement amélioré pour inclure également les attributs:
def separator(self):
return "!@#$%^&*" # Very ugly separator
def _traverseXML(self, xmlElem, tags, xpaths):
tags.append(xmlElem.tag)
for e in xmlElem:
self._traverseXML(e, tags, xpaths)
text = ''
if (xmlElem.text):
text = xmlElem.text.strip()
xpaths.add("/".join(tags) + self.separator() + text)
tags.pop()
def _xmlToSet(self, xml):
xpaths = set() # output
tags = list()
root = ET.fromstring(xml)
self._traverseXML(root, tags, xpaths)
return xpaths
def _areXMLsAlike(self, xml1, xml2):
xpaths1 = self._xmlToSet(xml1)
xpaths2 = self._xmlToSet(xml2)`enter code here`
return xpaths1 == xpaths2
Étant donné que l'ordre des attributs n'est pas significatif dans XML , vous souhaitez ignorer les différences dues à différents ordres d'attributs et la canonisation XML (C14N) ordonne les attributs de manière déterministe, vous pouvez utiliser cette méthode pour tester l'égalité:
xml1 = b''' <?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats start="1275955200" end="1276041599"></Stats>'''
xml2 = b''' <?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats end="1276041599" start="1275955200"></Stats>'''
xml3 = b''' <?xml version='1.0' encoding='utf-8' standalone='yes'?>
<Stats start="1275955200"></Stats>'''
import lxml.etree
tree1 = lxml.etree.fromstring(xml1.strip())
tree2 = lxml.etree.fromstring(xml2.strip())
tree3 = lxml.etree.fromstring(xml3.strip())
import io
b1 = io.BytesIO()
b2 = io.BytesIO()
b3 = io.BytesIO()
tree1.getroottree().write_c14n(b1)
tree2.getroottree().write_c14n(b2)
tree3.getroottree().write_c14n(b3)
assert b1.getvalue() == b2.getvalue()
assert b1.getvalue() != b3.getvalue()
Notez que cet exemple suppose Python 3. Avec Python 3, l'utilisation de b'''...'''
chaînes et de io.BytesIO
est obligatoire, tandis qu'avec Python 2 cette méthode fonctionne également avec des chaînes normales et io.StringIO
.
SimpleTAL utilise un gestionnaire personnalisé xml.sax pour comparer des documents xml https://github.com/janbrohl/SimpleTAL/blob/python2/tests/TALTests/XMLTests/TALAttributeTestCases.py#L472 (les résultats de getXMLChecksum sont comparés) mais je préfère générer une liste au lieu d’un hash md5