对比两个大 JSON 文件,和对比两段短文本不是一回事。文件一旦上了几 MB,最顺手的动作 —— 两份都粘进网页工具 —— 就失效了:标签页卡死,你还顺手把可能敏感的数据送到了第三方服务器。这篇讲的就是怎么在你自己的机器上、免费、正确地做这件事。
为什么不该用在线工具
两个原因,第二个是致命的:
- 隐私。JSON 导出正是 access token、用户记录、内部 schema 藏身之处。把它们上传到某个随机站点,泄露就是从这里开始的。
- 规模。多数浏览器里的 diff 界面对几 MB 就扛不住了。它们是给"粘贴"设计的,不是给"载荷"设计的。
下面所有操作都在本地跑。本站的 文本对比 和 JSON 查看器 都是纯前端,所以即便走浏览器这条路,数据也留在设备上。
对比前先归一化
假差异最大的来源是格式,不是内容。两份文件可能描述一模一样的数据,却因为键顺序、缩进或换行不同而不同。对这样的文件做文本 diff,产生的噪声会把真正的改动埋了。
# 排序键并美化输出两个文件,再当文本比
jq -S . a.json > a.norm.json
jq -S . b.json > b.norm.json
diff a.norm.json b.norm.json
jq -S(sort keys)会用稳定顺序重新输出对象。之后一次文本 diff 就只剩真正的内容差异。这一步单独就解决了大部分的"为什么全都不一样"问题。
文本 diff 与结构化 diff
考虑两份键顺序不同、但数据相同的文件:
// a.json
{"name": "x", "id": 1}
// b.json
{"id": 1, "name": "x"}
文本 diff 会把整行标成改了,尽管语义上什么都没变。结构化对比会把两份都解析成对象再比数值,报告"相等"。所以规则是:先归一化、再用文本 diff 求一个快答;当键顺序不可靠时,用结构化对比。
要不要忽略数组顺序
JSON 里数组是有序的,所以 [1,2,3] 和 [3,2,1] 在文本上和语义上都不一样。有时顺序对你毫无意义 —— 比如一个标签列表。要按情况决定:
- 如果顺序有意义(一串事件),保留它,让 diff 把重排显示出来。
- 如果顺序只是噪声,就在归一化时给数组排序,使对比忽略它。
一段做语义对比的 Python 脚本
要真正结构感知地比较 —— 包括按 id 对齐数组元素而非按位置 —— 一小段脚本胜过任何文本工具。下面这段会递归并报告不同的路径:
import json
def load(p):
with open(p, encoding="utf-8") as f:
return json.load(f)
def diff(a, b, path=""):
if type(a) != type(b):
print(f"{path}: 类型 {type(a).__name__} != {type(b).__name__}")
return
if isinstance(a, dict):
for k in set(a) | set(b):
diff(a.get(k), b.get(k), f"{path}.{k}")
elif isinstance(a, list):
if len(a) != len(b):
print(f"{path}: 长度 {len(a)} != {len(b)}")
for i, (x, y) in enumerate(zip(a, b)):
diff(x, y, f"{path}[{i}]")
elif a != b:
print(f"{path}: {a!r} != {b!r}")
diff(load("a.json"), load("b.json"))
若想按 id 而非位置对齐数组元素,把 list 分支换成先按每个元素的 id 字段建索引再递归。这能把"第 4 个元素被挪动了"变成真正的匹配。
超大文件的内存友好做法
文件大到没法整段加载时,用流式解析器。ijson 一次产出一个条目,内存保持平稳:
import ijson
with open("huge.json", encoding="utf-8") as f:
for item in ijson.items(f, "item"):
process(item) # 逐条对比
对很多顶层记录,流式读两个文件、边读边比,而不是建两个巨大的列表。很多工具也提供按行处理 JSON 的 --stream 模式,原则相同 —— 内存里同时最多只留一条记录。
把 diff 导出成可读报告
原始的路径式输出很难读。把差异收集进列表,再写个小摘要:
report = []
def diff(a, b, path=""):
...
else:
report.append((path, a, b))
diff(load("a.json"), load("b.json"))
with open("report.txt", "w", encoding="utf-8") as out:
for path, x, y in report:
out.write(f"{path}: {x!r} -> {y!r}\n")
print(f"{len(report)} 处差异已写入 report.txt")
上面那个 -> 是为了在代码块里存活而做的转义箭头;你自己的脚本里直接写字面 -> 或干脆用个普通横杠都行。
常见性能陷阱
- 没归一化就 diff。键顺序和空白噪声会膨胀差异、掩盖真改动。永远先
jq -S。 - 把全部内容读进内存。500 MB 的文件当成一个 Python 对象,可能撑爆内存;用流式。
- 美化输出超大结果。美化后的 JSON 体积可能涨 2–3 倍;去比紧凑且排好序的版本,别去比重新格式化的。
- 重复解析。每个文件只解析一次、复用对象;每次对比都重读纯属浪费。
串起来
可靠的流程是:用 jq -S 排序键、文本 diff 求快判,当顺序或数组误导了文本视图时,再上一段结构化小脚本。日常文件用 文本对比 在浏览器里就能搞定、且一个字节都不上传;真到了巨型导出,上面那个流式 Python 方案同时保住了你的数据和内存。