Como comparar dois objetos JSON com os mesmos elementos em uma ordem diferente igual?

101

Como posso testar se dois objetos JSON são iguais em python, desconsiderando a ordem das listas?

Por exemplo ...

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 bdeve comparar iguais, embora a ordem das "errors"listas seja diferente.

Petter Friberg
fonte
2
Duplicado de stackoverflow.com/questions/11141644/…
user2085282
1
Por que não apenas decodificá-los e compará-los? Ou você quer dizer que a ordem do "Array" ou dos listelementos também não importa?
mgilson
@ user2085282 Essa pergunta tem um problema diferente.
user193661
2
Por favor, perdoe minha ingenuidade, mas por quê? Os elementos da lista têm uma ordem específica por um motivo.
ATOzTOA de
1
Conforme observado nesta resposta, uma matriz JSON é classificada para que esses objetos contendo matrizes com diferentes ordens de classificação não sejam iguais no sentido estrito. stackoverflow.com/a/7214312/18891
Eric Ness

Respostas:

143

Se você deseja dois objetos com os mesmos elementos, mas em uma ordem diferente para comparar iguais, a coisa óbvia a fazer é comparar as cópias ordenadas deles - por exemplo, para os dicionários representados por suas strings 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

... mas isso não funciona, porque em cada caso, o "errors"item do dict de nível superior é uma lista com os mesmos elementos em uma ordem diferente e sorted()não tenta classificar nada, exceto o nível "superior" de um iterável.

Para corrigir isso, podemos definir uma orderedfunção que classificará recursivamente todas as listas que encontrar (e converterá dicionários em listas de (key, value)pares para que sejam ordenáveis):

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 aplicarmos esta função a ae b, os resultados serão iguais:

>>> ordered(a) == ordered(b)
True
Zero Piraeus
fonte
1
muito obrigado Zero Piraeus. é exatamente a solução geral de que preciso. mas o único problema é que o código funciona apenas para python 2.x, não para python3. Recebo o seguinte erro: TypeError: tipos não ordenáveis: dict () <dict () De qualquer forma, a solução agora está clara. Vou tentar fazer funcionar para python3. Muito obrigado
1
@HoussamHsm Eu pretendia consertar isso para funcionar com o Python 3.x quando você mencionou pela primeira vez o problema dos dictos não-encomendáveis, mas de alguma forma ele escapou de mim. Agora funciona em 2.xe 3.x :-)
Zero Piraeus
quando há uma lista como ['astr', {'adict': 'something'}], eu recebo TypeErrorao tentar classificá-los.
Zhenxiao Hao
1
@ Blairg23 você entendeu mal a questão, que é sobre comparar objetos JSON como iguais quando contêm listas cujos elementos são iguais, mas em uma ordem diferente, não sobre qualquer suposta ordem de dicionários.
Zero Piraeus
1
@ Blairg23 Concordo que a questão poderia ser escrita de forma mais clara (embora se você olhar para o histórico de edições , seja melhor do que começou). Re: dicionários e ordem - sim, eu sei ;-)
Zero Piraeus
45

Outra forma poderia ser usar a json.dumps(X, sort_keys=True)opção:

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

Isso funciona para listas e dicionários aninhados.

stpk
fonte
{"error":"a"}, {"error":"b"}vs {"error":"b"}, {"error":"a"} não será capaz de classificar o último caso no primeiro caso
ChromeHearts,
@ Blairg23 mas o que você faria se tivesse listas aninhadas no dicionário? Você não pode simplesmente comparar o dict de nível superior e encerrar o dia, não é disso que se trata esta questão.
stpk
4
Isso não funciona se você tiver listas dentro. por exemplo json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Danil
7
@Danil e provavelmente não deveria. As listas são uma estrutura ordenada e se diferirem apenas na ordem, devemos considerá-las diferentes. Talvez para seu caso de uso a ordem não importe, mas não devemos presumir isso.
stpk
porque as listas são ordenadas por índice, elas não serão reclassificadas. [0, 1] não deve ser igual a [1, 0] na maioria das situações. Portanto, esta é uma boa solução para o caso normal, mas não para a questão acima. ainda +1
Harrison
18

Decodifique-os e compare-os como comentário do mgilson.

A ordem não importa para o dicionário, desde que as chaves e os valores correspondam. (O dicionário não tem ordem em Python)

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

Mas a ordem é importante na lista; a classificação resolverá o problema das listas.

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

O exemplo acima funcionará para o JSON em questão. Para uma solução geral, consulte a resposta de Zero Piraeus.

falsetru
fonte
2

Para os dois dictes a seguir 'dictWithListsInValue' e 'reorderedDictWithReorderedListsInValue', que são simplesmente versões reordenadas um do outro

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

me deu resultado errado, ou seja, falso.

Então, criei meu próprio ObjectComparator cutstom assim:

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

que me deu a saída esperada correta!

A lógica é muito simples:

Se os objetos forem do tipo 'lista', então compare cada item da primeira lista com os itens da segunda lista até que seja encontrado, e se o item não for encontrado depois de passar pela segunda lista, então 'encontrado' seria = falso. o valor 'encontrado' é retornado

Caso contrário, se os objetos a serem comparados forem do tipo 'dict', compare os valores presentes para todas as respectivas chaves em ambos os objetos. (A comparação recursiva é realizada)

Ou simplesmente chame obj1 == obj2. Por padrão, funciona bem para o objeto de strings e números e para aqueles eq () é definido apropriadamente.

(Observe que o algoritmo pode ser melhorado removendo os itens encontrados no objeto2, de modo que o próximo item do objeto1 não se compare com os itens já encontrados no objeto2)

NiksVij
fonte
Você pode corrigir o recuo do seu código?
colidira
@colidyre o recuo está bom agora?
NiksVij
Não, ainda há problemas. Após o cabeçote de função, o bloco também deve ser indentado.
colidira
Sim. Eu reeditei mais uma vez. Copiei e colei no IDE e agora está funcionando.
NiksVij
1

Você pode escrever sua própria função igual:

  • dictos são iguais se: 1) todas as chaves são iguais, 2) todos os valores são iguais
  • as listas são iguais se: todos os itens são iguais e estão na mesma ordem
  • primitivas são iguais se a == b

Por estar lidando com json, você terá tipos de python padrão: dict , list, etc., de modo que você pode fazer difícil verificação de tipo if type(obj) == 'dict':, etc.

Exemplo aproximado (não testado):

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

Para outras pessoas que desejam depurar os dois objetos JSON (geralmente, há uma referência e um destino ), aqui está uma solução que você pode usar. Irá listar o " caminho " dos diferentes / incompatíveis do destino até a referência.

level opção é usada para selecionar o quão profundo você gostaria de olhar.

show_variables opção pode ser ativada para mostrar a variável relevante.

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
fonte