← 返回博客首页

文本差异算法浅析:如何读懂你的 diff 输出

先回答『diff 为什么不聪明』

「我明明只是把第 3 行的代码挪到第 9 行,为什么 git diff 把两段都标红了?」——这句吐槽背后,是 diff 算法根本不打算懂『移动』,它只求一件事:

用最少的编辑步骤(删除、插入),把旧文本变成新文本。

一旦理解了它的目标函数,绝大多数『反直觉』输出都有了解释。

网格模型:所有的 diff 都是一条路径

把旧文本放在横轴、新文本放在纵轴:

新文本 y
  ↑
  … ○──○──●   (对角线 = 字符相同,免费)
  …
  • 向右一步 → 删除旧文本一个字符;
  • 向上一步 → 插入新文本一个字符;
  • 对角线(两端字符相同)→ 免费,不算步数。

从起点到终点,每一条走法对应一种『把旧文本变到新文本』的编辑方式。diff 要的就是『总步数最少的走法』——也就是尽可能多走免费对角线,让改动集中到必须变的地方。

Myers:用斜线编号逼近最短路径

Myers 算法用每条网格斜线(diagonal,编号 k)上「走了几步」来追踪进度,逐轮扩展 k 值:

  1. 每轮尝试走到所有可及的斜线;
  2. 优先扩展能到达的『最大同行步数』,也就是把免费对角线走到头;
  3. 当某一步到达终点,这条路径就是编辑步数最短的解。

它的精妙在于:同样优劣时,Myers 倾向于统一:少插入或少删除较集中,因而 diff 输出通常相对紧凑、可读。这也是 git diff、GNU diff 等默认实现它的原因。

(细节上 Myers 走的是「d 步数」层推进而非严格 k;这里用「尽可能多吃免费对角线」的口径讲透直觉即可。)

行级 vs 字符级:取舍

维度 行级 diff 字符级 diff
观察粒度 行增删 行内单词/字符
适用 代码评审、配置对比 长行内参数改动
噪音 高(大文件满屏标红)
JSON 这类数据 会误判为整行改 语义化更好(用专门的 JSON diff)

建议:先看行级抓住结构,再对关心的行开字符级高亮。数据处理场景尽量用结构化 diff。

读懂一次真实 diff

假设某个配置把 timeout 从 30 提到 60,并挪了注释位置,git diff --no-index a.txt b.txt 输出可能是:

--- timeout: 30
+++ timeout: 60
@@ -1,5 +1,5 @@
  server { listen 80 }
- # TODO 调优
- timeout: 30
+ timeout: 60
  • @@ -1,5 +1,5 @@:上下文块的范围;
  • - 行旧文本独有,+ 行新文本独有;
  • 相同行保持不动——算法在组件周围尽量保住了不变上下文。

别急着以为「注释被删了」:那是因为最短路径里,删注释与调整时间在「编辑距离」视角下比『保留不动 + 移动位置』这个假想操作更划算。

练习:让直觉落地

拿两段带小改动的 C 代码/配置,用在线文本差异工具(或本地 diff -u)跑一遍:

  1. 观察 @@ 上下文范围怎么定;
  2. 故意把一大块内容上下移动,看它如何变成「全删 + 全加」;
  3. 再用 JSON 差异工具比较两份 JSON——对比行级 diff 与专业 JSON diff 在「识别数组位移」上的差别。

当你能预判「这段大概率会被 diff 标成全删全加」,说明你已经读懂了它的编辑距离视角。

小结

diff 不笨,只是它的目标函数是最少编辑步数 + 多吃免费对角线,严谨、确定、可复现。读懂它,你就能从一遍遍翻 commit 的疑惑中走出来:不是内容变了,而是编辑路径变了。

常见问题

diff 输出的『+』『-』是怎么决定的?

diff 的目标是找出**从原文本到新文本的最少编辑操作**(插入/删除/替换按需组合)。逐行比对时:某行只在新文本出现打 `+`(插入),只在旧文本出现打 `-`(删除),两边都一样且在最短路径上则不动。看起来『明明只是挪了个位置却整段显示增删』,是因为算法追求的是**最小代价的编辑序列**,不一定符合人类的『移动』直觉——Myers 算法就是为此权衡了步数与插入/删除的数量。

Myers diff 的『对角线』是什么意思?

把原文本列在 x 轴、新文本列在 y 轴构一张网格,从左下起点走到右上终点。**向右走 = 删除旧字符,向上走 = 插入新字符,沿对角线走 = 字符相同(免费)**。Myers 算法的核心:用『走了几步的斜线(k 值)』描述前进位置,逐轮扩展 k 值,第一个到达终点的路径就是**编辑步数最少**的方案。听懂了『免费的对角线要多走』,就能理解为什么 diff 会优先长出大段连续不变的内容、而把改动收敛到最小区域。

什么时候该用行级 diff,什么时候用字符级 diff?

行级 diff(`-U` 上下文)足够表达「哪些行增删、整体结构怎么变」,是代码评审、配置文件比对的默认选择,噪音低、可读性好。字符**级** diff(word/char-level,很多可视化工具支持)能把一行内的改动精确到单词或字符,适合检查「长行里某个参数值改了没」。建议:结构变化看行级,行内细微改动用字符级高亮。JSON 这类结构化数据用专门的「JSON diff」更能语义化区分增删改与数组位移。

为什么两边相同内容 multi 行长,有时 diff 仍认为全删全加?

这通常不是 diff 写错了,而是**相同性判定**只在相邻/相同的对齐候选路径上发生。当两版大块内容顺序颠倒或嵌套层级变化,最小编辑序列里这些块不会同时出现在同一条最短路径上,于是被拆成『旧消失、新出现』两段,即使字符完全一样。结论:`git diff` 看的是『编辑距离』不是『内容相似度』。要应对移动,可配合 move-detection 工具或在 JSON diff 里识别数组位移。

← 返回博客首页