文本对比结果全是差异?换行符 CRLF、行尾空格和不可见字符排查
文本对比比的是字符。两行看起来一样却被标成”已修改”,几乎都是因为其中有一个你看不见的字符不同。每一类不可见差异都有各自的特征,其中一部分连本页下方的浏览器版对比工具也不会标出来,所以先弄清楚哪些能看见、哪些看不见。
整个文件每一行都变了:换行符 CRLF 与 LF
症状:对比结果把全部行都标成”删除再新增”,可是并排读一遍,内容并没有变。原因多半是换行符不同:Windows 下的工具写出 \r\n(CRLF),Unix 和 macOS 写出 \n(LF),按字节比较时,每一行的末尾都不一样。常见的触发场景是:换了编辑器、从 Windows 机器上复制了文件,或者 git 的 core.autocrlf 设置在检出或提交时转换了行尾。
确认文件用的是哪种换行符,有几个办法:
file notes.txt # 有 CRLF 时会显示 "with CRLF line terminators"
git ls-files --eol # i/ 是暂存区,w/ 是工作区,例如 i/lf w/crlf
grep -c $'\r' notes.txt # 含回车符的行数
在 git 里,git diff --ignore-cr-at-eol 可以忽略这类差异;在仓库里加一行 .gitattributes,例如 * text=auto,让提交时统一行尾,问题就不会反复出现。
这里要特别说明本站”文本对比”工具的一个特点:它把 CRLF、单独的 CR 和 LF 都当作换行来拆行,所以纯换行符差异不会被标出来,这是有意的设计。如果两个文件只有换行符不同,工具会报告”没有发现差异”。这对”内容有没有变”这个问题是正确答案,但回答不了”为什么我的另一个工具说它们不一样”。所以换行符问题用上面的命令查,内容问题才用这个工具查。
一个文件内部还可能混着两种换行符(有人在 LF 文件里手工粘贴了一段 Windows 文本),这种情况用 grep -c 数出来的行数,会和文件总行数对不上。
某几行的差别在行尾空格或制表符
不可见的行尾空白是第二大噪音来源:一行末尾多了一个空格,或者一边用制表符、另一边用空格。文本对比工具会把行尾空格画成 ·,把制表符画成 →,所以”看起来没变却被标记”的行,会在末尾或中间露出这些标记。行内的制表符在任何位置都会被标出;行首的空格不会画出来,所以缩进用制表符还是空格的差别,会表现为只有一侧出现 →。
勾选”忽略行尾空白”后,工具会在比较之前去掉每行末尾的空格和制表符,别的什么都不改。行尾空白被忽略,但仍然会画出来,让你看到本来会被标记的是哪些行。这个选项会记在你的浏览器里。git 里相应的参数是 --ignore-space-at-eol,行为与它一致;-w 会忽略所有空白,对 Python、YAML 这类缩进有含义的文件来说范围太大,不建议默认使用。
最后一行:文件末尾有没有换行
以 ...end 结尾的文件,和以 ...end\n 结尾的文件,行内容相同,却不是同一个文件。git 会用 \ No newline at end of file 提示这一点;本站工具在一侧以换行结尾、另一侧没有时,也会在状态栏给出提示。由于两边的行列表是相同的,行差异里不会有任何变化,这条提示是唯一的线索。如果这时点”复制差异”,复制出的文本末尾同样会带上 \ No newline at end of file 这一行。常见原因是编辑器保存时自动加上或去掉了结尾换行。
看起来一样、其实是另一个字符
如果没有出现 · 或 →,行却还是被标记,就要怀疑那些显示为空白、显示为普通空格,或者长得像某个 ASCII 符号的字符:
| 字符 | 码点 | 常见来源 |
|---|---|---|
| 不换行空格 | U+00A0 | 网页、Word、部分聊天软件 |
| 零宽空格 | U+200B | 从网页或聊天记录复制 |
| 字节顺序标记 BOM | U+FEFF | 保存为”带 BOM 的 UTF-8”,位于第一个字符之前 |
| 全角空格 | U+3000 | 中文、日文输入法 |
| 全角逗号和句号 | U+FF0C、U+3002 | , 和 。 在很多字体里和 ,、. 很像 |
只有第 1 行不一样,而且怎么看都一样,多半是 BOM。要确认某个可疑字符是什么,可以把它粘贴到浏览器控制台里读它的码点:"a b".charCodeAt(1).toString(16) 对不换行空格会输出 a0。带重音的字母也可能在视觉上相同而内部不同:é 可以是单个字符 U+00E9,也可以是 e 加组合符号 U+0301,用 "é".normalize("NFC") 做一次规范化,两者就相等了。macOS 的文件名是第二种形式的常见来源。
中文文档里最常踩的是标点:一份稿子里混用了全角 () 和半角 (),或者全角空格和半角空格,肉眼几乎分辨不出,对比时每一处都是差异。把这类问题当成”格式统一”问题去处理,而不是逐行修改。
为什么只改一个字,整行都被标出来
文本对比工具是按行比较的。它不会在行内高亮具体改动的那个词,所以改了一个逗号和重写整句话,在结果里看起来一样,没有改动的那部分文字会同时出现在被删除和被新增的两行里。对写文章的人来说,这一点影响很大:一段话如果为了换宽度被重新折行,每一行都变了,即使一个字都没有挪。
对比文章之前,可以先让两边都做到”一句话一行”,或者都展开成相同宽度。这样对比结果指向的就是真正改过的那一句。
被移动的段落会显示成”一处删除、另一处新增”,按行比较没有”移动”的概念。同一对文本,也可能存在不止一种同样正确的对齐方式,因为最短的编辑方案不唯一。工具给出的是一种有效的最短方案,不是唯一的一种,所以在含大量重复行的文件上,它的行号和 git 打印出来的可能不完全一致。
数量限制,以及各个数字的含义
每一侧最多对比 4000 行。在这个范围内,工具会先去掉开头和结尾完全相同的行,所以在很长的文件里只改一行,开销很小;如果第一处改动和最后一处改动之间的区域太大,没法一次对齐,它会提示你只粘贴改动附近的内容。
“相似度”是两侧合计中未变行所占的比例;只要有任何一行不同,它最高就显示为 99%,避免出现”100% 相似”却又列着差异的矛盾。
复制结果
“复制差异”得到的是统一格式(unified diff)文本:每个变化区域有一个类似 @@ -3,4 +3,5 @@ 的头,包含三行未变的上下文,然后是以 +、- 或空格开头的行。它不含文件名头部,适合粘贴到评审评论或工单里,不适合直接交给 patch 使用。和这个工具的其他功能一样,对比和复制都在页面内完成,不会上传任何内容。