Ответ 1
Вы можете использовать formencode.doctest_xml_compare - функция xml_compare сравнивает два дерева ElementTree или lxml.
Основываясь на другом вопросе SO, как можно проверить, являются ли два хорошо сформированных фрагмента XML семантически равными. Все, что мне нужно, это "равно" или нет, поскольку я использую это для модульных тестов.
В системе, которую я хочу, они будут равны (обратите внимание на порядок "начала" ) и "конец" ):
<?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>
У меня есть lmxl и другие инструменты в моем распоряжении, и простая функция, которая разрешает переупорядочивание атрибутов, также будет работать отлично!
Рабочий фрагмент на основе ответа 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
Вы можете использовать formencode.doctest_xml_compare - функция xml_compare сравнивает два дерева ElementTree или lxml.
Порядок элементов может быть значительным в XML, возможно, поэтому большинство других предложенных методов сравниваются неравномерно, если порядок отличается... даже если элементы имеют одинаковые атрибуты и текстовое содержимое.
Но я также хотел нечувствительность к порядку, поэтому я придумал это:
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
У меня была та же проблема: два документа, которые я хотел сравнить, которые имели одинаковые атрибуты, но в разных порядках.
Кажется, что XML Canonicalization (C14N) в lxml хорошо работает для этого, но я определенно не эксперт по XML. Мне любопытно узнать, может ли кто-то еще указать на недостатки этого подхода.
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)
Выполнение этого дает мне:
$ python test.py
xml1 == xml2: false
pretty(xml1) == pretty(xml2): false
canonicalize(xml1) == canonicalize(xml2): true
Здесь простое решение, преобразование XML в словари (с xmltodict) и сравнение словарей вместе
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
unit test
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())
или в простом методе python:
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))))
Если вы используете подход DOM, вы можете перемещаться по двум деревьям одновременно при сравнении узлов (node type, text, attributes) по мере того, как вы идете.
Рекурсивное решение будет самым элегантным - просто короткое замыкание последующего сравнения, если пара узлов не будет "равна" или когда вы обнаружите лист в одном дереве, когда он является ветвью в другом и т.д.
Размышляя об этой проблеме, я придумал следующее решение, которое сопоставляет и сопоставляет элементы XML:
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
Адаптация Отличный ответ Anentropic на Python 3 (в основном, измените iteritems()
на items()
и 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
Так как порядок атрибутов не значим в XML, вы хотите игнорировать различия из-за разных порядков атрибутов и Канонизация XML (C14N) детерминистически заказывает атрибуты, вы можете использовать этот метод для проверки равенства:
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()
Обратите внимание, что в этом примере предполагается Python 3. С Python 3 использование строк b'''...'''
и io.BytesIO
является обязательным, а с Python 2 этот метод также работает с нормальными строками и io.StringIO
.
SimpleTAL использует собственный обработчик xml.sax для сравнения xml-документов https://github.com/janbrohl/SimpleTAL/blob/python2/tests/TALTests/XMLTests/TALAttributeTestCases.py#L47-L112 (сравниваются результаты для getXMLChecksum) но я предпочитаю генерировать список вместо md5-hash
Как насчет следующего фрагмента кода? Может быть легко улучшена, чтобы включить атрибуты:
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