Bagaimana cara membandingkan dua objek JSON dengan elemen yang sama dalam urutan berbeda yang sama?

103

Bagaimana saya dapat menguji apakah dua objek JSON sama dengan python, mengabaikan urutan daftar?

Sebagai contoh ...

JSON dokumen a :

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

Dokumen JSON b :

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

adan bharus membandingkan sama, meskipun urutan "errors"daftarnya berbeda.

Petter Friberg
sumber
2
Duplikat stackoverflow.com/questions/11141644/…
pengguna2085282
1
Mengapa tidak memecahkan kode dan membandingkannya? Atau maksud Anda urutan "Array" atau listelemen tidak penting?
mgilson
@ user2085282 Pertanyaan tersebut memiliki masalah berbeda yang sedang terjadi.
pengguna193661
2
Mohon maafkan kenaifan saya, tapi mengapa? Elemen daftar memiliki urutan tertentu karena suatu alasan.
ATOzTOA
1
Seperti disebutkan dalam jawaban ini, larik JSON diurutkan sehingga objek yang berisi larik dengan urutan pengurutan yang berbeda tidak akan sama dalam arti yang sebenarnya. stackoverflow.com/a/7214312/18891
Eric Ness

Jawaban:

144

Jika Anda menginginkan dua objek dengan elemen yang sama tetapi dalam urutan berbeda untuk membandingkan sama, maka hal yang harus dilakukan adalah membandingkan salinan yang diurutkan dari mereka - misalnya, untuk kamus yang diwakili oleh string JSON Anda adan 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

... tetapi itu tidak berhasil, karena dalam setiap kasus, "errors"item dari dict level teratas adalah daftar dengan elemen yang sama dalam urutan berbeda, dan sorted()tidak mencoba untuk mengurutkan apa pun kecuali level "teratas" dari sebuah iterable.

Untuk memperbaikinya, kita dapat mendefinisikan sebuah orderedfungsi yang secara rekursif akan mengurutkan daftar apa pun yang ditemukannya (dan mengonversi kamus menjadi daftar (key, value)pasangan sehingga dapat diurutkan):

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

Jika kita menerapkan fungsi ini ke adan b, hasilnya sebanding:

>>> ordered(a) == ordered(b)
True
Nol Piraeus
sumber
1
terima kasih banyak Zero Piraeus. itulah solusi umum yang saya butuhkan. tetapi satu-satunya masalah adalah bahwa kode hanya berfungsi untuk python 2.x bukan untuk python3. Saya mendapatkan error berikut: TypeError: unorderable types: dict () <dict () Solusinya sekarang sudah jelas. Saya akan mencoba membuatnya berfungsi untuk python3. Terima kasih banyak
1
@HoussamHsm Saya bermaksud untuk memperbaiki ini untuk bekerja dengan Python 3.x ketika Anda pertama kali menyebutkan masalah dicts yang tidak dapat dipesan, tetapi entah bagaimana itu menjauh dari saya. Sekarang berfungsi di 2.x dan 3.x :-)
Zero Piraeus
ketika ada daftar seperti ['astr', {'adict': 'something'}], saya dapatkan TypeErrorsaat mencoba menyortirnya.
Zhenxiao Hao
1
@ Blairg23 Anda telah salah memahami pertanyaan, yaitu tentang membandingkan objek JSON sebagai sama ketika mereka berisi daftar yang elemennya sama, tetapi dalam urutan yang berbeda, bukan tentang urutan kamus yang seharusnya.
Nol Piraeus
1
@ Blairg23 Saya setuju bahwa pertanyaannya bisa ditulis dengan lebih jelas (walaupun jika Anda melihat riwayat edit , itu lebih baik daripada awalnya). Re: kamus dan urutan - ya, saya tahu ;-)
Nol Piraeus
45

Cara lain bisa menggunakan json.dumps(X, sort_keys=True)opsi:

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

Ini berfungsi untuk kamus dan daftar bertingkat.

stpk
sumber
{"error":"a"}, {"error":"b"}vs {"error":"b"}, {"error":"a"} tidak akan dapat mengurutkan kasus terakhir menjadi kasus pertama
ChromeHearts
@ Blairg23 tapi apa yang akan Anda lakukan jika Anda memiliki daftar bersarang di dict? Anda tidak bisa hanya membandingkan dikt tingkat atas dan menyebutnya sehari, ini bukanlah tentang pertanyaan ini.
stpk
4
Ini tidak berfungsi jika Anda memiliki daftar di dalamnya. misalnya json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Danil
7
@Danil dan mungkin seharusnya tidak. Daftar adalah struktur yang teratur dan jika hanya berbeda dalam urutannya, kita harus menganggapnya berbeda. Mungkin untuk kasus penggunaan Anda, urutannya tidak penting, tetapi kita tidak boleh berasumsi demikian.
stpk
karena daftar diurutkan berdasarkan indeks, mereka tidak akan digunakan. [0, 1] tidak boleh sama dengan [1, 0] di kebanyakan situasi. Jadi ini adalah solusi yang baik untuk kasus normal, tetapi tidak untuk pertanyaan di atas. masih +1
Harrison
18

Pecahkan kodenya dan bandingkan sebagai komentar mgilson.

Urutan tidak menjadi masalah bagi kamus selama kunci, dan nilainya cocok. (Kamus tidak memiliki urutan dengan Python)

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

Tetapi urutan penting dalam daftar; penyortiran akan menyelesaikan masalah untuk daftar.

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

Contoh di atas akan berfungsi untuk JSON dalam pertanyaan. Untuk solusi umum, lihat jawaban Zero Piraeus.

falsetru.dll
sumber
2

Untuk dua dicts berikut 'dictWithListsInValue' dan 'reorderedDictWithReorderedListsInValue' yang merupakan versi yang disusun ulang satu sama lain

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

memberi saya hasil yang salah yaitu salah.

Jadi saya membuat ObjectComparator cutstom saya sendiri seperti ini:

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

yang memberi saya hasil yang diharapkan benar!

Logikanya cukup sederhana:

Jika objek bertipe 'list' maka bandingkan setiap item dari list pertama dengan item dari list kedua hingga ditemukan, dan jika item tersebut tidak ditemukan setelah melalui list kedua, maka 'found' akan menjadi = false. nilai 'ditemukan' dikembalikan

Lain jika objek yang akan dibandingkan adalah tipe 'dict' kemudian bandingkan nilai-nilai yang ada untuk semua kunci masing-masing di kedua objek. (Perbandingan rekursif dilakukan)

Lain cukup panggil obj1 == obj2. Ini secara default berfungsi dengan baik untuk objek string dan angka dan untuk eq () itu didefinisikan dengan tepat.

(Perhatikan bahwa algoritme selanjutnya dapat ditingkatkan dengan menghapus item yang ditemukan di objek2, sehingga item objek1 berikutnya tidak akan membandingkan dirinya sendiri dengan item yang sudah ditemukan di objek2)

NiksVij
sumber
Bisakah Anda memperbaiki indentasi kode Anda?
colidyre
@ Solidyre lekukan baik-baik saja sekarang?
NiksVij
Tidak, masih ada masalah. Setelah function head, blok juga harus diindentasi.
colidyre
Iya. Saya mengedit ulang sekali lagi. Saya menyalin dan menempelkannya ke IDE, dan itu berfungsi sekarang.
NiksVij
1

Anda dapat menulis fungsi sama dengan Anda sendiri:

  • dicts sama jika: 1) semua kunci sama, 2) semua nilai sama
  • daftar sama jika: semua item sama dan dalam urutan yang sama
  • primitif sama jika a == b

Karena Anda sedang berhadapan dengan json, Anda akan memiliki jenis python standar: dict, list, dll, sehingga Anda dapat melakukan pengecekan jenis hard if type(obj) == 'dict':, dll

Contoh kasar (tidak diuji):

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

Bagi orang lain yang ingin men-debug dua objek JSON (biasanya, ada referensi dan target ), berikut adalah solusi yang dapat Anda gunakan. Ini akan mencantumkan " jalur " yang berbeda / tidak cocok dari target ke referensi.

level opsi digunakan untuk memilih seberapa dalam Anda ingin melihat.

show_variables opsi dapat diaktifkan untuk menunjukkan variabel yang relevan.

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
sumber