【问题标题】:How to compare two JSON objects with the same elements in a different order equal?如何以不同的顺序比较具有相同元素的两个 JSON 对象相等?
【发布时间】:2014-11-09 03:57:21
【问题描述】:

如何在python中测试两个JSON对象是否相等,而不考虑列表的顺序?

例如...

JSON 文档 a

{
    "errors": [
        {"error": "invalid", "field": "email"},
        {"error": "required", "field": "name"}
    ],
    "success": false
}

JSON 文档 b

{
    "success": false,
    "errors": [
        {"error": "required", "field": "name"},
        {"error": "invalid", "field": "email"}
    ]
}

ab 应该比较相等,即使 "errors" 列表的顺序不同。

【问题讨论】:

  • 为什么不直接解码并比较呢?还是您的意思是“数组”或list 元素的顺序也不重要?
  • @user2085282 这个问题有另一个问题。
  • 请原谅我的幼稚,但为什么呢?列表元素有特定的顺序是有原因的。
  • 如本答案所述,对 JSON 数组进行了排序,因此这些包含具有不同排序顺序的数组的对象在严格意义上不会相等。 stackoverflow.com/a/7214312/18891

标签: python json django comparison


【解决方案1】:

是的!你可以使用jycm

from jycm.helper import make_ignore_order_func
from jycm.jycm import YouchamaJsonDiffer

a = {
    "errors": [
        {"error": "invalid", "field": "email"},
        {"error": "required", "field": "name"}
    ],
    "success": False
}
b = {
    "success": False,
    "errors": [
        {"error": "required", "field": "name"},
        {"error": "invalid", "field": "email"}
    ]
}
ycm = YouchamaJsonDiffer(a, b, ignore_order_func=make_ignore_order_func([
    "^errors",
]))
ycm.diff()
assert ycm.to_dict(no_pairs=True) == {} # aka no diff

举一个更复杂的例子(深层结构中的值变化)

from jycm.helper import make_ignore_order_func
from jycm.jycm import YouchamaJsonDiffer

a = {
    "errors": [
        {"error": "invalid", "field": "email"},
        {"error": "required", "field": "name"}
    ],
    "success": True
}

b = {
    "success": False,
    "errors": [
        {"error": "required", "field": "name-1"},
        {"error": "invalid", "field": "email"}
    ]
}
ycm = YouchamaJsonDiffer(a, b, ignore_order_func=make_ignore_order_func([
    "^errors",
]))
ycm.diff()
assert ycm.to_dict() == {
    'just4vis:pairs': [
        {'left': 'invalid', 'right': 'invalid', 'left_path': 'errors->[0]->error', 'right_path': 'errors->[1]->error'},
        {'left': {'error': 'invalid', 'field': 'email'}, 'right': {'error': 'invalid', 'field': 'email'},
         'left_path': 'errors->[0]', 'right_path': 'errors->[1]'},
        {'left': 'email', 'right': 'email', 'left_path': 'errors->[0]->field', 'right_path': 'errors->[1]->field'},
        {'left': {'error': 'invalid', 'field': 'email'}, 'right': {'error': 'invalid', 'field': 'email'},
         'left_path': 'errors->[0]', 'right_path': 'errors->[1]'},
        {'left': 'required', 'right': 'required', 'left_path': 'errors->[1]->error',
         'right_path': 'errors->[0]->error'},
        {'left': {'error': 'required', 'field': 'name'}, 'right': {'error': 'required', 'field': 'name-1'},
         'left_path': 'errors->[1]', 'right_path': 'errors->[0]'},
        {'left': 'name', 'right': 'name-1', 'left_path': 'errors->[1]->field', 'right_path': 'errors->[0]->field'},
        {'left': {'error': 'required', 'field': 'name'}, 'right': {'error': 'required', 'field': 'name-1'},
         'left_path': 'errors->[1]', 'right_path': 'errors->[0]'},
        {'left': {'error': 'required', 'field': 'name'}, 'right': {'error': 'required', 'field': 'name-1'},
         'left_path': 'errors->[1]', 'right_path': 'errors->[0]'}
    ],
    'value_changes': [
        {'left': 'name', 'right': 'name-1', 'left_path': 'errors->[1]->field', 'right_path': 'errors->[0]->field',
         'old': 'name', 'new': 'name-1'},
        {'left': True, 'right': False, 'left_path': 'success', 'right_path': 'success', 'old': True, 'new': False}
    ]
}

其结果可以呈现为

【讨论】:

    【解决方案2】:

    使用 KnoDL,无需映射字段即可匹配数据。

    【讨论】:

    • 这应该是评论,而不是答案。如果您想转换为答案,请添加功能代码或更深入的解释。
    【解决方案3】:

    对于想要调试这两个 JSON 对象(通常有一个 reference 和一个 target)的其他人,您可以使用以下解决方案。它将列出从目标到参考的不同/不匹配的“path”。

    level 选项用于选择您想要查看的深度。

    show_variables选项可以打开以显示相关变量。

    def compareJson(example_json, target_json, level=-1, show_variables=False):
      _different_variables = _parseJSON(example_json, target_json, level=level, show_variables=show_variables)
      return len(_different_variables) == 0, _different_variables
    
    def _parseJSON(reference, target, path=[], level=-1, show_variables=False):  
      if level > 0 and len(path) == level:
        return []
      
      _different_variables = list()
      # the case that the inputs is a dict (i.e. json dict)  
      if isinstance(reference, dict):
        for _key in reference:      
          _path = path+[_key]
          try:
            _different_variables += _parseJSON(reference[_key], target[_key], _path, level, show_variables)
          except KeyError:
            _record = ''.join(['[%s]'%str(p) for p in _path])
            if show_variables:
              _record += ': %s <--> MISSING!!'%str(reference[_key])
            _different_variables.append(_record)
      # the case that the inputs is a list/tuple
      elif isinstance(reference, list) or isinstance(reference, tuple):
        for index, v in enumerate(reference):
          _path = path+[index]
          try:
            _target_v = target[index]
            _different_variables += _parseJSON(v, _target_v, _path, level, show_variables)
          except IndexError:
            _record = ''.join(['[%s]'%str(p) for p in _path])
            if show_variables:
              _record += ': %s <--> MISSING!!'%str(v)
            _different_variables.append(_record)
      # the actual comparison about the value, if they are not the same, record it
      elif reference != target:
        _record = ''.join(['[%s]'%str(p) for p in path])
        if show_variables:
          _record += ': %s <--> %s'%(str(reference), str(target))
        _different_variables.append(_record)
    
      return _different_variables
    

    【讨论】:

      【解决方案4】:

      你可以编写自己的equals函数:

      • 如果满足以下条件,则字典相等:1) 所有键都相等,2) 所有值都相等
      • 如果满足以下条件,则列表是相等的:所有项目都相等且顺序相同
      • 如果a == b,原语是相等的

      因为您处理的是 json,所以您将拥有标准的 python 类型:dictlist 等,因此您可以进行硬类型检查 if type(obj) == 'dict': 等。

      粗略示例(未测试):

      def json_equals(jsonA, jsonB):
          if type(jsonA) != type(jsonB):
              # not equal
              return False
          if type(jsonA) == dict:
              if len(jsonA) != len(jsonB):
                  return False
              for keyA in jsonA:
                  if keyA not in jsonB or not json_equal(jsonA[keyA], jsonB[keyA]):
                      return False
          elif type(jsonA) == list:
              if len(jsonA) != len(jsonB):
                  return False
              for itemA, itemB in zip(jsonA, jsonB):
                  if not json_equal(itemA, itemB):
                      return False
          else:
              return jsonA == jsonB
      

      【讨论】:

        【解决方案5】:

        对于以下两个字典“dictWithListsInValue”和“reorderedDictWithReorderedListsInValue”,它们只是彼此重新排序的版本

        dictObj = {"foo": "bar", "john": "doe"}
        reorderedDictObj = {"john": "doe", "foo": "bar"}
        dictObj2 = {"abc": "def"}
        dictWithListsInValue = {'A': [{'X': [dictObj2, dictObj]}, {'Y': 2}], 'B': dictObj2}
        reorderedDictWithReorderedListsInValue = {'B': dictObj2, 'A': [{'Y': 2}, {'X': [reorderedDictObj, dictObj2]}]}
        a = {"L": "M", "N": dictWithListsInValue}
        b = {"L": "M", "N": reorderedDictWithReorderedListsInValue}
        
        print(sorted(a.items()) == sorted(b.items()))  # gives false
        

        给了我错误的结果,即错误。

        所以我像这样创建了自己的 cuttom ObjectComparator:

        def my_list_cmp(list1, list2):
            if (list1.__len__() != list2.__len__()):
                return False
        
            for l in list1:
                found = False
                for m in list2:
                    res = my_obj_cmp(l, m)
                    if (res):
                        found = True
                        break
        
                if (not found):
                    return False
        
            return True
        
        
        def my_obj_cmp(obj1, obj2):
            if isinstance(obj1, list):
                if (not isinstance(obj2, list)):
                    return False
                return my_list_cmp(obj1, obj2)
            elif (isinstance(obj1, dict)):
                if (not isinstance(obj2, dict)):
                    return False
                exp = set(obj2.keys()) == set(obj1.keys())
                if (not exp):
                    # print(obj1.keys(), obj2.keys())
                    return False
                for k in obj1.keys():
                    val1 = obj1.get(k)
                    val2 = obj2.get(k)
                    if isinstance(val1, list):
                        if (not my_list_cmp(val1, val2)):
                            return False
                    elif isinstance(val1, dict):
                        if (not my_obj_cmp(val1, val2)):
                            return False
                    else:
                        if val2 != val1:
                            return False
            else:
                return obj1 == obj2
        
            return True
        
        
        dictObj = {"foo": "bar", "john": "doe"}
        reorderedDictObj = {"john": "doe", "foo": "bar"}
        dictObj2 = {"abc": "def"}
        dictWithListsInValue = {'A': [{'X': [dictObj2, dictObj]}, {'Y': 2}], 'B': dictObj2}
        reorderedDictWithReorderedListsInValue = {'B': dictObj2, 'A': [{'Y': 2}, {'X': [reorderedDictObj, dictObj2]}]}
        a = {"L": "M", "N": dictWithListsInValue}
        b = {"L": "M", "N": reorderedDictWithReorderedListsInValue}
        
        print(my_obj_cmp(a, b))  # gives true
        

        这给了我正确的预期输出!

        逻辑很简单:

        如果对象是“列表”类型,则将第一个列表的每个项目与第二个列表的项目进行比较,直到找到,如果在通过第二个列表后未找到该项目,则“找到”将是=假。返回“找到”值

        否则,如果要比较的对象是“dict”类型,则比较两个对象中所有相应键的值。 (递归比较)

        否则只需调用 obj1 == obj2 。默认情况下,它适用于字符串和数字的对象,并且对于那些 eq() 被适当地定义。

        (请注意,可以通过删除object2中找到的项目来进一步改进算法,这样object1的下一个项目就不会与object2中已经找到的项目进行比较)

        【讨论】:

        • 能否请fix the indentation 提供您的代码?
        • @colidyre 现在缩进可以了吗?
        • 不,仍然存在问题。在函数头之后,块也必须缩进。
        • 是的。我又重新编辑了一遍。我将它复制粘贴到 IDE 中,它现在可以工作了。
        【解决方案6】:

        另一种方法是使用json.dumps(X, sort_keys=True) 选项:

        import json
        a, b = json.dumps(a, sort_keys=True), json.dumps(b, sort_keys=True)
        a == b # a normal string comparison
        

        这适用于嵌套字典和列表。

        【讨论】:

        • {"error":"a"}, {"error":"b"} vs {"error":"b"}, {"error":"a"} 将无法将后一种情况分类为第一种情况
        • @Blairg23 但是如果你有嵌套在字典中的列表,你会怎么做?你不能只比较顶级字典就结束了,这不是这个问题的目的。
        • 如果里面有列表,这不起作用。例如json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
        • @Danil 也许不应该。列表是一种有序结构,如果它们仅在顺序上有所不同,我们应该认为它们是不同的。也许对于您的用例,顺序并不重要,但我们不应该假设。
        • @stpk 鉴于列表是一个有序结构并不意味着无论它们的顺序如何,都无法检查两个列表是否包含相同的元素。同样的事情也适用于字典,也就是问题
        【解决方案7】:

        如果您希望两个具有相同元素但顺序不同的对象比较相等,那么显而易见的做法是比较它们的排序副本 - 例如,对于由 JSON 字符串 a 和 @ 表示的字典987654324@:

        import json
        
        a = json.loads("""
        {
            "errors": [
                {"error": "invalid", "field": "email"},
                {"error": "required", "field": "name"}
            ],
            "success": false
        }
        """)
        
        b = json.loads("""
        {
            "success": false,
            "errors": [
                {"error": "required", "field": "name"},
                {"error": "invalid", "field": "email"}
            ]
        }
        """)
        
        >>> sorted(a.items()) == sorted(b.items())
        False
        

        ... 但这不起作用,因为在每种情况下,顶级 dict 的 "errors" 项都是具有不同顺序的相同元素的列表,而 sorted() 不会尝试对除可迭代对象的“顶级”级别之外的任何内容进行排序。

        要解决这个问题,我们可以定义一个 ordered 函数,该函数将对它找到的任何列表进行递归排序(并将字典转换为 (key, value) 对的列表,以便它们可排序):

        def ordered(obj):
            if isinstance(obj, dict):
                return sorted((k, ordered(v)) for k, v in obj.items())
            if isinstance(obj, list):
                return sorted(ordered(x) for x in obj)
            else:
                return obj
        

        如果我们将此函数应用于ab,结果比较相等:

        >>> ordered(a) == ordered(b)
        True
        

        【讨论】:

        • 非常感谢零比雷埃夫斯。这正是我需要的通用解决方案。但唯一的问题是代码仅适用于 python 2.x,不适用于 python3。我收到以下错误: TypeError: unorderable types: dict()
        • @HoussamHsm 当您第一次提到不可排序的 dicts 问题时,我打算修复此问题以与 Python 3.x 一起使用,但不知何故它离开了我。它现在适用于 2.x 和 3.x :-)
        • 当有像['astr', {'adict': 'something'}] 这样的列表时,我在尝试对它们进行排序时得到TypeError
        • @Blairg23 你误解了这个问题,这是关于当 JSON 对象包含元素相同但顺序不同的列表时比较它们是否相等,不是关于任何假定的字典顺序。
        • @Blairg23 我同意这个问题可以写得更清楚(尽管如果您查看edit history,它比开始时要好)。回复:字典和订单——yes, I know ;-)
        【解决方案8】:

        解码它们并将它们作为 mgilson 评论进行比较。

        只要键和值匹配,字典的顺序就无关紧要。 (字典在Python中没有顺序)

        >>> {'a': 1, 'b': 2} == {'b': 2, 'a': 1}
        True
        

        但是列表中的顺序很重要;排序将解决列表的问题。

        >>> [1, 2] == [2, 1]
        False
        >>> [1, 2] == sorted([2, 1])
        True
        

        >>> a = '{"errors": [{"error": "invalid", "field": "email"}, {"error": "required", "field": "name"}], "success": false}'
        >>> b = '{"errors": [{"error": "required", "field": "name"}, {"error": "invalid", "field": "email"}], "success": false}'
        >>> a, b = json.loads(a), json.loads(b)
        >>> a['errors'].sort()
        >>> b['errors'].sort()
        >>> a == b
        True
        

        以上示例适用于问题中的 JSON。一般解决方案见零比雷埃夫斯的回答。

        【讨论】:

          猜你喜欢
          • 2011-12-27
          • 1970-01-01
          • 2014-11-20
          • 1970-01-01
          • 2020-03-23
          • 2016-09-15
          • 2019-10-24
          • 1970-01-01
          • 2022-01-18
          相关资源
          最近更新 更多