前言

前段时间同事说他有个需求是比较两个JSON文件之间的差异点,身为DB大神的同事用SQL实现了这个需求,让只会CRUD的我直呼神乎其技。当时用一个一千万多字符、四十多万行的JSON文件来测试,SQL查出来要9秒。周六有时间,拜读了下同事的SQL,打算用Python和Go实现下试试。

测试的json文件如下,其中dst.jsonsrc.json文件复制而来,随便找了个地方改了下。单文件为409510行,字符数为11473154左右。

$ wc -ml ./src.json dst.json
409510 11473154 ./src.json
409510 11473155 dst.json
819020 22946309 总计

第三方库jsondiff

先在网上搜了下有没有现成的第三方库,找到一个叫jsondiff的第三方python库。使用pip安装后,用法如下

import json
import jsondiff
import os
from typing import Any def read_json(filepath: str) -> Any:
if not os.path.exists(filepath):
raise FileNotFoundError(filepath)
try:
with open(filepath, "r") as f:
data = json.load(f)
except json.JSONDecodeError as e:
raise Exception(f"{filepath} is not a valid json file") from e
else:
return data if __name__ == "__main__":
src_data = read_json("src.json")
dst_data = read_json("dst.json") diffs = jsondiff.diff(src_data, dst_data)
print(diffs)

运行测试

$ /usr/bin/time -f 'Elapsed Time: %e s Max RSS: %M kbytes' python third.py
{'timepicker': {'time_options': {insert: [(7, '7dd')], delete: [7]}}}
Elapsed Time: 1576.30 s Max RSS: 87732 kbytes

运行时间太长了,接近半小时,肯定不能拿给别人用。

Python-仅用标准库

只试了jsondiff这一个第三方库,接下来打算直接参考同事那个SQL的思路,自己只用标准库实现一个。

from typing import Any, List
import json
import os
from dataclasses import dataclass
from collections.abc import MutableSequence, MutableMapping @dataclass
class DiffResult:
path: str
kind: str
left: Any
right: Any def add_path(parent: str, key: str) -> str:
"""将父路径和key name组合成完整的路径字符串"""
if parent == "":
return key
else:
return parent + "." + key def read_json(filepath: str) -> Any:
if not os.path.exists(filepath):
raise FileNotFoundError(filepath)
try:
with open(filepath, "r") as f:
data = json.load(f)
except json.JSONDecodeError as e:
raise Exception(f"{filepath} is not a valid json file") from e
else:
return data def collect_diff(path: str, left: Any, right: Any) -> List[DiffResult]:
"""比较两个json数据结构之间的差异 Args:
path (str): 当前路径
left (Any): 左侧数据
right (Any): 右侧数据 Returns:
List[DiffResult]: 差异列表
"""
diffs: List[DiffResult] = [] if isinstance(left, MutableMapping) and isinstance(right, MutableMapping):
# 处理字典:检查 key 的增删改
all_keys = set(left.keys()) | set(right.keys()) # 左右两边字典中所有键的并集,用于后续比较这些键在两个字典中的存在情况及对应的值
for k in all_keys:
l_exists = k in left
r_exists = k in right
key_path = add_path(path, k) if l_exists and not r_exists: # 如果一个键只存在于left,则记录为 removed 差异
diffs.append(DiffResult(key_path, "removed", left=left[k]))
elif not l_exists and r_exists: # 如果一个键只存在于 right,则记录为 added 差异
diffs.append(DiffResult(key_path, "added", right=right[k]))
else:
# 都存在,递归比较这两个键对应的值
diffs.extend(collect_diff(key_path, left[k], right[k])) elif isinstance(left, MutableSequence) and isinstance(right, MutableSequence):
# 处理列表:按索引比较
max_len = max(len(left), len(right)) # 找两个列表中最长的长度
for i in range(max_len):
l_exists = i < len(left)
r_exists = i < len(right)
idx_path = f"{path}[{i}]" lv = left[i] if l_exists else None
rv = right[i] if r_exists else None if l_exists and not r_exists: # 某个索引的元素只存在于 left,则记录为 removed 差异
diffs.append(DiffResult(idx_path, "removed", left=lv))
elif not l_exists and r_exists: # 某个索引的元素只存在于 right,则记录为 added 差异
diffs.append(DiffResult(idx_path, "added", right=rv))
else: # 都存在,递归比较这两个索引对应的值
diffs.extend(collect_diff(idx_path, lv, rv)) else:
# 基本类型或类型不一致
if left != right:
diffs.append(DiffResult(path, "modified", left=left, right=right)) return diffs if __name__ == "__main__":
src_dict = read_json("src.json")
dst_dict = read_json("dst.json") diffs = collect_diff("", src_dict, dst_dict)
if len(diffs) == 0:
print("No differences found.")
else:
print(f"Found {len(diffs)} differences:")
for diff in diffs:
match diff.kind:
case "added":
print(f"Added: {diff.path}, {diff.right}")
case "removed":
print(f"Removed: {diff.path}, {diff.left}")
case "modified":
print(f"Modified: {diff.path}, {diff.left} -> {diff.right}") # print(diffs)

运行测试

$ /usr/bin/time -f 'Elapsed Time: %e s Max RSS: %M kbytes' python main.py
Found 1 differences:
Modified: timepicker.time_options[7], 7d -> 7dd
Elapsed Time: 0.46 s Max RSS: 87976 kbytes

只要 0.46 秒就能比较出来差异点,单论比较性能来说,比jsondiff要好很多。

Go实现

再换go来实现个命令行工具,同样只需要用标准库即可。

package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
) var (
src_file string
dst_file string
) type DiffResult struct {
Path string
Kind string
Left any
Right any
} func addPath(parent, key string) string {
if parent == "" {
return key
}
return parent + "." + key
} func collectDiff(path string, left, right any) []DiffResult {
var diffs []DiffResult switch l := left.(type) {
case map[string]any:
if r, ok := right.(map[string]any); ok {
for k, lv := range l {
rk, exists := r[k]
if !exists {
diffs = append(diffs, DiffResult{
Path: addPath(path, k),
Kind: "removed",
Left: lv,
Right: nil,
})
} else {
diffs = append(diffs, collectDiff(addPath(path, k), lv, rk)...)
}
} for k, rv := range r {
if _, exists := l[k]; !exists {
diffs = append(diffs, DiffResult{
Path: addPath(path, k),
Kind: "added",
Left: nil,
Right: rv,
})
}
}
} else {
diffs = append(diffs, DiffResult{
Path: path,
Kind: "modified",
Left: left,
Right: right,
})
}
case []any:
if r, ok := right.([]any); ok {
// 比较 slice(这里简化:按索引比较)
maxLen := len(l)
if len(r) > maxLen {
maxLen = len(r)
}
for i := 0; i < maxLen; i++ {
var lv, rv any
var lExists, rExists bool if i < len(l) {
lv = l[i]
lExists = true
}
if i < len(r) {
rv = r[i]
rExists = true
} switch {
case lExists && !rExists:
diffs = append(diffs, DiffResult{
Path: fmt.Sprintf("%s[%d]", path, i),
Kind: "removed",
Left: lv,
Right: nil,
})
case !lExists && rExists:
diffs = append(diffs, DiffResult{
Path: fmt.Sprintf("%s[%d]", path, i),
Kind: "added",
Left: nil,
Right: rv,
})
case lExists && rExists:
diffs = append(diffs, collectDiff(fmt.Sprintf("%s[%d]", path, i), lv, rv)...)
}
}
} else {
diffs = append(diffs, DiffResult{
Path: path,
Kind: "modified",
Left: left,
Right: right,
})
}
default:
if fmt.Sprintf("%v", left) != fmt.Sprintf("%v", right) {
diffs = append(diffs, DiffResult{
Path: path,
Kind: "modified",
Left: left,
Right: right,
})
}
}
return diffs
} func readJSON(r io.Reader) (map[string]any, error) {
var data map[string]any
decoder := json.NewDecoder(r)
if err := decoder.Decode(&data); err != nil {
return nil, err
}
return data, nil
} func main() {
flag.StringVar(&src_file, "src", "src.json", "source file")
flag.StringVar(&dst_file, "dst", "dst.json", "destination file")
flag.Parse()
srcFile, err := os.Open(src_file)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening src.json: %v\n", err)
return
}
defer srcFile.Close() dstFile, err := os.Open(dst_file)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening dst.json: %v\n", err)
return
}
defer dstFile.Close() srcJson, err := readJSON(srcFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading src.json: %v\n", err)
return
}
dstJson, err := readJSON(dstFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading dst.json: %v\n", err)
return
}
diffs := collectDiff("", srcJson, dstJson)
if len(diffs) == 0 {
fmt.Println("No differences found.")
} else {
fmt.Printf("%d differences found:\n", len(diffs))
for _, diff := range diffs {
switch diff.Kind {
case "added":
fmt.Printf("Added: %s: %v\n", diff.Path, diff.Right)
case "removed":
fmt.Printf("Removed: %s: %v\n", diff.Path, diff.Left)
case "modified":
fmt.Printf("Modified: %s: %v -> %v\n", diff.Path, diff.Left, diff.Right)
}
}
}
}

运行测试,速度同样很快。

$ /usr/bin/time -f 'Elapsed Time: %e s Max RSS: %Mkbytes' ./diffjson -src ./src.json -dst ./dst.json
1 differences found:
Modified: timepicker.time_options[7]: 7d -> 7dd
Elapsed Time: 0.29 s Max RSS: 117468 kbytes

[Python][Go]比较两个JSON文件之间的差异的更多相关文章

  1. [Spark][python]以DataFrame方式打开Json文件的例子

    [Spark][python]以DataFrame方式打开Json文件的例子: [training@localhost ~]$ cat people.json{"name":&qu ...

  2. 【Linux】找出文件之间的差异

    使用命令comm可以找出2个文件之间的差异 现在有文件如下: Linux:/qinys # cat A.txt apple lemon onion orange pear Linux:/qinys # ...

  3. python webdriver 测试框架-数据驱动json文件驱动的方式

    数据驱动json文件的方式 test_data_list.json: [ "邓肯||蒂姆", "乔丹||迈克尔", "库里||斯蒂芬", & ...

  4. Python【8】-分析json文件

    一.本节用到的基础知识 1.逐行读取文件 for line in open('E:\Demo\python\json.txt'): print line 2.解析json字符串 Python中有一些内 ...

  5. 两个js文件之间函数互调问题

    按照常理来说,在<body>标签结束之前以下面的方式引入两个js文件 <script src="a.js"></script> <scri ...

  6. python 实现excel转化成json文件

    1.准备工作 python 2.7 安装 安装xlrd -- pip install xlrd 2. 直接上代码 import xlrd from collections import Ordered ...

  7. Json文件解析(下)

    Json文件解析(下) 代码地址:https://github.com/nlohmann/json   从STL容器转换 任何序列容器(std::array,std::vector,std::dequ ...

  8. LitJson(读Exce文件写入到json文件):

    读Exce文件写入到json文件汇总: //命名空间 using System.Collections; using System.Collections.Generic; using System. ...

  9. Python dict(或对象)与json之间的互相转化

    Python dict(或对象)与json之间的互相转化 原文转载自 1.JSON:JavaScript 对象表示法,是轻量级的文本数据交换格式,独立于语言,平台 2.JSON 语法规则 数据在名称/ ...

  10. python解析json文件之简介

    一.JSON简介 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.它基于JavaScript(Standard ECMA-262 3rd Edition ...

随机推荐

  1. 企业AI应用模式解析:从本地部署到混合架构

    在人工智能快速发展的今天,企业如何选择合适的大模型应用方式成为了一个关键问题.本文将详细介绍六种主流的企业AI应用模式,帮助您根据自身需求做出最优选择. 1. 本地部署(On-Premise Depl ...

  2. SparkSQL编程需注意的细节

    SparkSQL是把Hive转为字符串后,以参数形式传递到SparkSession.builder().enableHiveSupport().getOrcCreate().sql(Hive_Stri ...

  3. 网络安全中windows系统常用指令

    windows系统常用指令 dir 显示指定目录下的文件和文件夹列表 cd 更改当前工作目录,cd .. 返回上一级目录 cls 清除命令行窗口的内容 echo 在命令提示符(CMD)中输出文本或显示 ...

  4. AI Infra 运维实践:DeepSeek 部署运维中的软硬结合

    ​发布会资料 <AI Infra运维实践:DeepSeek部署运维中的软硬结合> 袋鼠云运维服务 1.行业痛点 随着数字化转型的深入,企业面临的运维挑战日益复杂,所依托的平台在长期使用的过 ...

  5. 四、Linux系统调用跟踪工具strace

    4.1.strace(系统调用跟踪) ​ strace 是 Linux 下的系统调用跟踪工具,可以监视进程执行时与内核的交互,包括文件操作.进程管理.网络通信.内存分配等.它主要用于调试.性能优化.问 ...

  6. LiteLLM - 统一接口调用100+ LLM模型

    :bullet_train: LiteLLM LiteLLM 是一个统一的接口层,支持调用100+种大语言模型(LLM),包括Bedrock.Huggingface.VertexAI.Together ...

  7. CF958E1 题解

    Problem 原题链接 Meaning 在二维平面内,有位置不同且不存在三点共线的 \(R\) 个红点和 \(B\) 个黑点,判断是否能用一些互不相交的线段连接每一个点,使得每条线段的两端都分别是黑 ...

  8. R Studio操作技巧笔记

    快捷键 Ctrl+Shift+C 注释快捷键,可以添加/消除注释,也可多行注释 Ctrl + Shift + Enter 执行整个文件 Ctrl+Enter 运行当前/被选中的代码 Ctrl+L 清空 ...

  9. openWrt使用rclone挂载webDav

    前言 觉得路由器(linux)硬盘太小,又不好扩展(x86机器可以插硬盘.但arm机器的硬盘是焊死的无法扩展). 这个时候,我们可以通过davfs或者rclone将外部资源如webDav挂载到本机上用 ...

  10. Let’s Encrypt申请证书

    前提 安装好ngixn,并配置解析好你的域名,仅仅留下证书配置处不填写即可. 安装certbot certbot 官方推荐的自动化脚本, 用来申请免费SSL证书的. (certbot中文翻译是 证书机 ...