为了满足业务小哥的需求,我开发了 omnidiff

32 阅读5分钟

image.png

前言

大家好,我是game1024, 一名喜欢编程的测试同学,工作了5年,做过不少的DIFF相关的建设,期间和研发小哥讨论过很多功能和细节,在业余时间,利用自己对DIFF这块的理解写了开源的omnidiff工具

为什么选择自己开发

在实际业务中,game1024观察发现各业务api返回的字段,是比较复杂的,比如

  • 有一些字段是不需要关注的,比如时间戳、埋点字段
  • 字段嵌套比较深
  • 部分字段并不简单的用==来判断逻辑,而是有自己的逻辑,比如对于相关性字段,通常我们关注它处在一个正常波动的范围,如果在正常阈值内,认为他是合理的
  • 有一些数组类型的判断需要忽略顺序去比较差异

而现有的开源diff工具中

  • 不支持字段加白配置:比较结果里面有很多噪音
  • 不支持通配规则:对同一个字段,往往要配20+个规则,非常不方便
  • 不支持忽略排序的比较
  • 返回的结果不直观:看不出是哪个路径下的字段有差异

基于以上原因,我想自己动手实现一个灵活,易用的万能diff工具 —— omnidiff

omnidiff功能介绍

先介绍一下,目前omnidiff可以做的事情

  • 检测字段数值差异:比较两个json中,各字段的类型、数值的差异
  • 检测字段缺失:比较两个json中,各自缺失的字段
  • 通配符字段加白:对不关注的字段进行跳过处理,支持通配符支配
  • 数组节点忽略排序:对于需要忽略排序的数组,可以将其转换为字典类型以key-key对应的方式完成差异比较

基本使用

安装

pip install omnidiff

比较差异

比较两个json,,可以看到,@.doc_id@.score两个字段的值是有差异的

from omnidiff import compare
import pprint

a_json = {
    'doc_id': '1111',
    "score": 0.111,
    "tag":["live", "food"]
}

b_json = {
    'doc_id': '2222',
    'score': 0.222,
    'author': 'game1024'
}


compare_result = compare(a_json, b_json)
pprint.pprint(compare_result.diff_fields)

输出如下

[('@.doc_id', # 有差异的字段路径
  '1111',     # 字段在a中的值
  '2222',     # 字段在b中的值
  'path:@.doc_id is different, a_value:1111 b_value:2222'), # diff 原因
 ('@.score',
  0.111,
  0.222,
  'path:@.score is different, a_value:0.111 b_value:0.222')]

实际上,我们还可以获取a,b分别缺失对方的哪些字段,可以通过如下两个属性获取

  • compare_result.a_missing_fields
  • compare_result.b_missing_fields
from omnidiff import compare

a_json = {
    'doc_id': '1111',
    "score": 0.111,
    "tag":["live", "food"]
}

b_json = {
    'doc_id': '2222',
    'score': 0.222,
    'author': 'game1024'
}

compare_result = compare(a_json, b_json)
print("a missing fields:", compare_result.a_missing_fields)
print("b missing fields:", compare_result.b_missing_fields) 

输出如下,missing_fields中的元组格式为(缺失字段路径, 对方的值)

a missing fields: [('@.author', 'game1024')]
b missing fields: [('@.tag', ['live', 'food']), ('@.tag[0]', 'live'), ('@.tag[1]', 'food')]

忽略字段

让我再看一个更复杂点的业务场景,

  • a_json.items和b_json.items是两个对象数组,
  • exp_map是每个对象的扩展字段,里面包含相关打分,相关性档位,更新时间等字段

很明显update_time这个字段是我们不太想要关注的, 希望在比较的过程中忽略

a_json = {
    "items":[
        {
            'doc_id': '1111',
            "score": 0.111,
            "ext_map": {
                "static_score": "0.1111",
                "relevance_level": "1",
                "update_time": "2025-03-17 11:30"
            }
        },
        {
            'doc_id': '2222',
            "score": 0.222,
            "ext_map": {
                "static_score": "0.2222",
                "relevance_level": "2",
                "update_time": "2025-03-17 11:30"
            }
        },
    ]
}

b_json = {
    "items":[
        {
            'doc_id': '1111',
            "score": 0.111,
            "ext_map": {
                "static_score": "0.1111",
                "relevance_level": "1",
                "update_time": "2025-03-17 11:31"
            }
        },
        {
            'doc_id': '2222',
            "score": 0.222,
            "ext_map": {
                "static_score": "0.2222",
                "relevance_level": "2",
                "update_time": "2025-03-17 11:31"
            }
        },
    ]
}

通过配置skip_paths = ['@.items[*].ext_map.update_time']即可忽略对应字段的比较

from omnidiff import compare
import pprint

a_json = {...}
b_json = {...}

skip_paths=['@.items[*].ext_map.update_time']
compare_result = compare(a_json, b_json, skip_paths=skip_paths)
pprint.pprint(compare_result.diff_fields)

输出

# 由于路径被忽略了,所以不会产生diff
[]

基于key-key匹配

如果业务场景更复杂点,对象数组的数量可能不一致,排序也被打乱了,如果直接比较必然会导致下面的结果

  • a_json['items'][0] 与 b_json['items'][0] 的文档对不上产生diff
  • a_json['items'][1] 与 b_json['items'][1] 的文档对不上产生diff
  • a_json['items'][2] 与 b_json['items'][2] 的文档对不上产生diff

但是我们实际想关注的是

  • a中的doc_id=1111与b中的doc_id=1111之间做diff
  • a中的doc_id=2222与b中的doc_id=2222之间做diff
  • a中的doc_id=3333与b中的doc_id=3333之间做diff
a_json = {
    "items":[
        {
            'doc_id': '1111',
            "score": 0.111,
        },
        {
            'doc_id': '2222',
            "score": 0.222,
        },
        {
            'doc_id': '3333',
            "score": 0.333,
        },
    ]
}

b_json = {
    "items":[
        {
            'doc_id': '3333',
            "score": 0.333,
        },
        {
            'doc_id': '1111',
            "score": 0.111,
        },
        {
            'doc_id': '2222',
            "score": 0.222,
        },
    ]
}

通过map_jmespath将对应的jmespath数组节点转换成dict节点

from omnidiff import compare, map_jmespath
import pprint

# @.items是数组节点
# lambda是hash函数,用来生成对应的key
# 这里生成key的时候加了_前缀,因为jmespath不支持纯数字的key
a_json = map_jmespath(a_json, '@.items', lambda item => '_'+item['doc_id'])
b_json = map_jmespath(b_json, '@.items', lambda item => '_'+item['doc_id'])

compare_result = compare(a_json, b_json, skip_paths=skip_paths)
pprint.pprint(compare_result.diff_fields)

输出

[]

map_jmespath做了什么?

实际上map_jmespath将对应的节点转换成了字典形式,这样做的一个目的是,原本数组节点是基于下标对应来比较差值的,转换后可以基于对象里面的id来对应比较差异

# 转换前
{
    "items":[
        {
            'doc_id': '1111',
            "score": 0.111,
        },
        {
            'doc_id': '2222',
            "score": 0.222,
        },
        {
            'doc_id': '3333',
            "score": 0.333,
        },
    ]
}

# 转换后
{
    'items': {
        '_1111': {
            'doc_id': '1111', 
            'score': 0.111
        },
        '_2222': {
            'doc_id': '2222', 
            'score': 0.222
        },
        '_3333': {
            'doc_id': '3333', 
            'score': 0.333
        }
    }
}

未来功能

  • 指标聚合
  • 自定义差异比较
  • ...

Github:game1024/omnidiff

如果这个工具对你有用的话,别忘了点赞噢!