Comment comparer deux objets JSON avec les mêmes éléments dans un ordre différent égal?

101

Comment puis-je tester si deux objets JSON sont égaux en python, sans tenir compte de l'ordre des listes?

Par exemple ...

Document JSON a :

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

Document JSON b :

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

aet bdevrait comparer égaux, même si l'ordre des "errors"listes est différent.

Petter Friberg
la source
2
Duplicate of stackoverflow.com/questions/11141644/…
user2085282
1
Pourquoi ne pas simplement les décoder et comparer? Ou voulez-vous dire que l'ordre du "tableau" ou des listéléments n'a pas d'importance non plus?
mgilson
@ user2085282 Cette question pose un problème différent.
user193661
2
Veuillez pardonner ma naïveté, mais pourquoi? Les éléments de liste ont un ordre spécifique pour une raison.
ATOzTOA
1
Comme indiqué dans cette réponse, un tableau JSON est trié afin que ces objets contenant des tableaux avec des ordres de tri différents ne soient pas égaux au sens strict. stackoverflow.com/a/7214312/18891
Eric Ness

Réponses:

143

Si vous voulez que deux objets avec les mêmes éléments mais dans un ordre différent se comparent égaux, alors la chose évidente à faire est de comparer des copies triées d'entre eux - par exemple, pour les dictionnaires représentés par vos chaînes JSON aet 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

... mais cela ne fonctionne pas, car dans chaque cas, l' "errors"élément du dict de niveau supérieur est une liste avec les mêmes éléments dans un ordre différent, et sorted()n'essaye pas de trier autre chose que le niveau "supérieur" de un itérable.

Pour résoudre ce problème, nous pouvons définir une orderedfonction qui triera de manière récursive toutes les listes trouvées (et convertira les dictionnaires en listes de (key, value)paires afin qu'ils puissent être triés):

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

Si nous appliquons cette fonction à aet b, les résultats se comparent égaux:

>>> ordered(a) == ordered(b)
True
Zéro Pirée
la source
1
merci beaucoup Zéro Pirée. c'est exactement la solution générale dont j'ai besoin. mais le seul problème est que le code ne fonctionne que pour python 2.x pas pour python3. J'obtiens l'erreur suivante: TypeError: types non triables: dict () <dict () Quoi qu'il en soit, la solution est maintenant claire. Je vais essayer de le faire fonctionner pour python3. Merci beaucoup
1
@HoussamHsm J'avais l'intention de résoudre ce problème pour fonctionner avec Python 3.x lorsque vous avez mentionné pour la première fois le problème des dicts non commandables, mais d'une manière ou d'une autre, cela m'a échappé. Il fonctionne maintenant à la fois dans 2.x et 3.x :-)
Zero Piraeus
quand il y a une liste comme ['astr', {'adict': 'something'}], j'ai TypeErrorquand j'essaye de les trier.
Zhenxiao Hao le
1
@ Blairg23 vous avez mal compris la question, qui consiste à comparer des objets JSON comme égaux lorsqu'ils contiennent des listes dont les éléments sont les mêmes, mais dans un ordre différent, pas sur un ordre supposé de dictionnaires.
Zero Piraeus
1
@ Blairg23 Je suis d'accord pour dire que la question pourrait être plus clairement écrite (bien que si vous regardez l' historique d'édition , c'est mieux qu'elle n'a commencé). Re: dictionnaires et commande - oui, je sais ;-)
Zero Piraeus
45

Une autre façon pourrait être d'utiliser l' json.dumps(X, sort_keys=True)option:

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

Cela fonctionne pour les dictionnaires et les listes imbriqués.

stpk
la source
{"error":"a"}, {"error":"b"}vs {"error":"b"}, {"error":"a"} il ne sera pas en mesure de trier ce dernier cas dans le premier cas
ChromeHearts
@ Blairg23 mais que feriez-vous si vous aviez des listes imbriquées dans le dict? Vous ne pouvez pas simplement comparer le dict de niveau supérieur et l'appeler un jour, ce n'est pas le sujet de cette question.
stpk
4
Cela ne fonctionne pas si vous avez des listes à l'intérieur. par exemple json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Danil
7
@Danil et probablement pas. Les listes sont une structure ordonnée et si elles ne diffèrent que par ordre, nous devons les considérer comme différentes. Peut-être que pour votre cas d'utilisation, l'ordre n'a pas d'importance, mais nous ne devrions pas le supposer.
stpk
parce que les listes sont classées par index, elles ne seront pas utilisées. [0, 1] ne devrait pas être égal à [1, 0] dans la plupart des situations. C'est donc une bonne solution pour le cas normal, mais pas pour la question ci-dessus. encore +1
Harrison
18

Décodez-les et comparez-les en tant que commentaire mgilson.

L'ordre n'a pas d'importance pour le dictionnaire tant que les clés et les valeurs correspondent. (Le dictionnaire n'a pas d'ordre en Python)

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

Mais l'ordre est important dans la liste; le tri résoudra le problème des listes.

>>> [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'exemple ci-dessus fonctionnera pour le JSON dans la question. Pour une solution générale, voir la réponse de Zero Piraeus.

falsetru
la source
2

Pour les deux dictionnaires suivants 'dictWithListsInValue' et 'reorderedDictWithReorderedListsInValue' qui sont simplement des versions réorganisées l'une de l'autre

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

m'a donné un mauvais résultat, c'est-à-dire faux.

J'ai donc créé mon propre ObjectComparator cutstom comme ceci:

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

ce qui m'a donné la sortie correcte attendue!

La logique est assez simple:

Si les objets sont de type «liste», comparez chaque élément de la première liste avec les éléments de la deuxième liste jusqu'à ce qu'ils soient trouvés, et si l'élément n'est pas trouvé après avoir parcouru la deuxième liste, alors «trouvé» serait = faux. La valeur 'found' est renvoyée

Sinon, si les objets à comparer sont de type «dict», comparez les valeurs présentes pour toutes les clés respectives dans les deux objets. (Une comparaison récursive est effectuée)

Sinon, appelez simplement obj1 == obj2. Par défaut, cela fonctionne bien pour l'objet de chaînes et de nombres et pour ceux-ci, eq () est défini de manière appropriée.

(Notez que l'algorithme peut encore être amélioré en supprimant les éléments trouvés dans object2, de sorte que l'élément suivant de object1 ne se compare pas avec les éléments déjà trouvés dans l'objet2)

NiksVij
la source
Pouvez-vous s'il vous plaît corriger l'indentation de votre code?
colidyre
@colidyre est-ce que l'indentation est correcte maintenant?
NiksVij
Non, il y a encore des problèmes. Après la tête de fonction, le bloc doit également être indenté.
colidyre
Oui. J'ai réédité une fois de plus. Je l'ai copié collé dans l'IDE, et cela fonctionne maintenant.
NiksVij
1

Vous pouvez écrire votre propre fonction égale:

  • les dicts sont égaux si: 1) toutes les clés sont égales, 2) toutes les valeurs sont égales
  • les listes sont égales si: tous les éléments sont égaux et dans le même ordre
  • les primitives sont égales si a == b

Parce que vous avez affaire à JSON, vous aurez les types de python standard: dict, list, etc., de sorte que vous pouvez faire la vérification de type dur if type(obj) == 'dict':, etc.

Exemple approximatif (non testé):

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
la source
0

Pour les autres qui souhaitent déboguer les deux objets JSON (généralement, il y a une référence et une cible ), voici une solution que vous pouvez utiliser. Il listera le " chemin " des différents / incompatibles de la cible à la référence.

level L'option est utilisée pour sélectionner la profondeur à laquelle vous souhaitez examiner.

show_variables L'option peut être activée pour afficher la variable appropriée.

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
la source