如何比较两个元素相同但顺序不同的JSON对象是否相等?

160

如何在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"列表的顺序不同。


2
重复的https://dev59.com/gWgu5IYBdhLWcg3ws5BC - user2085282
1
为什么不直接解码并比较呢?或者你是指“Array”或list元素的顺序也不重要? - mgilson
@user2085282,该问题存在不同的问题。 - user193661
2
请原谅我的天真,但是为什么呢?列表元素之间有特定的顺序是有原因的。 - ATOzTOA
1
正如这个答案中指出的那样,一个JSON数组是有序的,因此包含不同排序顺序数组的对象在严格意义上不相等。https://dev59.com/5Gw05IYBdhLWcg3wfhyg#7214312 - Eric Ness
对于@ATOzTOA和其他人的疑问,常见的原因可能是您确实拥有集合,并且想要测试集合的相等性,但是由于JSON没有集合,所以不得不将数据强制转换为列表。 - Ken Williams
9个回答

212
如果您希望两个具有相同元素但顺序不同的对象比较相等,那么显而易见的做法是比较它们的排序副本 - 例如,对于由您的JSON字符串ab表示的字典:
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

......但是这并不起作用,因为在每种情况下,顶层字典的"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

1
非常感谢Zero Piraeus。这正是我需要的通用解决方案。但唯一的问题是,这段代码只适用于Python 2.x而不适用于Python 3。我得到了以下错误信息: TypeError: unorderable types: dict() < dict()无论如何,解决方案现在很明确。我将尝试使其适用于Python 3。非常感谢! - user1635536
4
@HoussamHsm 当你第一次提到字典无序的问题时,我本来想修改代码以适配Python 3.x的,但不知怎么就忘了。现在它能同时在2.x和3.x版本中运行了 :-) - Zero Piraeus
1
@Blairg23,你误解了问题,问题是关于比较JSON对象是否相等,当它们包含的列表元素相同时,但顺序不同,而不是关于字典的任何假定顺序。 - Zero Piraeus
是的,如果您比较两个具有相同元素但顺序不同的字典,则无需事先对它们进行排序。当您将JSON对象导入Python时,它们等效于字典。试试吧,你会明白我的意思。更新:我现在明白问题在问什么了。它说“列表元素的不同顺序”,这是一个完全不同的问题。两个字典将是等效的,无论顺序如何,除非它们包含具有不同顺序的列表。 - Blairg23
1
@Blairg23 我同意这个问题的表述可以更清晰(不过如果你看一下编辑历史记录,它比最初要好)。关于字典和顺序 - 是的,我知道 ;-) - Zero Piraeus
显示剩余3条评论

84

另一种方法是使用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

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


1
{"error":"a"}, {"error":"b"}{"error":"b"}, {"error":"a"}它无法将后一种情况排序为第一种情况。 - ChromeHearts
@Blairg23 但是如果字典中嵌套了列表,你该怎么办?你不能只比较顶层字典并结束,这不是这个问题的重点。 - stpk
9
如果有嵌套列表的话就不能使用这个方法。比如:json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True) - Danil
14
@Danil,也许不应该这样做。列表是有序结构,如果它们只是在顺序上不同,我们应该认为它们是不同的。也许对于你的用例来说顺序并不重要,但我们不应该假设。 - stpk
4
虽然列表是有序结构,但这并不意味着没有任务可以检查两个列表是否包含相同的元素,而不考虑它们的顺序。同样的事情也适用于字典,即相同问题。 - igrek
显示剩余2条评论

20

按照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。有关通用解决方案,请参阅Zero Piraeus的答案。


9

是的!你可以使用 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}
    ]
}

这些结果可以呈现为在此输入图片描述


1
现在JYCM有一个CLI工具,您可以直接使用它来可视化差异结果! https://github.com/eggachecat/jycm - eggachecat
+1 的功能是可以指定忽略特定键和定义自己的 diff 函数。 - Gino Mempin

3
您可以编写自己的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

2

对于以下两个字典'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

我收到了错误的结果,即false。

因此,我创建了自己的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

这使我得到了正确的预期输出!

逻辑很简单:

如果对象是“list”类型,则将第一个列表的每个项与第二个列表的项进行比较,直到找到为止。如果在遍历第二个列表后未找到该项,则“found”将为false。返回“found”的值。

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

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

(请注意,通过从object2中删除已找到的项目,可以进一步改进算法,以便object1的下一个项目不会将自己与已在object2中找到的项目进行比较)


你能否请修正一下你代码的缩进? - colidyre
@colidyre,缩进现在没问题了吗? - NiksVij
不行,问题仍然存在。在函数头之后,代码块也必须缩进。 - colidyre
是的,我重新编辑了一次。我将它复制粘贴到IDE中,现在它可以工作了。 - NiksVij

2
对于想要调试两个JSON对象(通常有“参考”和“目标”)的其他人,这里是一个解决方案。它将列出从目标到参考的不同/不匹配的“路径”。
“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

0
import json

#API response sample
# some JSON:

x = '{ "name":"John", "age":30, "city":"New York"}'

# parse x json to Python dictionary:
y = json.loads(x)

#access Python dictionary
print(y["age"])


# expected json as dictionary
thisdict = { "name":"John", "age":30, "city":"New York"}
print(thisdict)


# access Python dictionary
print(thisdict["age"])

# Compare Two access Python dictionary

if thisdict == y:
    print ("dict1 is equal to dict2")
else:
    print ("dict1 is not equal to dict2")

https://drive.google.com/file/d/1_nUU_w0mA1Rl9izves-6flSkN7i7bIxI/view?usp=share_link - Kamaraj Kannan
你好,请查看https://meta.stackoverflow.com/editing-help 谢谢! - Eric Aya
目前你的回答不够清晰。请编辑并添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

-2

使用KnoDL,可以在不映射字段的情况下匹配数据。


1
这应该是一条注释,而不是答案。如果想转换成答案,请添加功能代码或更深入的解释。 - BLimitless

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接