跳到主内容
d.devtul.fun
EN
工作流 · 2026-08-24

如何在本地对比大体积 JSON 文件

对比两个大 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 方案同时保住了你的数据和内存。

继续阅读