Як порівняти два об’єкти JSON з однаковими елементами в різному порядку, рівному?


101

Як я можу перевірити, чи рівні два об'єкти 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

1
Як зазначено у цій відповіді, масив JSON сортується, щоб ці об'єкти, що містять масиви з різними порядками сортування, не були рівними у суворому сенсі. stackoverflow.com/a/7214312/18891
Ерік Несс,

Відповіді:


143

Якщо ви хочете, щоб два об'єкти з однаковими елементами, але в різному порядку, порівнювали однакові, очевидним є порівняння відсортованих їх копій - наприклад, для словників, представлених вашими рядками 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"елемент дикту верхнього рівня - це список із однаковими елементами в іншому порядку і 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
дякую тобі нульовий Пірей. мені потрібне саме загальне рішення. але єдина проблема полягає в тому, що код працює лише для python 2.x, а не для python3. Я отримую таку помилку: TypeError: unorderable types: dict () <dict () У будь-якому разі рішення тепер зрозуміле. Я спробую змусити це працювати для python3. Велике спасибі

1
@HoussamHsm Я хотів виправити це для роботи з Python 3.x, коли ви вперше згадали про нерозбірливу проблему з диктами, але якось вона мені пішла. Зараз він працює як в 2.x, так і в 3.x :-)
Нульовий Пірей

коли є список типу ['astr', {'adict': 'something'}], який я отримую TypeErrorпри спробі їх сортувати.
Zhenxiao Hao

1
@ Blairg23 ви неправильно зрозуміли питання, яке стосується порівняння об'єктів JSON як рівних, коли вони містять списки, елементи яких однакові, але в іншому порядку, а не про будь-який передбачуваний порядок словників.
Нульовий Пірей

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

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, але що б ви зробили, якщо у вас є списки, вкладені в дикт? Ви не можете просто порівняти дикт найвищого рівня і назвати його днем, це не те, про що йдеться в цьому питанні.
stpk

4
Це не працює, якщо у вас є списки всередині. напр. json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Даніл

7
@Danil і, мабуть, не повинен. Списки - це впорядкована структура, і якщо вони відрізняються лише в порядку, ми повинні вважати їх різними. Можливо, для вашої справи порядок не має значення, але ми не повинні це вважати.
stpk

оскільки списки впорядковані за індексом, вони не вдаватимуться. [0, 1] не має дорівнювати [1, 0] у більшості ситуацій. Отже, це хороше рішення для звичайного випадку, але не для питання вище. ще +1
Гаррісон,

18

Розшифруйте їх і порівняйте як коментар Мгільсона.

Порядок не має значення для словника, якщо клавіші та значення збігаються. (Словник не має порядку в 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 у питанні. Загальне рішення див. У відповіді Нульового Пірея.


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

дав мені неправильний результат, тобто помилковий.

Тож я створив свій власний 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. повертається значення "знайдено"

В іншому випадку, якщо об’єкти для порівняння мають тип „dict“, тоді порівняйте значення, наявні для всіх відповідних ключів в обох об’єктах. (Виконується рекурсивне порівняння)

В іншому випадку просто зателефонуйте obj1 == obj2. За замовчуванням він чудово працює для об'єкта рядків і чисел, а для цих eq () визначено належним чином.

(Зверніть увагу, що алгоритм можна додатково вдосконалити, видаливши елементи, знайдені в object2, щоб наступний елемент object1 не порівнював себе з елементами, вже знайденими в object2)


Чи можете ви, будь ласка, виправити відступ вашого коду?
colidyre

@colidyre зараз відступ чудовий?
NiksVij

Ні, проблеми все ще є. Після функції head, блок також повинен бути з відступом.
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.