Wie vergleiche ich zwei JSON-Objekte mit denselben Elementen in einer anderen Reihenfolge gleich?

99

Wie kann ich testen, ob zwei JSON-Objekte in Python gleich sind, ohne die Reihenfolge der Listen zu berücksichtigen?

Zum Beispiel ...

JSON-Dokument a :

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

JSON-Dokument b :

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

aund bsollte gleich vergleichen, obwohl die Reihenfolge der "errors"Listen unterschiedlich ist.

Petter Friberg
quelle
2
Duplikat von stackoverflow.com/questions/11141644/…
user2085282
1
Warum nicht einfach dekodieren und vergleichen? Oder meinst du, dass die Reihenfolge des "Arrays" oder der listElemente auch keine Rolle spielt?
mgilson
@ user2085282 Diese Frage hat ein anderes Problem.
user193661
2
Bitte vergib mir meine Naivität, aber warum? Listenelemente haben aus einem bestimmten Grund eine bestimmte Reihenfolge.
ATOzTOA
1
Wie in dieser Antwort erwähnt, wird ein JSON-Array so sortiert, dass diese Objekte, die Arrays mit unterschiedlichen Sortierreihenfolgen enthalten, im engeren Sinne nicht gleich sind. stackoverflow.com/a/7214312/18891
Eric Ness

Antworten:

141

Wenn Sie möchten, dass zwei Objekte mit denselben Elementen in einer anderen Reihenfolge gleich verglichen werden, müssen Sie natürlich sortierte Kopien davon vergleichen - beispielsweise für die Wörterbücher, die durch Ihre JSON-Zeichenfolgen dargestellt werden, aund 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

... aber das funktioniert nicht, denn in jedem Fall ist das "errors"Element des Diktats der obersten Ebene eine Liste mit denselben Elementen in einer anderen Reihenfolge und sorted()versucht nur, die "oberste" Ebene von zu sortieren eine iterable.

Um dies zu beheben, können wir eine orderedFunktion definieren , die alle gefundenen Listen rekursiv sortiert (und Wörterbücher in Listen von (key, value)Paaren konvertiert, damit sie geordnet werden können):

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

Wenn wir diese Funktion auf aund anwenden b, sind die Ergebnisse gleich:

>>> ordered(a) == ordered(b)
True
Null Piräus
quelle
1
Vielen Dank Zero Piraeus. Es ist genau die allgemeine Lösung, die ich brauche. Das einzige Problem ist jedoch, dass Code nur für Python 2.x funktioniert, nicht für Python3. Ich erhalte die folgende Fehlermeldung: TypeError: unordentliche Typen: dict () <dict () Wie auch immer, die Lösung ist jetzt klar. Ich werde versuchen, es für Python3 zum Laufen zu bringen. Vielen Dank
1
@HoussamHsm Ich wollte das Problem beheben, damit es mit Python 3.x funktioniert, als Sie das Problem mit ungeordneten Diktaten zum ersten Mal erwähnten, aber irgendwie ist es mir entgangen. Es funktioniert jetzt sowohl in 2.x als auch in 3.x :-)
Zero Piraeus
Wenn es eine Liste wie gibt ['astr', {'adict': 'something'}], habe ich TypeErrorbeim Versuch, sie zu sortieren.
Zhenxiao Hao
1
@ Blairg23 Sie haben die Frage falsch verstanden, bei der es darum geht, JSON-Objekte als gleich zu vergleichen, wenn sie Listen enthalten, deren Elemente gleich sind, aber in einer anderen Reihenfolge, nicht in einer vermeintlichen Reihenfolge von Wörterbüchern.
Zero Piraeus
1
@ Blairg23 Ich stimme zu, dass die Frage klarer geschrieben werden könnte (obwohl es besser ist, wenn Sie sich den Bearbeitungsverlauf ansehen , als er begonnen hat). Betreff: Wörterbücher und Reihenfolge - ja, ich weiß ;-)
Zero Piraeus
44

Eine andere Möglichkeit könnte darin bestehen, die json.dumps(X, sort_keys=True)Option zu verwenden :

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

Dies funktioniert für verschachtelte Wörterbücher und Listen.

stpk
quelle
{"error":"a"}, {"error":"b"}vs {"error":"b"}, {"error":"a"} es wird nicht in der Lage sein, den letzteren Fall in den ersten Fall zu sortieren
ChromeHearts
@ Blairg23 aber was würdest du tun, wenn du Listen hast, die im Diktat verschachtelt sind? Sie können das Diktat der obersten Ebene nicht einfach vergleichen und es einen Tag nennen, darum geht es in dieser Frage nicht.
stpk
3
Dies funktioniert nicht, wenn Sie Listen enthalten. zB json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Danil
6
@ Danil und wahrscheinlich sollte es nicht. Listen sind eine geordnete Struktur, und wenn sie sich nur in der Reihenfolge unterscheiden, sollten wir sie als unterschiedlich betrachten. Vielleicht spielt die Bestellung für Ihren Fall keine Rolle, aber das sollten wir nicht annehmen.
stpk
Da Listen nach Index geordnet sind, werden sie nicht neu sortiert. [0, 1] sollte in den meisten Situationen nicht gleich [1, 0] sein. Dies ist also eine gute Lösung für den Normalfall, aber nicht für die obige Frage. noch +1
Harrison
18

Dekodiere sie und vergleiche sie als mgilson-Kommentar.

Die Reihenfolge spielt für das Wörterbuch keine Rolle, solange die Schlüssel und Werte übereinstimmen. (Wörterbuch hat keine Reihenfolge in Python)

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

Aber die Reihenfolge ist wichtig in der Liste; Durch Sortieren wird das Problem für die Listen gelöst.

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

Das obige Beispiel funktioniert für den JSON in der Frage. Eine allgemeine Lösung finden Sie in der Antwort von Zero Piraeus.

falsetru
quelle
2

Für die folgenden zwei Diktate 'dictWithListsInValue' und 'reorderedDictWithReorderedListsInValue', die einfach neu geordnete Versionen voneinander sind

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

gab mir falsches Ergebnis dh falsch.

Also habe ich meinen eigenen cutstom ObjectComparator wie folgt erstellt:

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

das gab mir die richtige erwartete Ausgabe!

Logik ist ziemlich einfach:

Wenn die Objekte vom Typ 'Liste' sind, vergleichen Sie jedes Element der ersten Liste mit den Elementen der zweiten Liste, bis sie gefunden werden. Wenn das Element nach Durchlaufen der zweiten Liste nicht gefunden wird, ist 'gefunden' = falsch. Der Wert 'found' wird zurückgegeben

Andernfalls vergleichen Sie die Werte, die für alle jeweiligen Schlüssel in beiden Objekten vorhanden sind, wenn die zu vergleichenden Objekte vom Typ 'dict' sind. (Rekursiver Vergleich wird durchgeführt)

Andernfalls rufen Sie einfach obj1 == obj2 auf. Es funktioniert standardmäßig gut für das Objekt von Zeichenfolgen und Zahlen und für diese ist eq () entsprechend definiert.

(Beachten Sie, dass der Algorithmus weiter verbessert werden kann, indem die in Objekt2 gefundenen Elemente entfernt werden, sodass sich das nächste Element von Objekt1 nicht mit den bereits in Objekt2 gefundenen Elementen vergleicht.)

NiksVij
quelle
Können Sie bitte die Einrückung Ihres Codes korrigieren?
Colidyre
@colidyre ist Einrückung jetzt in Ordnung?
NiksVij
Nein, gibt es immer noch Probleme. Nach dem Funktionskopf muss auch der Block eingerückt werden.
Colidyre
Ja. Ich habe noch einmal überarbeitet. Ich habe es kopiert und in die IDE eingefügt, und es funktioniert jetzt.
NiksVij
1

Sie können Ihre eigene Gleichheitsfunktion schreiben:

  • Diktate sind gleich, wenn: 1) alle Schlüssel gleich sind, 2) alle Werte gleich sind
  • Listen sind gleich, wenn: alle Elemente gleich und in derselben Reihenfolge sind
  • Grundelemente sind gleich, wenn a == b

Weil Sie mit json zu tun hat , werden Sie Standard - Python - Typen haben: dict, listetc., so dass Sie hart Typprüfung tun können if type(obj) == 'dict':, usw.

Grobes Beispiel (nicht getestet):

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
quelle
0

Für andere, die die beiden JSON-Objekte debuggen möchten (normalerweise gibt es eine Referenz und ein Ziel ), ist hier eine Lösung, die Sie verwenden können. Es wird der " Pfad " verschiedener / nicht übereinstimmender vom Ziel zur Referenz aufgelistet.

level Mit dieser Option können Sie auswählen, in welche Tiefe Sie schauen möchten.

show_variables Option kann aktiviert werden, um die relevante Variable anzuzeigen.

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
quelle