Как сравнить два объекта 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"списках разный.

Петтер Фриберг
источник
2
Дубликат stackoverflow.com/questions/11141644/…
user2085282
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

Это работает для вложенных словарей и списков.

stpk
источник
{"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)

NiksVij
источник
Не могли бы вы исправить отступы в вашем коде?
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
Чие-И Чен
источник