Porównać fragmenty XML?

Bazując na kolejne pytanie , Jak można sprawdzić, czy dwa dobrze uformowane fragmenty XML są semantycznie równe. Wszystko, czego potrzebuję, to "równe" lub nie, ponieważ używam tego do testów jednostkowych.

W systemie, który chcę, będą one równe (zwróć uwagę na kolejność 'start' "end"): {]}

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

Mam do dyspozycji lmxl i inne narzędzia, a prosta funkcja, która pozwala tylko na zmianę kolejności atrybutów, również będzie działać dobrze!


Working snippet na podstawie odpowiedzi 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
Author: Community, 2010-06-09

10 answers

Możesz użyć formencode.doctest_xml_compare -- funkcja xml_compare porównuje dwa drzewa ElementTree lub lxml.

 27
Author: Ian Bicking,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-02-28 17:43:15

Kolejność elementów może być znacząca w XML, może to być powodem, dla którego większość innych sugerowanych metod porównuje nierówne, jeśli kolejność jest inna... nawet jeśli elementy mają te same atrybuty i zawartość tekstową.

Ale ja też chciałam mieć porównanie, więc wpadłam na to:

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
 14
Author: Anentropic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-29 15:01:30

Miałem ten sam problem: dwa dokumenty, które chciałem porównać, które miały te same atrybuty, ale w różnych porządkach.

Wydaje się, że Kanonicalizacja XML (C14N) w lxml działa do tego dobrze, ale zdecydowanie nie jestem ekspertem w XML. Jestem ciekaw, czy ktoś inny może wskazać wady tego podejścia.

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)

Running this gives me:

$ python test.py 
xml1 == xml2: false
pretty(xml1) == pretty(xml2): false
canonicalize(xml1) == canonicalize(xml2): true
 5
Author: Mark E. Haase,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-14 19:52:09

Tutaj proste rozwiązanie, przekonwertować XML do słowników (z xmltodict) i porównać słowniki razem

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 jednostkowy

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

Lub w prostej metodzie Pythona:

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))))
 5
Author: Guillaume Vincent,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-07 10:32:09

Jeśli zastosujesz podejście DOM, możesz przemierzać oba drzewa jednocześnie, porównując węzły (typ węzła, tekst, atrybuty).

Rozwiązanie rekurencyjne będzie najbardziej eleganckie - wystarczy zwarcie dalszego porównania, gdy para węzłów nie jest "równa" lub gdy wykryje się liść w jednym drzewie, gdy jest gałęzią w innym, itp.

 2
Author: Jeremy Brown,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-09 15:48:24

Myśląc o tym problemie, wymyśliłem następujące rozwiązanie, które sprawia, że elementy XML są porównywalne i sortowalne:

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
 2
Author: user3116268,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-18 18:08:26

Adaptacjaanentropic ' s great answer do Pythona 3 (zasadniczo zmień iteritems() na items(), a basestring na 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
 0
Author: Nico Villanueva,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:31

Ponieważ kolejność atrybutów nie jest znacząca w XML , chcesz zignorować różnice wynikające z różnych porządków atrybutów i kanonicalizacja XML (C14N) deterministycznie porządkuje atrybuty, Możesz tę metodę testowania równości:

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

Zauważ, że ten przykład zakłada Pythona 3. W Pythonie 3 Użycie b'''...''' i io.BytesIO jest obowiązkowe, podczas gdy w Pythonie 2 ta metoda działa również z normalnymi łańcuchami i io.StringIO.

 0
Author: maxschlepzig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-03 11:40:01

SimpleTAL używa niestandardowego xml.obsługa sax do porównywania dokumentów xml https://github.com/janbrohl/SimpleTAL/blob/python2/tests/TALTests/XMLTests/TALAttributeTestCases.py#L47-L112 (wyniki dla getXMLChecksum są porównywane) ale wolę generowanie listy zamiast MD5-hash

 0
Author: janbrohl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-25 18:44:37

A co z poniższym fragmentem kodu ? Może być łatwo rozszerzony o atrybuty:

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
 0
Author: Pankaj Raheja,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-18 12:29:27