จะเปรียบเทียบออบเจ็กต์ JSON สองชิ้นที่มีองค์ประกอบเดียวกันในลำดับที่ต่างกันได้อย่างไร


104

ฉันจะทดสอบได้อย่างไรว่าออบเจ็กต์ 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
ทำไมไม่ถอดรหัสและเปรียบเทียบดูล่ะ? หรือคุณหมายถึงลำดับของ "Array" หรือlistองค์ประกอบก็ไม่สำคัญเช่นกัน?
mgilson

@ user2085282 คำถามนั้นมีปัญหาอื่นเกิดขึ้น
user193661

2
โปรดให้อภัยความไร้เดียงสาของฉัน แต่ทำไม? องค์ประกอบรายการมีลำดับที่เฉพาะเจาะจงสำหรับเหตุผล
ATOzTOA

1
ดังที่ระบุไว้ในคำตอบนี้อาร์เรย์ JSON จะถูกจัดเรียงดังนั้นอ็อบเจ็กต์เหล่านี้ที่มีอาร์เรย์ที่มีลำดับการจัดเรียงต่างกันจะไม่เท่ากัน stackoverflow.com/a/7214312/18891
Eric Ness

คำตอบ:


146

หากคุณต้องการให้ออบเจ็กต์สองชิ้นที่มีองค์ประกอบเดียวกัน แต่อยู่ในลำดับที่ต่างกันเพื่อเปรียบเทียบให้เท่ากันสิ่งที่ต้องทำก็คือเปรียบเทียบสำเนาที่เรียงลำดับของพวกมันเช่นสำหรับพจนานุกรมที่แสดงโดยสตริง JSON ของคุณaและ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

... แต่ไม่ได้ผลเพราะในแต่ละกรณี"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

หากเราใช้ฟังก์ชันนี้กับaและbผลลัพธ์จะเปรียบเทียบเท่ากับ:

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

1
ขอบคุณมาก Zero Piraeus มันเป็นวิธีแก้ปัญหาทั่วไปที่ฉันต้องการ แต่ปัญหาเดียวคือโค้ดใช้งานได้กับ python 2.x เท่านั้นไม่ใช่สำหรับ python3 ฉันได้รับข้อผิดพลาดต่อไปนี้: TypeError: unorderable types: dict () <dict () วิธีแก้ปัญหาตอนนี้ชัดเจนแล้ว ฉันจะพยายามทำให้มันใช้งานได้กับ python3 ขอบคุณมาก

1
@HoussamHsm ฉันตั้งใจจะแก้ไขสิ่งนี้เพื่อทำงานกับ Python 3.x เมื่อคุณพูดถึงปัญหาคำสั่งที่ไม่สามารถเปลี่ยนได้เป็นครั้งแรก แต่อย่างใดมันก็หนีไปจากฉัน ตอนนี้ใช้งานได้ทั้ง 2.x และ 3.x :-)
Zero Piraeus

เมื่อมีรายการเช่น['astr', {'adict': 'something'}]ฉันได้รับTypeErrorเมื่อพยายามจัดเรียง
Zhenxiao Hao

1
@ Blairg23 คุณเข้าใจผิดคำถามซึ่งเกี่ยวกับการเปรียบเทียบออบเจ็กต์ JSON ว่าเท่ากันเมื่อมีรายการที่มีองค์ประกอบเหมือนกัน แต่อยู่ในลำดับที่ต่างกันไม่เกี่ยวกับลำดับของพจนานุกรมใด ๆ
Zero Piraeus

1
@ Blairg23 ฉันยอมรับว่าคำถามสามารถเขียนได้ชัดเจนกว่านี้ (แม้ว่าคุณจะดูประวัติการแก้ไขแต่ก็ดีกว่าที่จะเริ่มต้น) Re: พจนานุกรมและคำสั่ง - ใช่ฉันรู้ ;-)
Zero Piraeus

46

อีกวิธีหนึ่งคือการใช้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"}เทียบกับ{"error":"b"}, {"error":"a"} จะไม่สามารถจัดเรียงกรณีหลังเป็นกรณีแรกได้
ChromeHearts

@ Blairg23 แต่คุณจะทำอย่างไรถ้าคุณมีรายการที่ซ้อนอยู่ในคำสั่ง? คุณไม่สามารถเปรียบเทียบคำสั่งระดับบนสุดและเรียกมันว่าวันได้นี่ไม่ใช่คำถามเกี่ยวกับ
stpk

4
จะใช้ไม่ได้ถ้าคุณมีรายการอยู่ภายใน เช่น json.dumps({'foo': [3, 1, 2]}, sort_keys=True) == json.dumps({'foo': [2, 1, 3]}, sort_keys=True)
Danil

8
@ ดานิลและอาจจะไม่ควร รายการเป็นโครงสร้างตามลำดับและหากแตกต่างกันตามลำดับเท่านั้นเราควรพิจารณาว่าแตกต่างกัน บางทีสำหรับผู้ใช้ของคุณคำสั่งซื้อไม่สำคัญ แต่เราไม่ควรคิดเช่นนั้น
stpk

เนื่องจากรายการเรียงลำดับตามดัชนีจึงจะไม่ถูกนำไปใช้ [0, 1] ไม่ควรเท่ากับ [1, 0] ในสถานการณ์ส่วนใหญ่ ดังนั้นนี่จึงเป็นทางออกที่ดีสำหรับกรณีปกติ แต่ไม่ใช่สำหรับคำถามข้างต้น ยังคง +1
Harrison

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 ในคำถาม สำหรับวิธีแก้ปัญหาทั่วไปดูคำตอบของ Zero Piraeus


2

สำหรับสอง dicts ต่อไปนี้ '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

ให้ผลลัพธ์ที่ผิดคือเท็จ

ดังนั้นฉันจึงสร้าง cutstom 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

ซึ่งทำให้ฉันได้ผลลัพธ์ที่คาดหวังที่ถูกต้อง!

ตรรกะค่อนข้างง่าย:

หากวัตถุเป็นประเภท 'รายการ' ให้เปรียบเทียบแต่ละรายการของรายการแรกกับรายการของรายการที่สองจนกว่าจะพบและหากไม่พบรายการหลังจากผ่านรายการที่สองแล้ว 'พบ' จะเป็น = เท็จ ค่า 'found' จะถูกส่งกลับ

ไม่เช่นนั้นถ้าวัตถุที่จะเปรียบเทียบเป็นประเภท 'dict' ให้เปรียบเทียบค่าที่มีอยู่สำหรับคีย์ทั้งหมดในวัตถุทั้งสอง (ทำการเปรียบเทียบแบบวนซ้ำ)

หรือเรียกง่ายๆว่า obj1 == obj2 โดยค่าเริ่มต้นทำงานได้ดีสำหรับออบเจ็กต์ของสตริงและตัวเลขและสำหรับeq () เหล่านั้นถูกกำหนดอย่างเหมาะสม

(โปรดทราบว่าอัลกอริทึมสามารถปรับปรุงเพิ่มเติมได้โดยการลบรายการที่พบใน object2 เพื่อไม่ให้รายการถัดไปของ object1 เปรียบเทียบตัวเองกับรายการที่พบใน object2 แล้ว)


คุณช่วยแก้ไขการเยื้องรหัสของคุณได้ไหม
colidyre

@colidyre ตอนนี้เยื้องดีไหม
NiksVij

ไม่ยังคงมีปัญหาอยู่ หลังจากส่วนหัวของฟังก์ชันแล้วบล็อกต้องเยื้องด้วย
colidyre

ใช่. ฉันแก้ไขใหม่อีกครั้ง ฉันคัดลอกวางลงใน IDE และใช้งานได้แล้ว
NiksVij

1

คุณสามารถเขียนฟังก์ชันเท่ากับของคุณเอง:

  • คำสั่งจะเท่ากันถ้า: 1) คีย์ทั้งหมดเท่ากัน 2) ค่าทั้งหมดเท่ากัน
  • รายการจะเท่ากันถ้า: รายการทั้งหมดเท่ากันและอยู่ในลำดับเดียวกัน
  • ดึกดำบรรพ์มีค่าเท่ากันถ้า a == b

เพราะคุณกำลังติดต่อกับ JSON คุณจะมีประเภทหลามมาตรฐาน: dict, listฯลฯ เพื่อให้คุณสามารถทำการตรวจสอบชนิดยาก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

สำหรับคนอื่น ๆ ที่ต้องการดีบักออบเจ็กต์ 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.