使用方法
- 打开页面时两侧已经填好同一份接口文档的旧版与新版,结果区直接给出比对结果,不用先点按钮。左侧「旧文本」放改动前的内容,右侧「新文本」放改动后的内容。
- 「比对粒度」默认选逐行比对:以换行符为界把文本切成行,一行对一行地找增删,适合代码、日志、配置文件这类一行一条记录的内容。
- 短文本(比如一句话的修辞改动)可以切成逐字符比对,它按字素逐个比较,会把改动定位到具体哪几个字,而不是笼统地标成「整行改了」。
- 「保留上下文行数」默认 2:改动行上下各留 2 行未改动的内容作参照,中间跳过的部分折叠成一行省略号;填 0 就只看改动行。
- 「忽略行尾空白」用于对付编辑器自动去掉行尾空格造成的假改动;「忽略大小写」用于对付变量名大小写被统一的情况。两者都只影响比较时的判定,显示出来的仍是原文。
- 点「开始比对」(或在任一文本框里按 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 行。实测结果:
- 未改动 3 行:第 1 行的标题、第 3 行的请求方法、第 5 行的返回说明;
- 新增 4 行:新版把
/api/user/login改成/api/v2/user/login、参数里加了captcha、超时从 30 秒改成 60 秒,并多了最后一行备注; - 删除 3 行:对应的旧地址、旧参数、旧超时各删掉一行;
- 编辑距离
D = 4 + 3 = 7,行相似度3 ÷ 7 = 42.9%; - 这个规模下 Myers 的上界是
(6 + 7) × 7 = 91次运算,而 LCS 动态规划要填6 × 7 = 42格——小文本上两者都不值一提,差距要到上万行才显出来。
注意「修改一行」在 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) |
|---|---|---|---|---|
| 100 | 10,000 | 0.02 MB | 2,000 | 200,000 |
| 500 | 250,000 | 0.48 MB | 10,000 | 1,000,000 |
| 1,000 | 1,000,000 | 1.91 MB | 20,000 | 2,000,000 |
| 2,000 | 4,000,000 | 7.63 MB | 40,000 | 4,000,000 |
| 3,000 | 9,000,000 | 17.17 MB | 60,000 | 6,000,000 |
| 5,000(本页上限) | 25,000,000 | 47.68 MB | 100,000 | 10,000,000 |
| 8,000 | 64,000,000 | 122.07 MB | 160,000 | 16,000,000 |
| 10,000 | 100,000,000 | 190.73 MB | 200,000 | 20,000,000 |
| 20,000 | 400,000,000 | 762.94 MB | 400,000 | 40,000,000 |
| 50,000 | 2,500,000,000 | 4.66 GB | 1,000,000 | 100,000,000 |
| 100,000 | 10,000,000,000 | 18.63 GB | 2,000,000 | 200,000,000 |
| 200,000 | 40,000,000,000 | 74.51 GB | 4,000,000 | 400,000,000 |
| 500,000 | 250,000,000,000 | 465.66 GB | 10,000,000 | 1,000,000,000 |
| 1,000,000 | 1,000,000,000,000 | 1862.65 GB | 20,000,000 | 2,000,000,000 |
| 2,000,000 | 4,000,000,000,000 | 7450.58 GB | 40,000,000 | 4,000,000,000 |
| 5,000,000 | 25,000,000,000,000 | 46566.13 GB | 100,000,000 | 10,000,000,000 |
看第 8 行与第 6 行的对照:同样是「两篇各一万行」,如果实际只改了 10 处,Myers 只要 20 万次运算,而 LCS 要先开出 1 亿格、占 190.73 MB。差距不是几个百分点,而是三个数量级。
边界与精度说明
- 每侧 5000 行是硬上限。超出的部分不会被比对,页面会在提示区写明「只比对了前 5000 行」。要处理更大的文件,先用 格式化 或按模块切分,再分段比对。
- 行尾统一成 LF,且这一步不可关。它是无条件执行的,没有开关——关掉只会让两段内容相同的文本被报成完全不同,没有使用价值。
- 空白差异只在你勾选时才忽略。「忽略行尾空白」只去掉每行末尾的空格和制表符,行首的缩进差异仍然算改动;制表符与空格在行内混用也仍算改动。
- 「忽略大小写」只影响判定,不改显示。开启后
Token与token被视为同一行,但结果区显示的仍是你原本输入的写法,方便核对真实内容。 - 字素切分只在「逐字符比对」下生效。逐行比对以换行为界,行内是一个整体,不做字素拆分。
- 相似度是「未改动行数 ÷ 两侧中较多的行数」。它是一个粗略指标,不衡量字符级相似度:一行里改了一个字,也算这一行整体未变还是变更?本页按「变更」计,所以相似度只会偏低、不会虚高。
- 本工具不产生补丁文件。「复制差异结果」复制的是带
+/-前缀的纯文本,不是 git 能直接apply的 unified diff。
常见问题
为什么两段明明一模一样的文字,比出来每一行都不一样?
九成是行尾不统一。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 行并在提示区说明。