同じ要素を持つ2つのJSONオブジェクトを異なる順序で比較するにはどうすればよいですか?


99

リストの順序を無視して、2つのJSONオブジェクトがPythonで等しいかどうかをテストするにはどうすればよいですか?

例えば ​​...

JSONドキュメントa

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

JSONドキュメントb

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

aそして、bのオーダーにもかかわらず、同等比較してください"errors"リストは異なっています。



1
なぜそれらをデコードして比較しないのですか?それとも、「配列」またはlist要素の順序も関係ないということですか?
mgilson 2014

@ user2085282その質問には別の問題が起こっています。
user193661

2
私の世間知らずを許してください、しかしなぜですか?リスト要素には、理由のために特定の順序があります。
ATOzTOA 2017

1
この回答で述べたように、JSON配列は並べ替えられるため、異なる並べ替え順序の配列を含むこれらのオブジェクトは厳密な意味では等しくありません。stackoverflow.com/a/7214312/18891
エリックネス

回答:


141

あなたは同じ要素を持つが、同じ比較する異なる順序で2つのオブジェクトをしたい場合は、行うには明白なことは、それらのソートコピーを比較している-例えば、辞書のためのあなたのJSON文字列によって表されるab

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

...しかし、それは機能しません。なぜなら"errors"、トップレベルの辞書のアイテムは、同じ要素を異なる順序で含むリストでありsorted()、「トップ」レベル以外のものをソートしようとしないためです。反復可能。

これを修正するには、ordered見つかったリストを再帰的にソートする関数を定義します(そして、辞書を(key, value)ペアのリストに変換して、順序付けできるようにします)。

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

この関数をaandに適用するとb、結果は次のように比較されます。

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

1
ゼロピレウス、ありがとうございました。それはまさに私が必要とする一般的な解決策です。ただし、唯一の問題は、コードがpython 2.xでのみ機能し、python3では機能しないことです。次のエラーが表示されます。TypeError:順序付けできない型:dict()<dict()とにかく解決策が明確になりました。私はそれをpython3で動作するようにしようとします。

1
@HoussamHsm順序付けできないdictsの問題について最初に触れたとき、私はこれをPython 3.xで動作するように修正するつもりでしたが、どういうわけか私から離れました。2.xと3.xの両方で動作するようになりました:-)
Zero Piraeus

のようなリスト['astr', {'adict': 'something'}]があるTypeError場合、それらを並べ替えようとしたときに取得しました。
Zhenxiao Hao

1
@ Blairg23あなたは質問を誤解しました。これは、JSONオブジェクトが要素のリストが同じであるが異なる順序で含まれている場合に、JSONオブジェクトを等しいものとして比較することに関するものであり、想定される辞書の順序についてではありません
ゼロピレウス

1
@ Blairg23私は質問をより明確に書くことができることに同意します(編集履歴を見ると、最初よりも優れています)。日時:辞書や順序- はい、私は知っている ;-)
ゼロピレウス

44

別の方法は、json.dumps(X, sort_keys=True)オプションを使用することです:

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

これは、ネストされた辞書とリストで機能します。


{"error":"a"}, {"error":"b"}vs {"error":"b"}, {"error":"a"} 後者のケースを最初のケースに分類することはできません
ChromeHearts 2016年

@ Blairg23しかし、dictにリストがネストされている場合はどうしますか?トップレベルの辞書を比較して1日と呼ぶことはできません。これはこの質問についてではありません。
stpk

3
内部にリストがある場合、これは機能しません。例 json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Danil

6
@Danil、そしておそらくそれはすべきではない。リストは順序付けられた構造であり、順序だけが異なる場合は、異なるものと見なす必要があります。多分あなたのユースケースのために順序は重要ではありませんが、私たちはそれを仮定すべきではありません。
stpk

リストはインデックス順に並べられているため、並べ替えは行われません。ほとんどの場合、[0、1]は[1、0]と等しくないはずです。したがって、これは通常の場合には適切な解決策ですが、上記の質問には適していません。まだ1
ハリソン

18

それらをデコードし、mgilsonコメントとして比較します。

キーと値が一致する限り、辞書の順序は関係ありません。(辞書はPythonでは順序がありません)

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

しかし、リストでは順序が重要です。ソートはリストの問題を解決します。

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

上記の例は、質問のJSONで機能します。一般的な解決策については、ゼロピレウスの回答を参照してください。


2

次の2つのディクテーション 'dictWithListsInValue'および 'reorderedDictWithReorderedListsInValue'は、お互いの単純に並べ替えられたバージョンです。

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

間違った結果、つまりfalseを返しました。

そこで、次のように独自のカットオブジェクトObjectComparatorを作成しました。

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

これは私に正しい期待された出力を与えました!

ロジックはかなりシンプルです:

オブジェクトのタイプが「リスト」である場合、最初のリストの各アイテムを2番目のリストのアイテムと比較して、見つかった場合、2番目のリストを調べてもアイテムが見つからなかった場合、「見つかった」は= falseになります。'found'値が返されます

または、比較するオブジェクトのタイプが「dict」の場合は、両方のオブジェクトのそれぞれのキーすべてに存在する値を比較します。(再帰的な比較が行われます)

または、単にobj1 == obj2を呼び出します。デフォルトでは、文字列と数値のオブジェクトに対して正常に動作し、それらのeq()は適切に定義されています。

(object2で見つかったアイテムを削除することにより、アルゴリズムをさらに改善できることに注意してください。これにより、object1の次のアイテムは、object2ですでに見つかったアイテムと比較されません。)


コードのインデント修正できますか?
コリディア

@colidyreは今インデントで問題ありませんか?
NiksVij 2018

いいえ、まだ問題があります。関数ヘッドの後、ブロックもインデントする必要があります。
コリディア

はい。もう一度再編集しました。それをIDEにコピーして貼り付けたところ、現在は機能しています。
NiksVij 2018

1

独自のequals関数を書くことができます:

  • dictsが等しい場合:1)すべてのキーが等しい、2)すべての値が等しい
  • リストが等しい場合:すべてのアイテムが等しく、同じ順序
  • プリミティブが等しい場合 a == b

あなたはJSONを扱っているので、あなたは、標準のpythonの種類があるでしょう:dictlistなど、あなたがハード型チェックを行うことができるようにif type(obj) == 'dict':など、

大まかな例(テストされていません):

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

2つのJSONオブジェクト(通常、参照ターゲットがあります)をデバッグしたい人のために、以下のソリューションを使用できます。ターゲットから参照への異なる/不一致の「パス」がリストされます。

level オプションは、調べたい深さを選択するために使用されます。

show_variables オプションをオンにして、関連する変数を表示できます。

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.