Как сравнить два объекта JSON с одинаковыми элементами в разном порядке?


103

Как я могу проверить, равны ли два объекта JSON в python, не обращая внимания на порядок списков?

Например ...

Документ 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"}
    ]
}

aи bдолжны сравниваться одинаково, даже если порядок в "errors"списках разный.



1
Почему бы просто не расшифровать их и не сравнить? Или вы имеете в виду, что порядок «массива» или listэлементов тоже не имеет значения?
— mgilson

@ user2085282 У этого вопроса другая проблема.
— user193661

2
Прошу простить мою наивность, но почему? Элементы списка имеют определенный порядок не зря.
— ATOzTOA 07

1
Как отмечено в этом ответе, массив JSON сортируется, поэтому эти объекты, содержащие массивы с разными порядками сортировки, не будут равны в строгом смысле. stackoverflow.com/a/7214312/18891
— Эрик Несс

Ответы:


144

Если вы хотите, чтобы два объекта с одинаковыми элементами, но в разном порядке сравнивали равные, то очевидное дело - сравнить их отсортированные копии - например, для словарей, представленных вашими строками JSON aи b:

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"элемент dict верхнего уровня представляет собой список с одинаковыми элементами в другом порядке и 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

Если мы применим эту функцию к aи b, результаты будут равны:

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

1
Большое вам спасибо Zero Piraeus. это именно то общее решение, которое мне нужно. но единственная проблема в том, что код работает только для python 2.x, а не для python3. Я получаю следующую ошибку: TypeError: unorderable types: dict () <dict () В любом случае решение теперь ясно. Я постараюсь заставить его работать на python3. Большое спасибо

1
@HoussamHsm Я хотел исправить это для работы с Python 3.x, когда вы впервые упомянули о проблеме неупорядочиваемых dicts, но каким-то образом она ускользнула от меня. Теперь он работает как в 2.x, так и в 3.x :-)
— Zero Piraeus

когда есть список вроде ['astr', {'adict': 'something'}], я получил TypeErrorпри попытке их отсортировать.
— Zhenxiao Hao 04

1
@ Blairg23, вы неправильно поняли вопрос, который касается сравнения объектов JSON как равных, когда они содержат списки, элементы которых одинаковы, но в другом порядке, а не о каком-либо предполагаемом порядке словарей.
— Zero Piraeus

1
@ Blairg23 Я согласен с тем, что вопрос можно было бы написать более четко (хотя, если вы посмотрите историю редактирования , она лучше, чем начиналась). Re: словари и порядок - да, знаю ;-)
— Zero Piraeus

45

Другой способ - использовать 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"}против, {"error":"b"}, {"error":"a"} он не сможет отсортировать последний случай в первый случай
— ChromeHearts

@ Blairg23, а что бы вы сделали, если бы у вас были списки, вложенные в dict? Вы не можете просто сравнить диктат верхнего уровня и назвать это днем, это не то, о чем этот вопрос.
— stpk

4
Это не работает, если у вас есть списки внутри. например json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
— Данил

7
@ Данил и, наверное, не должно. Списки представляют собой упорядоченную структуру, и если они отличаются только порядком, мы должны считать их разными. Возможно, для вашего варианта использования порядок не имеет значения, но мы не должны этого предполагать.
— stpk

поскольку списки упорядочены по индексу, они не будут пересортированы. [0, 1] не должно равняться [1, 0] в большинстве ситуаций. Так что это хорошее решение для обычного случая, но не для вопроса выше. все еще +1
— Харрисон

18

Расшифруйте их и сравните как комментарий 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.


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

дал мне неправильный результат, т.е. ложный.

Итак, я создал свой собственный компаратор объектов Cutstom вот так:

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

что дало мне правильный ожидаемый результат!

Логика довольно проста:

Если объекты относятся к типу «список», тогда сравнивайте каждый элемент первого списка с элементами второго списка до тех пор, пока он не будет найден, и если элемент не будет найден после просмотра второго списка, тогда «найденный» будет = false. возвращается значение 'found'

В противном случае, если сравниваемые объекты относятся к типу «dict», сравните значения, представленные для всех соответствующих ключей в обоих объектах. (Выполняется рекурсивное сравнение)

В противном случае просто вызовите obj1 == obj2. По умолчанию он отлично работает для объекта строк и чисел, и для них eq () определен соответствующим образом.

(Обратите внимание, что алгоритм можно дополнительно улучшить, удалив элементы, найденные в объекте 2, чтобы следующий элемент объекта 1 не сравнивался с элементами, уже найденными в объекте 2)


Не могли бы вы исправить отступы в вашем коде?
— colidyre

@colidyre теперь в порядке с отступом?
— NiksVij

Нет, все еще есть проблемы. После заголовка функции блок также должен иметь отступ.
— colidyre

Да. Я отредактировал еще раз. Копирую, вставил в IDE, и теперь он работает.
— NiksVij

1

Вы можете написать свою собственную функцию равенства:

  • dicts равны, если: 1) все ключи равны, 2) все значения равны
  • списки равны, если: все элементы равны и находятся в одном порядке
  • примитивы равны, если a == b

Потому что вы имеете дело с JSON, вы будете иметь стандартные типы питона: dict, listи т.д., так что вы можете сделать жесткий контроль типов 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

0

Для тех, кто хочет отладить два объекта 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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.