Come confrontare due oggetti JSON con gli stessi elementi in un ordine diverso uguale?


103

Come posso verificare se due oggetti JSON sono uguali in Python, ignorando l'ordine degli elenchi?

Per esempio ...

Documento JSON a :

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

Documento JSON b :

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

ae bdovrebbe essere uguale, anche se l'ordine degli "errors"elenchi è diverso.



1
Perché non decodificarli e confrontarli? O vuoi dire che anche l'ordine degli "Array" o degli listelementi non ha importanza?
mgilson

@ user2085282 Questa domanda presenta un problema diverso.
user193661

2
Per favore perdona la mia ingenuità, ma perché? Gli elementi dell'elenco hanno un ordine specifico per un motivo.
ATOzTOA

1
Come notato in questa risposta, un array JSON viene ordinato in modo che questi oggetti contenenti array con diversi ordinamenti non siano uguali in senso stretto. stackoverflow.com/a/7214312/18891
Eric Ness

Risposte:


144

Se vuoi che due oggetti con gli stessi elementi ma in un ordine diverso siano uguali, la cosa ovvia da fare è confrontare le copie ordinate di essi, ad esempio per i dizionari rappresentati dalle stringhe JSON ae 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

... ma non funziona, perché in ogni caso, l' "errors"elemento del dict di primo livello è un elenco con gli stessi elementi in un ordine diverso e sorted()non cerca di ordinare nulla tranne il livello "principale" di un iterabile.

Per risolvere questo problema, possiamo definire una orderedfunzione che ordinerà in modo ricorsivo tutti gli elenchi che trova (e convertirà i dizionari in elenchi di (key, value)coppie in modo che siano ordinabili):

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

Se applichiamo questa funzione a ae b, i risultati sono uguali:

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

1
grazie mille Zero Pireo. è esattamente la soluzione generale di cui ho bisogno. ma l'unico problema è che il codice funziona solo per python 2.x non per python3. Ottengo il seguente errore: TypeError: unorderable types: dict () <dict () Comunque la soluzione è ora chiara. Proverò a farlo funzionare per python3. Grazie mille

1
@HoussamHsm Volevo risolvere questo problema per funzionare con Python 3.x quando hai menzionato per la prima volta il problema dei dict non ordinabili, ma in qualche modo mi è sfuggito. Ora funziona sia in 2.x che in 3.x :-)
Zero Piraeus

quando c'è un elenco come ['astr', {'adict': 'something'}], ho ottenuto TypeErrorquando ho provato a ordinarli.
Zhenxiao Hao

1
@ Blairg23 hai frainteso la domanda, che riguarda il confronto di oggetti JSON come uguali quando contengono elenchi i cui elementi sono gli stessi, ma in un ordine diverso, non su un presunto ordine di dizionari.
Zero Pireo

1
@ Blairg23 Concordo sul fatto che la domanda potrebbe essere scritta più chiaramente (anche se se guardi la cronologia delle modifiche , è meglio di come è iniziata). Ri: dizionari e ordine - si , lo so ;-)
Zero Pireo

45

Un altro modo potrebbe essere usare l' json.dumps(X, sort_keys=True)opzione:

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

Funziona con dizionari ed elenchi annidati.


{"error":"a"}, {"error":"b"}vs {"error":"b"}, {"error":"a"} non sarà in grado di ordinare quest'ultimo caso nel primo caso
ChromeHearts

@ Blairg23 ma cosa faresti se avessi elenchi annidati nel dict? Non puoi semplicemente confrontare il dict di primo livello e chiamarlo un giorno, non è questo l'argomento di questa domanda.
stpk

4
Questo non funziona se hai degli elenchi all'interno. ad esempio json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Danil

7
@ Danil e probabilmente non dovrebbe. Le liste sono una struttura ordinata e se differiscono solo per ordine, dovremmo considerarle diverse. Forse per il tuo caso l'ordine non ha importanza, ma non dovremmo presumerlo.
stpk

poiché gli elenchi sono ordinati per indice, non verranno ricorsi. [0, 1] non dovrebbe essere uguale a [1, 0] nella maggior parte delle situazioni. Quindi questa è una buona soluzione per il caso normale, ma non per la domanda sopra. ancora +1
Harrison

18

Decodificali e confrontali come commento mgilson.

L'ordine non ha importanza per il dizionario fintanto che le chiavi e i valori corrispondono. (Il dizionario non ha ordine in Python)

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

Ma l'ordine è importante nell'elenco; l'ordinamento risolverà il problema per gli elenchi.

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

L'esempio sopra funzionerà per il JSON nella domanda. Per la soluzione generale, vedere la risposta di Zero Pireo.


2

Per i seguenti due dict 'dictWithListsInValue' e 'reorderedDictWithReorderedListsInValue' che sono semplicemente versioni riordinate l'una dell'altra

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

mi ha dato un risultato sbagliato cioè falso.

Quindi ho creato il mio Cutstom ObjectComparator in questo modo:

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

che mi ha dato l'output previsto corretto!

La logica è piuttosto semplice:

Se gli oggetti sono di tipo "elenco", confronta ogni elemento del primo elenco con gli elementi del secondo elenco finché non vengono trovati, e se l'elemento non viene trovato dopo aver esaminato il secondo elenco, "trovato" sarebbe = falso. viene restituito il valore "trovato"

Altrimenti se gli oggetti da confrontare sono di tipo 'dict' allora confronta i valori presenti per tutte le rispettive chiavi in ​​entrambi gli oggetti. (Viene eseguito il confronto ricorsivo)

Altrimenti chiama semplicemente obj1 == obj2. Di default funziona bene per l'oggetto di stringhe e numeri e per quelli eq () è definito in modo appropriato.

(Si noti che l'algoritmo può essere ulteriormente migliorato rimuovendo gli elementi trovati in object2, in modo che l'elemento successivo di object1 non si paragonerebbe con gli elementi già trovati in object2)


Puoi correggere il rientro del tuo codice?
colidyre

@colidyre va bene l'indentazione ora?
NiksVij

No, ci sono ancora problemi. Dopo l'intestazione della funzione, anche il blocco deve essere rientrato.
colidyre

Sì. Ho rieditato ancora una volta. L'ho copiato e incollato nell'IDE e ora funziona.
NiksVij

1

Puoi scrivere la tua funzione uguale:

  • dict sono uguali se: 1) tutte le chiavi sono uguali, 2) tutti i valori sono uguali
  • le liste sono uguali se: tutti gli elementi sono uguali e nello stesso ordine
  • le primitive sono uguali se a == b

Perché hai a che fare con JSON, avrete tipi standard di Python: dict, list, ecc, in modo da poter fare il controllo di tipo rigido if type(obj) == 'dict':, ecc

Esempio approssimativo (non testato):

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

Per gli altri che vorrebbero eseguire il debug dei due oggetti JSON (di solito, c'è un riferimento e un obiettivo ), ecco una soluzione che potresti usare. Elencherà il " percorso " di quelli diversi / non corrispondenti dalla destinazione al riferimento.

level viene utilizzata per selezionare la profondità in cui desideri esaminare.

show_variables l'opzione può essere attivata per mostrare la variabile rilevante.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.