json数据比较器

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)
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值