2010-06-09 7 views
30

Aufbauend auf another SO question, wie kann man prüfen, ob zwei wohlgeformte XML-Snippets semantisch gleich sind. Alles was ich brauche ist "gleich" oder nicht, da ich das für Unit-Tests verwende.XML-Snippets vergleichen?

Im System ich will, würde diese gleich sein (man beachte die Reihenfolge des ‚Starts‘ und ‚Ende‘):

<?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> 

Ich habe lmxl und andere Werkzeuge zu meiner Verfügung, und eine einfache Funktion nur das Neuordnen von Attributen würde ebenfalls funktionieren!


Arbeits Snippet auf IanB Antwort basiert:

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 
+1

'von formencode.doctest_xml_compare Import xml_compare' – laike9m

Antwort

24

Sie können formencode.doctest_xml_compare verwenden - die xml_compare-Funktion vergleicht zwei ElementTree- oder lxml-Bäume.

+0

Danke Ian, ich bin froh, dass jemand das schon gelöst hat! –

+2

Diese Funktion ist falsch, wenn Sie die Reihenfolge der Attribute in xml vertauschen, wird False zurückgegeben. – mnowotka

+0

@mnowotka nicht wahr, es vergleicht gleich für _attributes_ in unterschiedlicher Reihenfolge – Anentropic

2

Wenn Sie einen DOM-Ansatz nehmen, können Sie die beiden Bäume gleichzeitig durchqueren, während die Knoten zu vergleichen (Knotentyp, Text, Attribute), wie Sie gehen.

Eine rekursive Lösung wird die eleganteste sein - nur kurzschluss weiteren Vergleich einmal ein Paar von Knoten nicht „gleich“ oder, wenn Sie ein Blatt in einem Baum erkennen, wenn es sich um eine Niederlassung in einem anderen ist, usw.

+1

Dies ist die Lösung, hatte gehofft, dass ich nur jemand eine bereits geschrieben hatte. –

5

Ich hatte das gleiche Problem: zwei Dokumente, die ich vergleichen wollte, hatte die gleichen Attribute, aber in verschiedenen Ordnungen.

Es scheint, dass XML Canonicalization (C14N) in Lxml funktioniert gut dafür, aber ich bin definitiv kein XML-Experte. Ich bin neugierig zu wissen, ob jemand anderes auf diese Vorgehensweise hinweisen kann.

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) 

das Lauf gibt mir:

$ python test.py 
xml1 == xml2: false 
pretty(xml1) == pretty(xml2): false 
canonicalize(xml1) == canonicalize(xml2): true 
+0

Hatte auch diesen Ansatz im Hinterkopf und bin auf der Suche nach den Nachteilen oder ob dies wirklich die kanonische Art sein könnte, XML-Dokumente zu vergleichen ... (Wortspiel beabsichtigt) – michuelnik

+0

Ich benutze dies seit über einem Jahr auf einer Website, die ich vergleiche XML-Dokumente zur Versionskontrolle. Es funktioniert ziemlich gut, aber das c14n kontrolliert nicht, um die gleichen Kindelemente in einer anderen Reihenfolge zu haben, so dass ich manchmal immer noch falsche Ergebnisse bekomme. –

+0

Ersetzt c14n childs? Ich denke nein ... Meinst du den Fall, wo die gleichen Kinder anwesend sind, aber in einer anderen Reihenfolge, möchtest du ein "kein Unterschied" Ergebnis, aber das liefert "Unterschied erkannt"? In meinem Fall könnte die Reihenfolge der Kinder wichtig sein. ;) – michuelnik

1

über dieses Problem denken, kam ich auf die folgende Lösung, die XML-Elemente vergleichbar und sortierbar macht:

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 
14

Die Reihenfolge der Elemente können in XML signifikant sein. Dies ist der Grund, warum die meisten anderen vorgeschlagenen Methoden ungleiche Werte vergleichen, wenn die Reihenfolge unterschiedlich ist ... selbst wenn die Elemente die gleichen Attribute und Textinhalte haben.

Aber ich wollte auch einen Auftrag unempfindlichen Vergleich, so kam ich mit auf den Punkt:

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 
+1

Dies ist definitiv die beste Antwort und sollte akzeptiert werden. Dies ist die einzige Antwort, die sich tatsächlich um die entscheidende Tatsache kümmert, dass die Reihenfolge der Felder in XML keine Rolle spielt. – mnowotka

+3

gibt es zwei Dinge zu beachten: Reihenfolge der _attributes_ ist wirklich egal. Allerdings ist die Reihenfolge der Elemente in XML signifikant, dieser Code ist für einen speziellen Fall, in dem die Reihenfolge der Elemente nicht wichtig ist. – Anentropic

0

Anpassung Anentropic's great answer auf Python 3 (im Grunde, iteritems() zu items() ändern und basestring-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 
+1

Sie können die Option 'dict_constructor = dict' für xmltodict verwenden:' xmltodict.parse (a, dict_constructor = dict) ', Sie sollten also nicht die Funktion' normalise_dict' verwenden müssen. – inoks

0

Da die order of attributes is not significant in XML, möchten Sie Unterschiede aufgrund unterschiedlicher Attribut Sortierungen und XML canonicalization (C14N) deterministisch Orders Attribut ignorieren s, können Sie diese Methode zum Testen der Gleichheit:

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

Beachten Sie, dass dieses Beispiel Python 3 annimmt.Bei Python 3 ist die Verwendung von b'''...''' Strings und io.BytesIO obligatorisch, während bei Python 2 diese Methode auch mit normalen Strings und io.StringIO funktioniert.

5

Hier ist eine einfache Lösung, Konvertieren von XML in Wörterbuch (mit xmltodict) und zusammen

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 

Komponententest

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

oder in einfachen Python Verfahren Worte vergleichen:

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)))) 
0

Was ist mit dem folgenden Code-Schnipsel zu erzeugen? Kann leicht auch umfassen attribs verbessert werden:

def separator(self): 
    return "[email protected]#$%^&*" # 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