another SO question に基づいて、2つの整形式XMLスニペットが意味的に等しいかどうかを確認する方法を教えてください。必要なのは「等しい」かどうかだけですm単体テストにこれを使用します。
私が欲しいシステムでは、これらは等しくなります(「開始」と「終了」の順序に注意してください):
<?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関数は、2つの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
私は同じ問題を抱えていました。同じ属性を持ち、順序が異なる、比較したい2つのドキュメントです。
LxmlのXML Canonicalization(C14N)はこれに適しているようですが、私は間違いなく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
単体テスト
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 method:
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))))
この問題について考えて、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
DOMアプローチを採用すると、ノード(ノードタイプ、テキスト、属性)を比較しながら2つのツリーを同時にトラバースできます。
再帰的ソリューションは最もエレガントです。ノードのペアが「等しくない」場合や、あるツリーのリーフが別のツリーのブランチであることが検出された場合などは、さらに比較を短絡します。
適応 Anentropic's great answer to 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
次のコードスニペットはどうですか? attribsを含めるように簡単に拡張できます。
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
属性の順序は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-documentsを比較します https://github.com/janbrohl/SimpleTAL/blob/python2/tests/TALTests/XMLTests/TALAttributeTestCases.py#L47-L112 ( getXMLChecksumの結果が比較されます)が、md5-hashではなくリストを生成することを好みます