Jak porównać dwa obiekty JSON z tymi samymi elementami w różnej kolejności są równe?

102

Jak mogę sprawdzić, czy dwa obiekty JSON są równe w Pythonie, pomijając kolejność list?

Na przykład ...

Dokument JSON a :

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

Dokument JSON b :

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

ai bpowinny być równe, mimo że kolejność "errors"list jest inna.

Petter Friberg
źródło
1
Dlaczego po prostu ich nie odkodować i nie porównać? A może masz na myśli to, że kolejność „tablicy” lub listelementów też nie ma znaczenia?
mgilson
@ user2085282 To pytanie dotyczy innego problemu.
user193661
2
Proszę wybacz moją naiwność, ale dlaczego? Elementy listy mają określoną kolejność z jakiegoś powodu.
ATOzTOA
1
Jak wspomniano w tej odpowiedzi, tablica JSON jest sortowana, więc te obiekty zawierające tablice z różnymi porządkami sortowania nie byłyby równe w ścisłym sensie. stackoverflow.com/a/7214312/18891
Eric Ness

Odpowiedzi:

143

Jeśli chcesz, aby dwa obiekty z tymi samymi elementami, ale w innej kolejności były porównywane równo, to oczywistą rzeczą do zrobienia jest porównanie ich posortowanych kopii - na przykład dla słowników reprezentowanych przez twoje ciągi JSON ai 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

... ale to nie działa, ponieważ w każdym przypadku "errors"element dyktowania najwyższego poziomu jest listą z tymi samymi elementami w innej kolejności i sorted()nie próbuje sortować niczego poza „najwyższym” poziomem iterowalny.

Aby to naprawić, możemy zdefiniować orderedfunkcję, która będzie rekurencyjnie sortować wszystkie znalezione listy (i konwertować słowniki na listy (key, value)par, aby można je było uporządkować):

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

Jeśli zastosujemy tę funkcję do ai b, wyniki będą równe:

>>> ordered(a) == ordered(b)
True
Zero Piraeus
źródło
1
dziękuję bardzo Zero Piraeus. to jest dokładnie taka ogólna rzecz, której potrzebuję. ale jedynym problemem jest to, że kod działa tylko dla Pythona 2.x, a nie dla Pythona3. Pojawia się następujący błąd: TypeError: unorderable types: dict () <dict () W każdym razie rozwiązanie jest teraz jasne. Postaram się, aby działało na python3.
1
@HoussamHsm Chciałem to naprawić, aby działało z Pythonem 3.x, kiedy po raz pierwszy wspomniałeś o problemie unorderable dicts, ale jakoś to mi wymknęło się. Teraz działa zarówno w 2.x, jak i 3.x :-)
Zero Piraeus
kiedy jest taka lista ['astr', {'adict': 'something'}], jaką otrzymałem TypeError, próbując je posortować.
Zhenxiao Hao
1
@ Blairg23 źle zrozumiałeś pytanie, które dotyczy porównywania obiektów JSON jako równych, gdy zawierają one listy, których elementy są takie same, ale w innej kolejności, a nie o jakiejkolwiek przypuszczalnej kolejności słowników.
Zero Piraeus
1
@ Blairg23 Zgadzam się, że pytanie mogłoby być napisane jaśniej (chociaż jeśli spojrzysz na historię edycji , to lepiej, niż się zaczęło). Re: słowniki i porządek - tak, wiem ;-)
Zero Piraeus
45

Innym sposobem może być użycie json.dumps(X, sort_keys=True)opcji:

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

Działa to w przypadku zagnieżdżonych słowników i list.

stpk
źródło
{"error":"a"}, {"error":"b"}vs {"error":"b"}, {"error":"a"} nie będzie w stanie
podzielić
@ Blairg23, ale co byś zrobił, gdybyś miał listy zagnieżdżone w dyktecie? Nie możesz po prostu porównać dyktowania najwyższego poziomu i nazwać go dniem, nie o to chodzi w tym pytaniu.
stpk
4
To nie działa, jeśli masz w środku listy. np. json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Danil,
7
@Danil i prawdopodobnie nie powinno. Listy są strukturą uporządkowaną i jeśli różnią się tylko kolejnością, należy je traktować inaczej. Może dla twojego przypadku kolejność nie ma znaczenia, ale nie powinniśmy tego zakładać.
stpk
ponieważ listy są uporządkowane według indeksu, nie będą używane. [0, 1] nie powinno być równe [1, 0] w większości sytuacji. Jest to więc dobre rozwiązanie w normalnym przypadku, ale nie w przypadku powyższego pytania. nadal +1
Harrison
18

Odszyfruj je i porównaj jako komentarz mgilson.

Porządek nie ma znaczenia dla słownika, jeśli klucze i wartości są zgodne. (Słownik nie ma kolejności w Pythonie)

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

Ale porządek jest ważny na liście; sortowanie rozwiąże problem dotyczący list.

>>> [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

Powyższy przykład zadziała dla JSON w pytaniu. Ogólne rozwiązanie można znaleźć w odpowiedzi Zero Piraeus.

falsetru
źródło
2

Dla następujących dwóch dykt „dictWithListsInValue” i „reorderedDictWithReorderedListsInValue”, które są po prostu zmienionymi wersjami siebie

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

dał mi zły wynik, tj. fałszywy.

Stworzyłem więc własny cutstom ObjectComparator w następujący sposób:

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

co dało mi prawidłowe oczekiwane wyjście!

Logika jest dość prosta:

Jeśli obiekty są typu „lista”, porównaj każdą pozycję z pierwszej listy z pozycjami z drugiej listy, aż do znalezienia, a jeśli pozycja nie zostanie znaleziona po przejrzeniu drugiej listy, to „znaleziony” będzie miał wartość = fałsz. Zwracana jest wartość „znaleziona”

W przeciwnym razie, jeśli porównywane obiekty są typu „dict”, porównaj wartości obecne dla wszystkich odpowiednich kluczy w obu obiektach. (Wykonywane jest porównanie rekurencyjne)

W przeciwnym razie po prostu wywołaj obj1 == obj2. Domyślnie działa dobrze dla obiektu łańcuchów i liczb oraz dla tych eq () jest odpowiednio zdefiniowane.

(Zwróć uwagę, że algorytm można dalej ulepszyć, usuwając elementy znalezione w object2, aby następny element object1 nie porównywał się z elementami już znalezionymi w object2)

NiksVij
źródło
Czy możesz poprawić wcięcie swojego kodu?
colidyre
@colidyre czy wcięcia są teraz w porządku?
NiksVij,
Nie, nadal problemy. Po nagłówku funkcji należy również wciąć blok.
colidyre
Tak. Ponownie zredagowałem. Skopiowałem, wkleiłem go do IDE i teraz działa.
NiksVij,
1

Możesz napisać własną funkcję równości:

  • dykty są równe, jeśli: 1) wszystkie klucze są równe, 2) wszystkie wartości są równe
  • listy są równe, jeśli: wszystkie pozycje są równe i w tej samej kolejności
  • prymitywy są równe, jeśli a == b

Ponieważ mamy do czynienia z json, musisz standardowe typy Pythona: dict, list, itd., Więc można zrobić twardy sprawdzanie typuif type(obj) == 'dict': itd

Szorstki przykład (nie testowano):

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
Gordon Bean
źródło
0

Dla innych, którzy chcieliby debugować dwa obiekty JSON (zwykle jest to odniesienie i cel ), oto rozwiązanie, którego możesz użyć. Wyświetli listę „ ścieżka ” różnych / niedopasowanych od celu do odniesienia.

level Opcja służy do wyboru, jak głęboko chcesz zajrzeć.

show_variables można włączyć, aby wyświetlić odpowiednią zmienną.

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
Chieh-I Chen
źródło