文本差异检测

把两段纯文本分别拆成行,逐行比对出哪些行是新增、哪些行被删除,用颜色标出来并给出增删统计。行尾 CRLF 与 LF 会先统一,避免每一行都被误判成改动。

新增行4
删除行3
未变行3
行相似度42.9%
用户登录接口说明
- 请求地址:/api/user/login
+ 请求地址:/api/v2/user/login
请求方法:POST
- 参数:username, password
+ 参数:username, password, captcha
返回:token 与过期时间
- 超时时间:30 秒
+ 超时时间:60 秒
+ 备注:验证码 5 分钟有效

提示:文本框里按 Ctrl + Enter(macOS 用 Cmd + Enter)立即比对,Enter 留给换行;两侧文本框各自的换行互不影响。结果区上方的统计卡与正文示例的数字一致。

使用方法

  1. 打开页面时两侧已经填好同一份接口文档的旧版与新版,结果区直接给出比对结果,不用先点按钮。左侧「旧文本」放改动前的内容,右侧「新文本」放改动后的内容。
  2. 「比对粒度」默认选逐行比对:以换行符为界把文本切成行,一行对一行地找增删,适合代码、日志、配置文件这类一行一条记录的内容。
  3. 短文本(比如一句话的修辞改动)可以切成逐字符比对,它按字素逐个比较,会把改动定位到具体哪几个字,而不是笼统地标成「整行改了」。
  4. 「保留上下文行数」默认 2:改动行上下各留 2 行未改动的内容作参照,中间跳过的部分折叠成一行省略号;填 0 就只看改动行。
  5. 「忽略行尾空白」用于对付编辑器自动去掉行尾空格造成的假改动;「忽略大小写」用于对付变量名大小写被统一的情况。两者都只影响比较时的判定,显示出来的仍是原文。
  6. 点「开始比对」(或在任一文本框里按 Ctrl + Enter)刷新结果;点「交换两侧」把新旧对调,方便确认某一行到底是加进来还是删掉的。

这个文本差异检测能解决什么问题

一是核对两版配置到底改了哪几行。运维改完 nginx 或应用配置后,把改动前后的文件各贴一次,比肉眼从头到尾读一遍可靠得多,也不会漏掉只差一个字符的那种改动。

二是确认接口字段有没有被悄悄加删。前后端联调时字段名差一个字母,或者有人顺手多返回一个 captcha,靠人工比对很容易滑过去;逐行比对会把新增那一行单独标绿。

三是排查「看起来一样」的两段文字为什么程序判不相等。这类问题九成出在行尾:从 Windows 复制来的文本每行尾多一个 \r,或者行尾多了几个看不见的空格。文本差异检测会先把行尾统一成 \n 再比,一测就知道是不是这个原因。

四是把改动记录成一份可粘贴的清单。点「复制差异结果」拿到的是带 + / - 前缀的纯文本,可以直接贴进工单、提交说明或聊天记录里。

本页不做这些事:不做三方合并、不生成补丁文件(patch)、不比较两个文件夹、不读取本地文件。它只回答一个问题——这两段文本之间,多了哪几行、少了哪几行。

比对原理:先归一化行尾,再用 Myers 差分

整页只做三件事:切分、求最短编辑脚本、上色渲染。真正有讲究的是中间那一步。

第一步必须先归一化行尾,否则会全军覆没。不同系统给换行打的标记不一样:Windows 是 CRLF(两个字符:回车 \r 加换行 \n),Linux 与 macOS 是单个 LF,老式 Mac 是单个 CR。假设你手上有两段完全一样的 5 行文字,一段来自 Windows、一段来自 Linux,不归一化就直接按行比,每一行的行尾都差一个 \r,于是5 行全被判为改动;实测 1000 行的文件会凭空多出 1000 个 \r,也就是 1000 行全部「不同」。本页在任何比较之前先执行 text.replace(/\r\n?/g, '\n'),把 CRLF 和落单的 CR 统统折成 LF,再切行。

第二步是求最短编辑脚本,本页用 Myers 差分。求「把旧文本变成新文本最少要删几行、加几行」,本质是找两个序列的最长公共子序列(LCS)。最直白的做法是动态规划:开一张 n × m 的表,逐格填数。它的复杂度是 O(n × m)——两边越长就越慢,而且慢得是平方级的。两篇各 10000 行的文本,表就有 1 亿格;用 16 位整数存这一张表要 190.7 MB,还没算填表的时间。而参考实现里 Myers 的算法复杂度是 O((N + M) × D),其中 D 是编辑距离(删的行数加上加的行数),与文本长度不成平方关系,只与实际改动了多少有关。同样 10000 对 10000 行,如果只改了 10 处,Myers 的上界是 (10000 + 10000) × 10 = 200000 次运算,比 LCS 的 1 亿格快了约 500 倍;反过来若两篇几乎完全不同,D 逼近 N + M,Myers 也会退化到与 LCS 同量级。这正是 git 这类工具内部选 Myers 的原因:真实改动通常是稀疏的。

第三步,逐行还是逐字符,取决于你关心什么。逐行比对把「一个字符不同」放大成「整行删除 + 整行新增」,读数更清爽,适合结构化文本;但如果两段是自然语言的一段话,整行都会被标成改动,信息量为零。这时切到逐字符比对更有用。逐字符的关键是不能按 UTF-16 码元切:一个「家庭」表情 👨‍👩‍👧‍👦 由 4 个表情加 3 个零宽连接符组成,在 UTF-16 里占 11 个码元,硬切会把它劈成好几段,渲染出乱码方块。本页用 Intl.Segmenter 按字素(grapheme)切分,上面那个表情会被当成一个整体;一面国旗 🇨🇳 占 4 个码元,同样只算 1 个字素。

为什么给行数上限。Myers 虽然快,但它的记录表开销随 D 增长(要保存每一步的 V 快照),两侧行数越多、差异越碎,内存占用越高。这个文本差异检测把每侧限制在 5000 行,超出的部分只比对前 5000 行并在提示区写明,而不是让页面卡住;编辑距离 D 超过 4000 时也会中止行级比对并提示改用更细的粒度。

完整示例(数值仅作演示)

示例条件与页面打开时完全一致:左侧 6 行旧文档、右侧 7 行新文档、逐行比对、上下文 2 行。实测结果:

注意「修改一行」在 diff 里的表示是删一行 + 加一行,所以三处修改贡献了 3 次删除和 3 次新增,再加上末尾纯新增的 1 行,就是上面的 4 加 3。

行数规模、LCS 与 Myers 对照表

下表两列「上界」都在同一输入下计算:LCS 一列是动态规划表格的格数 n × n(内存按 16 位整数折算),Myers 两列分别按编辑距离 D = 10 与 D = 1000 估算 (N + M) × D。数字能直接说明「为什么不能拿 LCS 去比大文件」。

每侧行数LCS 表格规模(格)LCS 整表内存Myers 上界(D=10)Myers 上界(D=1000)
10010,0000.02 MB2,000200,000
500250,0000.48 MB10,0001,000,000
1,0001,000,0001.91 MB20,0002,000,000
2,0004,000,0007.63 MB40,0004,000,000
3,0009,000,00017.17 MB60,0006,000,000
5,000(本页上限)25,000,00047.68 MB100,00010,000,000
8,00064,000,000122.07 MB160,00016,000,000
10,000100,000,000190.73 MB200,00020,000,000
20,000400,000,000762.94 MB400,00040,000,000
50,0002,500,000,0004.66 GB1,000,000100,000,000
100,00010,000,000,00018.63 GB2,000,000200,000,000
200,00040,000,000,00074.51 GB4,000,000400,000,000
500,000250,000,000,000465.66 GB10,000,0001,000,000,000
1,000,0001,000,000,000,0001862.65 GB20,000,0002,000,000,000
2,000,0004,000,000,000,0007450.58 GB40,000,0004,000,000,000
5,000,00025,000,000,000,00046566.13 GB100,000,00010,000,000,000

看第 8 行与第 6 行的对照:同样是「两篇各一万行」,如果实际只改了 10 处,Myers 只要 20 万次运算,而 LCS 要先开出 1 亿格、占 190.73 MB。差距不是几个百分点,而是三个数量级。

边界与精度说明

常见问题

为什么两段明明一模一样的文字,比出来每一行都不一样?

九成是行尾不统一。Windows 的行尾是 \r\n,Linux 是 \n;不归一化时,每一行末尾都差一个 \r,于是 1000 行会被报成 1000 处改动。本页在比对前已用 text.replace(/\r\n?/g, '\n') 统一,所以这种假改动不会出现。如果你看到的是行首差异,多半是缩进用了制表符与空格混排。

「修改一行」为什么统计成一次新增加一次删除?

因为差分算法只知道「这一行在旧文本里、那一行在新文本里」,它没有「修改」这个动作,只能表达成删掉旧行、加入新行。示例里三处修改因此记成 3 次删除 + 3 次新增,再加上末尾那次纯新增的 1 行,结果是新增 4 行、删除 3 行。这也是所有 diff 工具的通行口径。

逐行和逐字符到底该选哪个?

看内容形态:代码、日志、CSV 这类一行一条记录的结构化文本选逐行。改动会精确到行,读数清爽;一整段自然语言、或者要看清「到底改了哪几个字」时选逐字符。逐字符把换行也当成一个字符参与比较,所以适合短文本;很长的一段话用逐字符会让结果区非常长。

为什么不能拿 LCS 动态规划直接比大文件?

因为它是 O(n × m)。两篇各一万行就要开一亿格,按 16 位整数算内存已到 190.73 MB,填表时间也随规模平方增长。本页用的是 Myers 的 O((N + M) × D) 方案,复杂度只和实际改动量 D 有关,同样一万对一万、只改 10 处时只要 20 万次运算。只有当两篇几乎完全不同、D 逼近总行数时,两者才差不多。

表情符号会被拆坏吗?

逐行比对不会,因为一行整体作为一个单位比较。逐字符比对做了保护:用 Intl.Segmenter 按字素切分。一个肤色的点赞表情 👍🏽 在 UTF-16 里占 4 个码元、一个家庭表情 👨‍👩‍👧‍👦 占 11 个码元,按码元硬切会劈成好几段显示成方块;按字素切分后它们各自只算 1 个字符。

「忽略行尾空白」会不会把有意义的空行也吃掉?

不会。它只去掉每行末尾的空格与制表符,空行仍然是空行,行数不变。它唯一的作用是抹掉「一个空格都没有的行尾」与「多敲了几个空格的行尾」之间的差别,这类差异在编辑器里几乎看不见,却会让比对结果多出一堆噪声。

结果区里的「…」是什么?

是折叠掉的未改动行。默认保留改动行上下各 2 行作参照,中间的连续未改动内容被折成一行占位,避免一段长文本里只有两处改动却要把全文都铺出来。把「保留上下文行数」填 0 就只看改动行,填大一点就多看几行前后文。

我的文本会被上传吗?有长度限制吗?

不会上传。切分、求差分、上色渲染全在当前页面完成,没有任何网络请求,也不写浏览器存储。想自己验证,打开开发者工具的 Network 面板,点几次「开始比对」即可。限制只有一条:每侧最多 5000 行,超出部分只比对前 5000 行并在提示区说明。

相关工具