【问题标题】:Compare XML snippets?比较 XML 片段?
【发布时间】:2010-06-09 15:33:25
【问题描述】:

another SO question 为基础,如何检查两个格式良好的 XML sn-ps 在语义上是否相等。我所需要的只是“相等”与否,因为我将它用于单元测试。

在我想要的系统中,这些是相等的(注意“开始”的顺序 和“结束”):

<?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 的回答工作 sn-p:

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

【问题讨论】:

  • from formencode.doctest_xml_compare import xml_compare

标签: python xml diff


【解决方案1】:

您可以使用formencode.doctest_xml_compare -- xml_compare 函数比较两个 ElementTree 或 lxml 树。

【讨论】:

  • 这个函数不正确,如果你在xml中交换属性顺序会返回False。
  • @mnowotka 不正确,它确实以不同的顺序比较 属性 相等
  • @ian-bicking 很棒的工具!你给它贴了许可证吗,我似乎找不到任何信息。
  • 这是一个很棒而且很有帮助的 sn-p。谢谢!
【解决方案2】:

元素的顺序在 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 中字段顺序无关紧要这一关键事实的答案。
  • 有两件事要考虑:属性的顺序真的不重要。但是元素的顺序在 XML 中很重要,此代码适用于您不关心元素顺序的特殊情况
【解决方案3】:

这里有一个简单的解决方案,将 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方法:

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

【讨论】:

    【解决方案4】:

    我遇到了同样的问题:我想比较的两个文档具有相同的属性但顺序不同。

    lxml 中的 XML 规范化 (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 文档的规范方法...(双关语)
    • 我在一个比较 XML 文档以进行版本控制的网站上使用它已经一年多了。它工作得相当好,但 c14n 不能控制以不同的顺序拥有相同的子元素,所以有时我仍然会得到虚假的结果。
    • c14n 是否重新排序孩子?我猜不...您的意思是存在相同孩子但顺序不同的情况,您希望得到“没有差异”的结果,但这会提供“检测到差异”吗?在我的情况下,孩子的顺序可能很重要。 ;)
    • @michuelnik 正确。 C14n 不会重新排序孩子。在我的用例中,这会导致误报,我不关心孩子的顺序。
    【解决方案5】:

    如果您采用 DOM 方法,您可以在比较节点(节点类型、文本、属性)的同时同时遍历两棵树。

    递归解决方案将是最优雅的 - 一旦一对节点不“相等”或一旦您检测到一棵树中的叶子而另一棵树的分支等等,只需短路进一步比较即可。

    【讨论】:

    • 这是解决方案,我只是希望有人已经写了一个。
    【解决方案6】:

    考虑到这个问题,我想出了以下解决方案,它可以使 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
    

    【讨论】:

      【解决方案7】:

      Anentropic's great answer 适配为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
      

      【讨论】:

      • 您可以为 xmltodict 使用 dict_constructor=dict 选项:xmltodict.parse(a, dict_constructor=dict),因此您不需要使用 normalise_dict 函数。
      【解决方案8】:

      由于order of attributes is not significant in XML,您想忽略由于不同属性排序而导致的差异,而XML canonicalization (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

      【讨论】:

        【解决方案9】:

        SimpleTAL 使用自定义 xml.sax 处理程序来比较 xml 文档 https://github.com/janbrohl/SimpleTAL/blob/python2/tests/TALTests/XMLTests/TALAttributeTestCases.py#L47-L112 (比较 getXMLChecksum 的结果) 但我更喜欢生成一个列表而不是 md5-hash

        【讨论】:

          【解决方案10】:

          下面的代码 sn-p 怎么样?也可以轻松增强以包含属性:

          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
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-08-26
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多