json数据比较器
不仅支持对json对象、数组的直接比较,还支持嵌套的复杂json对象和数组的直接比较。
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@Author: 思文伟
'''
import json
class JSONCheckpoint(object):
"""JSON格式数据检查点,支持两个复杂JSON直接比较
JSON 数据写为名称/值对。 名称/值由字段名称构成,后跟冒号和值:{ "name":"Bill Gates" }
在 JSON 中,值必须是以下数据类型之一:
字符串
数字
对象(JSON 对象)
数组
布尔
null
Simple usage:
alist = [{
"status": "success",
"data": {
"44131521": [{
"joinno": "201812120000013935",
"zzbcode": "44131521",
}, {
"joinno": "201812120000013936",
"zzbcode": "44131521",
}, {
"joinno": "201812120000013937",
"zzbcode": "44131521",
}]
}
}]
elist = [{
"status": "success",
"data": {
"44131521": [{
"joinno": "201812120000013937",
"zzbcode": "44131521"
}, {
"joinno": "201812120000013935",
"zzbcode": "44131521",
}, {
"joinno": "201812120000013936",
"zzbcode": "44131521",
}]
}
}]
JSONCheckpoint().test(alist, elist, False)
"""
CONVERT_JSON_TEXT_TO_PYTHON_OBJECT = True
def to_py_obj(self, json_text):
return json.loads(json_text)
def test(self, actual, expected, convert=CONVERT_JSON_TEXT_TO_PYTHON_OBJECT, ignore_position=True, list_strict=True, dict_strict=True):
"""比较两个对象是否相等
Args:
- actual: 实际
- expected: 预期
- ignore_position: 控制是调用忽略位置比较函数还是完全相等比较函数 True - 忽略位置 False - 完全相等
- list_strict: ignore_position为True的时候,该参数才有意义
- dict_strict: ignore_position为True的时候,该参数才有意义
"""
if convert == self.CONVERT_JSON_TEXT_TO_PYTHON_OBJECT:
expected = self.to_py_obj(expected)
print(self.equals(actual, expected, ignore_position=ignore_position, list_strict=list_strict, dict_strict=dict_strict))
def equals(self, actual, expected, ignore_position=True, list_strict=True, dict_strict=True):
"""比较两个对象是否相等
Args:
- actual: 实际
- expected: 预期
- ignore_position: 控制是调用忽略位置比较函数还是完全相等比较函数 True - 忽略位置 False - 完全相等
- list_strict: ignore_position为True的时候,该参数才有意义
- dict_strict: ignore_position为True的时候,该参数才有意义
"""
if isinstance(expected, (list, tuple)) and isinstance(actual, (list, tuple)):
if ignore_position:
return self.json_array_equals_ignore_position(actual, expected, list_strict=list_strict, dict_strict=dict_strict)
else:
return self.json_array_equals(actual, expected)
elif isinstance(expected, dict) and isinstance(actual, dict):
if ignore_position:
return self.json_obj_equals_ignore_position(actual, expected, dict_strict=dict_strict, list_strict=list_strict)
else:
return self.json_obj_equals(actual, expected)
elif isinstance(expected, bool) or isinstance(actual, bool): # 任意一个是bool值,则进行bool值比较
return self.all_is_same_bool_value(actual, expected)
elif expected is None or actual is None: # 任意一个是None值,则进行None比较
return self.all_is_none(actual, expected)
else:
return actual == expected
def _group(self, dlist):
"""根据数据类型对列表的子元素分组,分组规则如下:
- None 分为一组
- 布尔值 分为一组
- 字符串 整型 浮点型 分为一组
- 列表 元祖 分组一组
- 字典类型分为一组
- 除了上面之外的其他类型分为一组
"""
others = []
sublists = []
subdicts = []
nonelist = []
boollist = []
str_int_float = []
for one in dlist:
if one is None:
nonelist.append(one)
elif isinstance(one, bool):
boollist.append(one)
elif isinstance(one, (str, int, float)):
str_int_float.append(one)
elif isinstance(one, (list, tuple)):
sublists.append(one)
elif isinstance(one, dict):
subdicts.append(one)
else:
others.append(one)
return nonelist, boollist, str_int_float, sublists, subdicts, others
def json_obj_equals_ignore_position(self, adict, edict, dict_strict=True, list_strict=True):
"""json对象类型数据相当于python的字典,比较两个字典是否相等,支持嵌套字典和列表元祖
Args:
- adict 实际字典
- edict 预期字典
- dict_strict 布尔值,控制字典及其包含的嵌套字典的比较方式, True - 两个字典必须键和值完全一致才相等 False - 只要实际字典的包含有预期字典的键值就认为相等
- list_strict 布尔值,控制列表及其包含的嵌套列表的比较方式, True - 两个列表元素个数必须完全相等,同时元素都一样则认为两个列表相等 False - 只要实际列表包含有预期列表的元素就认为两个列表相等(相当于预期列表的元素是实际列表的元素的子集)
Useage:
self.json_obj_equals_ignore_position({"a":1,"b":2}, {"a":1,"b":2}) >>> True
self.json_obj_equals_ignore_position({"a":1,"b":2}, {"a":1}, dict_strict=False) >>> True
self.json_obj_equals_ignore_position({"a":1,"b":{"c":3}}, {"a":1,"b":{"c":3}}) >>> True
"""
akeys = adict.keys()
ekeys = edict.keys()
if dict_strict:
if len(akeys) != len(ekeys):
return False
for k in ekeys:
if k not in akeys:
return False
else:
v1 = adict[k]
v2 = edict[k]
if isinstance(v1, (list, tuple)) and isinstance(v2, (list, tuple)):
if not self.json_array_equals_ignore_position(v1, v2, list_strict=list_strict, dict_strict=dict_strict):
return False
elif isinstance(v1, dict) and isinstance(v2, dict):
subresult = self.json_obj_equals_ignore_position(v1, v2, dict_strict=dict_strict, list_strict=list_strict)
if not subresult:
return False
elif isinstance(v1, bool) or isinstance(v2, bool): # 任意一个是bool值,则进行bool值比较
if not self.all_is_same_bool_value(v1, v2):
return False
elif v1 is None or v2 is None: # 任意一个是None值,则进行None比较
if not self.all_is_none(v1, v2):
return False
else:
if v1 != v2:
return False
return True
def _none_equals(self, a_nonelist, e_nonelist, strict=True):
is_equals = True
if strict:
len1 = len(a_nonelist)
len2 = len(e_nonelist)
used = []
if len1 == len2:
for val in e_nonelist:
if val not in used:
used.append(val)
count1 = a_nonelist.count(val)
count2 = e_nonelist.count(val)
if count1 != count2:
is_equals = False
break
else:
is_equals = False
else:
foundindexs = []
for e_none in e_nonelist:
found = False
for i, a_none in enumerate(a_nonelist):
if i not in foundindexs:
if e_none is a_none:
foundindexs.append(i)
found = True
if not found:
is_equals = False
break
return is_equals
_bool_equals = _none_equals
def _basic_data_equals(self, a_data, e_data, strict=True):
is_equals = True
if strict:
used = []
len1 = len(a_data)
len2 = len(e_data)
if len1 == len2:
for val in a_data:
if val not in used:
used.append(val)
count1 = a_data.count(val)
count2 = e_data.count(val)
if count1 != count2:
is_equals = False
break
else:
is_equals = False
else:
found_indexs = []
for e_val in e_data:
found = False
for i, a_val in enumerate(a_data):
if i not in found_indexs:
if e_val == a_val:
found_indexs.append(i)
found = True
break
if not found:
is_equals = False
break
return is_equals
_unknow_type_data_equals = _basic_data_equals
@staticmethod
def is_all_empty_objs(*objs):
empty = True
for obj in objs:
if len(obj) >= 1:
empty = False
break
return empty
@staticmethod
def all_is_same_bool_value(*objs):
truelist = []
falselist = []
non_bool = False
for obj in objs:
if isinstance(obj, bool):
if obj is True:
truelist.append(obj)
else:
falselist.append(obj)
else:
non_bool = True
break
if non_bool:
return False # any object is not boolean
else:
if len(objs) == len(truelist) or len(objs) == len(falselist):
return True
else:
return False
@staticmethod
def all_is_none(*objs):
nonelist = []
for obj in objs:
if obj is None:
nonelist.append(obj)
else:
break
if len(nonelist) == len(objs):
return True
else:
return False
def _json_array_equals_ignore_position(self, source, expected, list_strict=True, dict_strict=True):
"""
Args:
- source 实际列表中所有是列表的子元素构成的列表
- expected 预期列表中所有是列表的子元素构成的列表
- list_strict 布尔值,控制列表及其包含的嵌套列表的比较方式, True - 两个列表元素个数必须完全相等,同时元素都一样则认为两个列表相等 False - 只要实际列表包含有预期列表的元素就认为两个列表相等(相当于预期列表的元素是实际列表的元素的子集)
- dict_strict 布尔值,控制字典及其包含的嵌套字典的比较方式, True - 两个字典必须键和值完全一致才相等 False - 只要实际字典的包含有预期字典的键值就认为相等
"""
len1 = len(source)
len2 = len(expected)
founds = []
for a_val in source:
a_len = len(a_val)
for e_val in expected:
e_len = len(e_val)
if a_len == e_len:
if self.json_array_equals_ignore_position(a_val, e_val, list_strict=list_strict, dict_strict=dict_strict):
founds.append(a_val)
break
flen = len(founds)
return (len1 == len2) and (len2 == flen)
def _dictlist_check(self, source, expected, list_strict=True, dict_strict=True):
"""
Args:
- source 实际列表中所有是字典的子元素构成的列表
- expected 预期列表中所有是字典的子元素构成的列表
- dict_strict 布尔值,控制字典及其包含的嵌套字典的比较方式, True - 两个字典必须键和值完全一致才相等 False - 只要实际字典的包含有预期字典的键值就认为相等
- list_strict 布尔值,控制列表及其包含的嵌套列表的比较方式, True - 两个列表元素个数必须完全相等,同时元素都一样则认为两个列表相等 False - 只要实际列表包含有预期列表的元素就认为两个列表相等(相当于预期列表的元素是实际列表的元素的子集)
"""
len1 = len(source)
len2 = len(expected)
if len1 == len2:
used_indexs = []
not_found_in_source = []
for edict in expected:
found = False
for index, sdict in enumerate(source):
if index in used_indexs:
continue
if self.json_obj_equals_ignore_position(sdict, edict, dict_strict=dict_strict, list_strict=list_strict):
found = True
used_indexs.append(index)
break
if not found:
not_found_in_source.append(edict)
break
if not_found_in_source:
return False
else:
return True
else:
return False
def printjson(self, *obj, **config):
msg = config.get('msg', '')
printable = [json.dumps(o, ensure_ascii=False) for o in obj]
print(msg + '\n'.join(printable))
def json_array_equals_ignore_position(self, alist, elist, list_strict=True, dict_strict=True):
"""忽略列表元素位置,比较两个列表是否相等,支持列表元素可以是嵌套的数据
Args:
- alist 实际列表
- elist 预期列表
- dict_strict 布尔值,控制字典及其包含的嵌套字典的比较方式, True - 两个字典必须键和值完全一致才相等 False - 只要实际字典的包含有预期字典的键值就认为相等
- list_strict 布尔值,控制列表及其包含的嵌套列表的比较方式, True - 两个列表元素个数必须完全相等,同时元素都一样则认为两个列表相等 False - 只要实际列表包含有预期列表的元素就认为两个列表相等(相当于预期列表的元素是实际列表的元素的子集)
"""
if list_strict:
if len(alist) != len(elist):
return False
a_nonelist, a_boollist, a_basedata, a_array_list, a_dict_list, a_other_list = self._group(alist)
e_nonelist, e_boollist, e_basedata, e_array_list, e_dict_list, e_other_list = self._group(elist)
noneresult = self._none_equals(a_nonelist, e_nonelist, list_strict)
boolresult = self._bool_equals(a_boollist, e_boollist, list_strict)
bresult = self._basic_data_equals(a_basedata, e_basedata, list_strict)
uresult = self._unknow_type_data_equals(a_other_list, e_other_list, list_strict)
r1 = noneresult and boolresult and bresult and uresult
empty = self.is_all_empty_objs(a_array_list, a_dict_list, e_array_list, e_dict_list)
if (not r1) or empty:
return r1
r2 = self._json_array_equals_ignore_position(a_array_list, e_array_list, list_strict=list_strict, dict_strict=dict_strict)
r3 = self._dictlist_check(a_dict_list, e_dict_list, dict_strict=dict_strict, list_strict=list_strict)
return r1 and r2 and r3
def json_obj_equals(self, adict, edict):
"""判断两个json对象是否完全相等
Args:
- adict 实际字典
- edict 预期字典
Useage:
self.json_obj_equals({"a":1,"b":2}, {"a":1,"b":2}) >>> True
self.json_obj_equals({"a":1,"b":2}, {"a":1}) >>> False
self.json_obj_equals({"a":1,"b":{"c":3}}, {"a":1,"b":{"c":3}}) >>> True
"""
akeys = adict.keys()
ekeys = edict.keys()
if len(akeys) != len(ekeys):
return False
for k in ekeys:
if k not in akeys:
return False
else:
v1 = adict[k]
v2 = edict[k]
if isinstance(v1, (list, tuple)) and isinstance(v2, (list, tuple)):
if not self.json_array_equals(v1, v2):
return False
elif isinstance(v1, dict) and isinstance(v2, dict):
subresult = self.json_obj_equals(v1, v2)
if not subresult:
return False
elif isinstance(v1, bool) or isinstance(v2, bool): # 任意一个是bool值,则进行bool值比较
if not self.all_is_same_bool_value(v1, v2):
return False
elif v1 is None or v2 is None: # 任意一个是None值,则进行None比较
if not self.all_is_none(v1, v2):
return False
else:
if v1 != v2:
return False
return True
def json_array_equals(self, alist, elist):
"""判断json数组是否完全相等,即两个数组的元素个数以及相同位置的元素值必须完全相等,才相等
Args:
- alist 实际列表
- elist 预期列表
"""
alen = len(alist)
elen = len(elist)
if alen != elen:
return False
for i, v1 in enumerate(elist):
v2 = alist[i]
if isinstance(v1, (list, tuple)) and isinstance(v2, (list, tuple)):
if not self.json_array_equals(v2, v1):
return False
elif isinstance(v1, dict) and isinstance(v2, dict):
if not self.json_obj_equals(v2, v1):
return False
elif isinstance(v1, bool) or isinstance(v2, bool): # 任意一个是bool值,则进行bool值比较
if not self.all_is_same_bool_value(v1, v2):
return False
elif v1 is None or v2 is None: # 任意一个是None值,则进行None比较
if not self.all_is_none(v1, v2):
return False
else:
if v1 != v2:
return False
return True
class Age(object):
def __init__(self, number):
self._number = number
def __eq__(self, other):
return self._number == other._number
if __name__ == "__main__":
checker = JSONCheckpoint()
# ignore_position=False 比较相等(完全一致)
# 即两个值必须完全相等,如果两个值是列表或字典,那么列表及其嵌套列表的元素个数以及相同位置的元素值必须完全相等,
# 字典及其嵌套的子孙字典的键值必须完全一致,才认为则两个值相等
a = [1, 2, {"age": 18}]
b = [1, 2, {"age": 18}]
checker.test(a, b, False, ignore_position=False)
a = {"age": 18, "friends": ["小明", "小红"]}
b = {"age": 18, "friends": ["小明", "小红"]}
checker.test(a, b, False, ignore_position=False)
# ignore_position=True, list_strict=True, dict_strict=True
# 忽略数组或者嵌套数组中元素的位置,实际和预期数组的元素必须完全相同,实际和预期字典的键值必须完全一致,则认为相等
a = [1, 2, {"age": 18}]
b = [2, {"age": 18}, 1]
checker.test(a, b, False, ignore_position=True, list_strict=True, dict_strict=True)
a = {"age": 18, "friends": ["小红", "小明"]}
b = {"age": 18, "friends": ["小明", "小红"]}
checker.test(a, b, False, ignore_position=True, list_strict=True, dict_strict=True)
# ignore_position=True, list_strict=False, dict_strict=True
# 忽略数组或者嵌套数组中元素的位置,实际数组完全包含有预期数组的元素,实际和预期字典的键值必须完全一致,则认为相等
a = [1, 2, {"age": 18}, 3]
b = [2, {"age": 18}, 1]
checker.test(a, b, False, ignore_position=True, list_strict=False, dict_strict=True)
a = {"age": 18, "friends": ["小红", "小明", "小霞"]}
b = {"age": 18, "friends": ["小明", "小红"]}
checker.test(a, b, False, ignore_position=True, list_strict=False, dict_strict=True)
# ignore_position=True, list_strict=True, dict_strict=False
# 忽略数组或者嵌套数组中元素的位置,实际和预期数组的元素必须完全相同,实际字典完全包含有预期字典的键值,则认为相等
a = [1, 2, {"age": 18, "name": "小玄"}]
b = [2, {"age": 18}, 1]
checker.test(a, b, False, ignore_position=True, list_strict=True, dict_strict=False)
a = {"age": 18, "name": "卡罗拉", "friends": ["小红", "小明"]}
b = {"age": 18, "friends": ["小明", "小红"]}
checker.test(a, b, False, ignore_position=True, list_strict=True, dict_strict=False)
# ignore_position=True, list_strict=False, dict_strict=False
# 忽略数组或者嵌套数组中元素的位置,实际数组完全包含有预期数组的元素,实际字典完全包含有预期字典的键值,则认为相等
a = [1, 2, {"age": 18, "name": "小玄"}, 3, 2, False, False, None, None, Age(1), Age(1)]
b = [2, {"age": 18}, 1, 2, False, None, Age(1)]
checker.test(a, b, False, ignore_position=True, list_strict=False, dict_strict=False)
a = {"age": 18, "name": "卡罗拉", "friends": ["小红", "小明", "小霞"]}
b = {"age": 18, "friends": ["小明", "小红"]}
checker.test(a, b, False, ignore_position=True, list_strict=False, dict_strict=False)