python中递归比较json、列表和字典,显示差别的库,可忽略顺序,可支持正则,可设浮点精度(已上传至pypi,库名jsoncomparedeep)

在做接口自动化测试的时候,经常需要对返回的json比较、做断言。
但是如果返回的json串很大,手写断言就非常的麻烦。

网上虽然有很多轮子,但是都不是特别好用,存在比较严重的这样那样的缺陷。
所以我自己写了一个(功能更新于10月8日,版本1.20)。目前已经完整测试了python26/27,35/36/37/38/39的兼容性,且已经是公司接口自动化测试的主力断言库。但不管是哪个版本都欢迎继续测试,发现问题请告知我,谢谢!
github仓库 https://github.com/rainydew/jsoncomparedeep 欢迎issue star PR~

自己也想写一个库,上传到pypi上给大家用? 参考我的另一篇博文https://blog.csdn.net/qq_27884799/article/details/96664812

经过多次迭代,功能已经比较完善。目前已上传至pypi(上传pypi一堆坑啊),可以cmd里使用如下命令快速安装和使用(兼容python26+和35+)

pip install jsoncomparedeep

目前pypi上最新版本是1.18。还在使用旧版本的可以用如下命令升级

pip install -U jsoncomparedeep

在python中使用,更详细示例见文末

1.可以递归显示具体差异发生的位置。默认采用忽略列表顺序的方式比较

from json_compare import Jcompare
cp = Jcompare()
a = {
   "k1":"v1","k2":["v1", "v3"]}
b = {
   "k1":"v1","k2":["v4", "v1"]}
print(cp.compare(a, b))

输出

a is {'k1': 'v1', 'k2': ['v1', 'v3']}
b is {'k1': 'v1', 'k2': ['v4', 'v1']}
ignore_list_seq = True, re_compare = True, ignore_path = None
list b at /k2/0
has element that another list hasn't :
'v4'
list a at /k2/1
has element that another list hasn't :
'v3'
False


2.考虑列表顺序比较

print(cp.compare(a, b, ignore_list_seq=False))

输出

a is {'k1': 'v1', 'k2': ['v1', 'v3']}
b is {'k1': 'v1', 'k2': ['v4', 'v1']}
ignore_list_seq = False, re_compare = True, ignore_path = None
different value at /k2/0
a: 'v1'
b: 'v4'
different value at /k2/1
a: 'v3'
b: 'v1'
False


3.在上面的基础之上,忽略/k2/0和/k2/1两个位置比较
(即键k2下面,列表下标为0和1的元素不参加比较,所以两个json就被视为一致)

print(cp.compare(a, b, ignore_list_seq=False, ignore_path=["/k2/0", "/k2/1"]))

输出

a is {'k1': 'v1', 'k2': ['v1', 'v3']}
b is {'k1': 'v1', 'k2': ['v4', 'v1']}
ignore_list_seq = False, re_compare = True, ignore_path = ['/k2/0', '/k2/1']
True


4.使用正则表达式匹配,v.代表以v+任意一个字符,所以能匹配上v3

a = {
   "k1":"v1","k2":["v1", "v3"]}
b = {
   "k1":"v1","k2":["v1", "^(v.)"]}
print(cp.compare(a, b, ignore_list_seq=False, re_compare=True))

输出

a is {'k1': 'v1', 'k2': ['v1', 'v3']}
b is {'k1': 'v1', 'k2': ['v1', '^(v.)']}
ignore_list_seq = False, re_compare = True, ignore_path = None
True


5.使用正则表达式匹配,如果匹配不到结果,或者括号内匹配的内容和实际字符串不相符,匹配就会失败。这里括号外多了一个小数点,导致数字3被迫匹配到括号外侧,从而括号内侧只剩下v而不能通过

a = {
   "k1":"v1","k2":["v1", "v3"]}
b = {
   "k1":"v1","k2":["v1", "^(v.*)."]}
print(cp.compare(a, b, ignore_list_seq=False, re_compare=True))

输出

a is {'k1': 'v1', 'k2': ['v1', 'v3']}
b is {'k1': 'v1', 'k2': ['v1', '^(v.)']}
ignore_list_seq = False, re_compare = True, ignore_path = None
re compare failed, found v, expect v3, see next line
different value at /k2/1
a: u'v3'
b: u'^(v.*).'
False


6.可以兼容不同类型的字符串,可以兼容不同类型的数值,可以兼容元组和列表,还可以拿json字符串和对象比较……

a = ("字节", u"字符", 1)
b = '[1.0, "字符", "字节"]'
print(cp.compare(a, b))

输出

a is ('字节', '字符', 1)
b is [1.0, '字符', '字节']
ignore_list_seq = True, re_compare = True, ignore_path = None
True


7.当设置属性print_before为False后,再次比较将不再打印调试信息

cp.print_before = False
print(cp.compare(a, b))

只输出

True


8.默认情况下,浮点数比较时会遇到误差累计的情况

a = [0.1+0.1+0.1]
b = [0.3]
print(cp.compare(a, b))

会导致匹配不通过,显示如下

a is [0.30000000000000004]
b is [0.3]
ignore_list_seq = False, re_compare = True, ignore_path = None, float_fuzzy_digits = 0
different value at /0
a: 0.30000000000000004
b: 0.3
False

9.可以通过指定精度来解决(默认为0,即完全匹配)。精度6,表示允许10的-6次方以内的误差。

cp.float_fuzzy_digits = 6
print(cp.compare(a, b))

则可以正确匹配

a is [0.30000000000000004]
b is [0.3]
ignore_list_seq = False, re_compare = True, ignore_path = None, float_fuzzy_digits = 6
True


10.如今,指定忽略路径时也开始支持正则表达式。所以以下写法变为可能

a = [{
   "a": 1, "b": 2}, {
   "a": 1, "b": 4}]  # also useful under list_seq ignored
b = [{
   "a": 2, "b": 4}, {
   "a": 2, "b": 2}]
print(cp.compare(a, b, ignore_path=[r"^(/\d+/a)"]))

因为忽略了所有列表中嵌套的子字典的键a,所以只有键b的值参加比较。又因为采用默认忽略列表顺序的比较,所以键b的值2,4和4,2是相等的,这个匹配会成功

a is [{'a': 1, 'b': 2}, {'a': 1, 'b': 4}]
b is [{'a': 2, 'b': 4}, {'a': 2, 'b': 2}]
ignore_list_seq = True, re_compare = True, ignore_path = ['^(/\\d+/a)'], float_fuzzy_digits = 0
True

全部功能:

  • 可以断定两个对象(或json字串,会自动解析json字串)是否相等。如不等,可以打印出差异项和发生差异的位置
  • 可以兼容各种类型,包括json字符串,也包括json解析后的列表、字典所构成的嵌套对象
  • 可以识别字典、列表和多层嵌套,且可以深入层层嵌套到内部打印,并同时打印出深入的路径
  • 可以和gson等兼容,忽略列表的顺序,也可以选择不忽略。在忽略状态下,[[1,2],[3,4]]和[[4,3],[2,1]]是相等的;如不忽略则不相等
  • 可以兼容处理unicode类型的字符串和str(utf-8编码)类型的字符串。例如对象 [u"你好"] 和 [“你好”] 是相等的;json字串 u’{“name”: “姓名”}’ 和 ‘{“name”: “姓名”}’ 也是相等的
  • 若解析的对象不是合法的json对象,会被断言出来发现
  • 新增,支持正则表达式断言,提供字符串模糊匹配
  • 新增,支持元组格式(适合pymysql的dictCursor查出的结果直接拿来断言)
  • 新增,支持跳过特定路径的项做比较

更新:

  • 2019.7.1 支持模糊匹配整型、长整型、浮点型,1、1L和1.0三者比较,不会再报类型不一致
  • 2019.7.4 修复了字符型json和字典/列表型对象之间比较时,传参为None的bug
  • 2019.7.9 升级多处
    • 修复了循环中有些不进一步打印不同点的bug
    • 增加了正则表达式断言
    • 不同点的展示更友好
    • 对不等长的列表,现在支持进一步报告差异了(目前只支持以无序的方式显示不同点。如要有序需动态规划,性价比不高,暂时不纳入)
    • 提供了支持python 3的版本
  • 2019.7.12 修复了一些缺陷,通过six和codecs提供了python 2和3都兼容的版本,并完善demo的注释和断言
  • 2019.7.13 支持跳过指定路径的某个或某些项的比较
  • 2019.7.14 做了一些跨平台适配,并上传至PyPi
  • 2019.8.17 跳过指定路径比较时,也支持用正则表达式匹配路径了;支持浮点数模糊比较(自己指定精度)
  • 2019.8.19 修复了一个在字符串json比较时,忽略路径丢失的bug
  • 2020.3.22 修复了python 3.8不兼容的问题,并上传到github
  • 2020.5.8 新增了异常结果的重定向,可以自定义回调函数来接受字符信息
  • 2020.7.16 修复了handler在有一个对象是字符串json的情况下没有递归透传handler的问题
  • 2020.7.25 修复了少许小问题,提供了strict_number_type,在strict_number_type下,数据比较不仅仅是值比较,还会做类型比较。因而会有1.0(float)!=1(int),适合静态类型语言接口的返回值定义检查
  • 2020.10.8 支持了omit_path。原先提供的忽略路径参数ignore_path只会忽略字典/列表的某个key/item里值不相等的问题,而无法忽略根本不存在的key。例如指定ignore_path为["/a"],{“a”: 1, “b”: 2}和{“b”: 2}仍然是不能match的。而如果指定omit_path为["/a"],则可以包容任何一边/a根本不存在的情况。对于go通过struct序列化json时的场景,这个功能能很好的支持omitempty的tag。

缺陷:目前对于不等长的list,只报告不等长,不会报告具体哪些差异,要实现需要一定量的改动,欢迎二次开发和反馈bug。
(现在已经大部分提供此功能)

这是目前pypi上1.20的代码,支持几乎所有新功能,且在windows、linux和mac上均做过测试。最新代码请移步github仓库或pypi查看。

#!/usr/bin/env python
# coding: utf-8
# author: Rainy Chan rainydew@qq.com
# platform: python 2.6-2.7, 3.5-3.8+
# demos are provided in test_json_compare.py
# version: 1.19
from __future__ import print_function
import json
import re
import traceback
import six
import codecs

NUMBER_TYPES = list(six.integer_types) + [float]


class Jcompare(object):
    def __init__(self, print_before=True, float_fuzzy_digits=0, strict_number_type=False):
        """
        :param bool print_before: Set True to print the objects or strings to compare first, disable it if printed
        :param int float_fuzzy_digits: The accuracy (number of digits) required when float compare. 0 disables fuzzy
        :param bool strict_number_type: Set True to let <float> 1.0 != <int> 1, however int and long types are still the
        same. Default value means int and float with equal value can pass the test
        """
        self.print_before = print_before
        self.float_fuzzy_digits = float_fuzzy_digits
        self.strict_number_type = strict_number_type
        self._res = None
        self._ignore_list_seq = None
        self._re_compare = True
        self._ignore_path = None
        self._omit_path = None
        self._handle = print

    @staticmethod
    def _tuple_append(t, i):
        return tuple(list(t) + [six.text_type(i)])

    @staticmethod
    def _to_unicode_if_string(strlike):
        if type(strlike) == six.binary_type:
            try:
                return strlike.decode('utf-8')
            except UnicodeDecodeError:
                raise ValueError("decoding string {} failed, may be local encoded".format(repr(strlike)))
        else:
            return strlike

    @staticmethod
    def _to_list_if_tuple(listlike):
        if type(listlike) == tuple:
            return list(listlike)
        else:
            return listlike

    def _common_warp(self, anylike):
        return self._to_list_if_tuple(self._to_unicode_if_string(anylike))

    def _fuzzy_float_equal(self, a, b):
        if self.float_fuzzy_digits:
            return abs(a - b) < 10 ** (-self.float_fuzzy_digits)
        else:
            return a == b

    @staticmethod
    def _modify_a_key(dic, from_key, to_key):
        assert not any([type(to_key) == type(exist_key) and to_key == exist_key for exist_key in
                        dic.keys()]), 'cannot change the key due to key conflicts'
        # cannot use IN here `to_key in dic.keys()`, because u"a" in ["a"] == True
        dic[to_key] = dic.pop(from_key)

    def _fuzzy_number_type(self, value):
        if not self.strict_number_type:
            type_dict = {
   x: float for x in six.integer_types}
        else:
            type_dict = {
   x: int for x in six.integer_types}
        res = type(value)
        return type_dict.get(res, res)

    def _turn_dict_keys_to_unicode(self, dic):
        keys = dic.keys()
        modifiers = []
        for key in keys:  # a.keys() returns a constant, so it is safe because ak won't change
            if type(key) == six.binary_type:
                modifiers.append((key, self._to_unicode_if_string(key)))
            else:
                assert type(key) == six.text_type, 'key {} must be string or unicode in dict {}'.format(key, dic)
                
        for from_key, to_key in modifiers:
            self._modify_a_key(dic, from_key, to_key)

    def _set_false(self):
        self._res = False

    @staticmethod
    def _escape(s):
        """
        :param s: binary if py2 else unicode
        :return:
        """
        if r'\x' in s
  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值