前言
大家好,我是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]
的文档对不上产生diffa_json['items'][1] 与 b_json['items'][1]
的文档对不上产生diffa_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
如果这个工具对你有用的话,别忘了点赞噢!