名词释义 06:什么是逐字比对 / 字符级比对(Character-level Diff)?

0 评论 570 浏览 0 收藏 33 分钟

逐字比对,文本差异检测的基石,却也充满挑战。本文深入剖析其技术原理、应用场景及行业现状,带你领略逐字比对的魅力与困境。

从01到05,我们把文档比对的”上层建筑”基本搭完了:比对是什么、版本是什么、版本控制是什么、差异报告是什么、变更记录是什么……这些概念回答的都是”做什么”和”为什么做”的问题。

从这一篇开始,我们要往下钻一层,进入”怎么做”的技术细节。

第一个要讲的技术概念,就是:逐字比对 / 字符级比对(Character-level Diff)

这是所有比对技术中最基础、最精细、也最”笨”的一种。但正是这种”笨功夫”,构成了整个比对体系的地基。

一、差之毫厘,谬以千里

《礼记·经解》里有句话:「差若毫厘,谬以千里。」

这句话用来形容逐字比对的价值,再合适不过。

大家都知道,彦祖我是一家合同管理系统软件公司的产品经理。在我们这个行业,有两类文本是”差一个字都不行”的:一类是代码,一类是合同。

  1. 代码里少一个分号,程序崩溃;多一个空格,可能编译不过;把==写成=,逻辑就全错了。
  2. 合同里少一个”不”字,意思完全相反;把”5%“写成”50%”,赔偿金额差十倍;把”甲方”写成”乙方”,责任主体就换人了。

这两类文本有一个共同特点:每一个字符都有意义,每一个字符的变化都可能带来巨大的后果。

所以,当我们需要比对这类文本时,不能只看”大概改了哪里”,而是要精确到每一个字符——这就是逐字比对存在的意义。

1.1 什么是逐字比对?

用一句话定义:

逐字比对(Character-level Diff)是一种以单个字符为最小比对单位的文本差异检测技术,它能够精确识别两段文本之间每一个字符的新增、删除和修改。

如果把文档比对比作”找不同”游戏,那么:

  • 段落比对是”这两幅画里,哪个区域不一样”——粒度最粗,只告诉你大概位置。
  • 逐行比对是”这两幅画里,哪一行不一样”——粒度中等,能定位到具体的行。
  • 逐字比对是”这两幅画里,哪一个像素不一样”——粒度最细,精确到最小单位。

在文本的世界里,”像素”就是字符。逐字比对就是把两段文本拆成一个个字符,然后逐一对比,找出所有不同的地方。

1.2 一个简单的例子

假设我们有两个版本的合同条款:

  • 旧版:「乙方应在收到货物后30日内完成付款。」
  • 新版:「乙方应在收到货物后45日内完成付款。」

逐字比对会这样分析:

  • 「乙」=「乙」✓
  • 「方」=「方」✓
  • 「应」=「应」✓
  • 「在」=「在」✓
  • 「收」=「收」✓
  • 「到」=「到」✓
  • 「货」=「货」✓
  • 「物」=「物」✓
  • 「后」=「后」✓
  • 「3」≠「4」✗ → 修改
  • 「0」≠「5」✗ → 修改
  • 「日」=「日」✓
  • 「内」=「内」✓

……

最终结果:发现2处字符级差异,位置在第10-11个字符,「30」被修改为「45」。

这就是逐字比对的工作方式:不放过任何一个字符的变化。

二、程序员的老朋友:diff命令的前世今生

对于程序员来说,逐字比对不是什么新鲜概念——他们每天都在用,只是可能没意识到。

2.1 Unix diff:一切的起点

1974年,贝尔实验室的Douglas McIlroy为Unix系统开发了diff命令。这个命令的作用很简单:比较两个文件,输出它们的差异。

diff命令的输出格式后来成为了行业标准,被称为”diff格式”或”patch格式”。它长这样:

– old_file.txt

+++ new_file.txt

@@ -1,3 +1,3 @@

第一行内容

-旧的第二行

+新的第二行

第三行内容

这里的-表示删除的行,+表示新增的行。

diff命令最初是逐行比对的,但后来的改进版本(如GNU diff)增加了–word-diff和字符级比对的能力。

2.2 Git diff:程序员的日常

如果说diff命令是逐字比对的”祖师爷”,那Git diff就是它最成功的”传人”。

每一个程序员,每天都在用Git diff。当你执行git diff命令时,Git会比较工作区和暂存区(或者两个commit)之间的差异,精确到每一个字符的变化。

Git diff的输出不仅会告诉你哪一行变了,还会用颜色高亮显示具体变化的字符:红色表示删除,绿色表示新增。

比如你把一个变量名从userName改成username,Git diff会精确地告诉你:删除了N,这不是整行的变化,而是一个字符的变化。

2.3 代码审查中的逐字比对

在代码审查(Code Review)场景中,逐字比对的价值体现得淋漓尽致。

想象一下,一个程序员提交了一个Pull Request,改动了1000行代码。如果没有逐字比对,审查者只能看到”这1000行和之前不一样”,然后一行一行地人工对比。

有了逐字比对,审查者可以清楚地看到:

  • 第123行:把if (a = b)改成了if (a == b)——修复了一个赋值和比较混淆的Bug。
  • 第456行:把timeout: 30改成了timeout: 300——超时时间从30秒改成了300秒。
  • 第789行:把// TODO: fix this删掉了——终于把这个TODO处理了。

每一个字符的变化都清清楚楚,审查效率大幅提升。

2.4 IDE中的实时比对

现代IDE(如VS Code、IntelliJ IDEA、WebStorm)都内置了强大的逐字比对功能。

当你打开一个文件的历史版本对比视图时,IDE会用并排或内联的方式展示差异,精确到每一个字符。变化的字符会用不同的背景色高亮,让你一眼就能看出改了什么。

更厉害的是,很多IDE支持”实时比对”——你一边编辑,它一边显示和上一个保存版本的差异。这对于代码重构特别有用:你可以随时看到自己改了哪些地方,避免改着改着忘了改过什么。

三、合同系统中的逐字比对:一个字都不能错

讲完了程序员的世界,现在回到合同系统。

在合同管理领域,逐字比对的重要性一点都不比代码低——甚至更高。因为代码错了,最多是系统崩溃,可以修复;合同错了,可能是几百万的损失,而且签了就生效,想改都改不了。

3.1 合同中”一字之差”的血泪教训

在合同管理的江湖里,流传着无数”一字之差”酿成大祸的故事。

故事一:少了一个”不”字。

某公司的采购合同里有一条:”乙方不得将本合同项下的权利义务转让给第三方。”结果在某一版修改中,”不”字被误删,变成了”乙方得将本合同项下的权利义务转让给第三方。”意思完全相反。等到乙方真的把合同转让给了一个资质很差的第三方,甲方才发现问题,但为时已晚。

故事二:小数点点错位置。

某工程合同的违约金条款写的是”违约金为合同金额的0.5%“,结果在打印时小数点丢失,变成了”违约金为合同金额的5%”。一个1000万的合同,违约金从5万变成了50万,差了整整10倍。

故事三:甲乙方写反了。

某服务合同的保密条款写的是”甲方应对乙方提供的技术资料承担保密义务”,结果在修改过程中,甲乙方被调换,变成了”乙方应对甲方提供的技术资料承担保密义务”。保密义务的承担方完全反了。

这些故事的共同点是:如果当时有逐字比对,这些错误在签署前就能被发现。

3.2 合同比对的特殊挑战

相比代码比对,合同比对面临一些特殊的挑战:

第一,格式干扰。代码通常是纯文本,格式简单;合同通常是Word或PDF,有复杂的格式(字体、字号、颜色、缩进、表格等)。逐字比对需要能够”穿透”格式,只比较文字内容。

第二,中文特性。英文单词之间有空格分隔,比对时可以以单词为单位;中文没有空格,只能以字符为单位。而且中文有很多形近字(如”己”和”已”、“戊”和”戌”),人眼很难发现,必须靠机器逐字比对。

第三,法律语言的精确性。法律文本对用词极其讲究,“应当”和”可以”、“或”和”及”、“之前”和”之日前”,这些细微的差别都有不同的法律含义。逐字比对必须能够捕捉这些细微差异。

第四,多版本流转。一份合同从起草到签署,可能要经过十几个版本,在业务、法务、客户之间来回修改。每一次修改都可能引入新的差异,逐字比对需要能够追踪整个版本链条。

3.3 理想很丰满,现实很骨感

讲到这里,我要给团队里的年轻同事泼一盆冷水:纯粹的逐字比对,在实际的文档比对产品中几乎不存在。

不是技术上做不到,而是做出来没法用。

为什么?因为逐字比对的前提是”两段文本已经被准确提取出来了”。但在真实的业务场景中,文档往往是Word、PDF、扫描件,甚至是拍照件。要把这些文档变成可比对的文本,需要经过OCR识别、版面分析、文本提取等一系列步骤。

问题就出在这里。

如果用传统OCR做逐字比对,只要坐标差一点点,就会被识别为差异。比如同一份合同,扫描时稍微歪了一点,或者打印时边距不一样,OCR提取出来的文本位置就会有偏差。这时候做逐字比对,会产生大量的”伪差异”——明明内容一样,系统却说不一样。

更麻烦的是表格。表格里的文字如果跨行、跨列,OCR的阅读顺序可能会乱掉。本来是”第一行第一列、第一行第二列”,结果被识别成”第一列第一行、第一列第二行”。这时候做逐字比对,结果就是一团乱麻。

所以,逐字比对是一个“理论上正确”的概念,但在实际产品中,必须结合大量的预处理和后处理,才能真正可用。

四、行业现状:从OCR到视觉大模型

既然纯粹的逐字比对不好用,那行业里的玩家们都在怎么做?

这一节,我想给团队的年轻同事讲讲行业现状,让大家对自己所处的赛道有个清醒的认识。

4.1 传统方案:OCR + 规则引擎

早期的文档比对产品,基本都是这个套路:

第一步,用OCR把文档转成文本。

第二步,用规则引擎做文本对齐和比对。

第三步,输出差异结果。

这个方案的优点是逻辑清晰、可控性强。缺点是对OCR的准确率要求极高,而且规则引擎很难覆盖所有的边界情况。

在标准化程度高的场景(比如打印清晰的合同、格式统一的表单),这个方案效果还不错。但一旦遇到扫描件质量差、版面复杂、手写内容等情况,准确率就会断崖式下跌。

4.2 新兴方案:视觉大模型

最近几年,随着深度学习和大模型技术的发展,越来越多的产品开始采用视觉大模型来做文档理解和比对。

视觉大模型的思路是:不再把文档拆成”OCR识别→文本比对”两个独立的步骤,而是让模型直接”看”文档图像,理解其中的内容和结构。

这个方案的优点是对复杂版面的适应性更强,不需要人工编写大量规则。缺点也很明显:

  • 阅读顺序问题。模型可能会把文档的阅读顺序搞错,比如把页眉当成正文,把脚注当成段落。
  • 联想问题。大模型有时候会”脑补”,把文档里没有的内容联想出来,或者把相似的内容混淆。
  • 可解释性差。传统方案出了问题,可以一步步排查是哪个环节出错;大模型出了问题,往往只能说”模型就是这么判断的”,很难解释原因。
  • 计算成本高。视觉大模型的推理成本比传统OCR高很多,对于大批量文档比对场景,成本是个问题。

4.3 行业玩家的不同选择

目前市面上做文档比对的公司,基本都在”传统方案”和”大模型方案”之间寻找平衡点,而且各有侧重:

  • 有的公司专注于法律文书场景,比如法狗狗。法律文书的格式相对规范,用词有固定套路,所以可以针对法律领域做深度优化。但脱离了法律文书场景,换成其他类型的文档,效果就会大打折扣。
  • 有的公司专注于金融类文件,比如庖丁科技。金融文件有大量的表格、数字、专业术语,需要针对这些特点做专门的处理。但遇到手写内容或者非标准格式,准确率也会下降很多。
  • 有的公司专注于合同场景,比如肇新科技。在合同领域做到了很高的准确率,但换成手写文档、外语文档,准确率也会从99%以上降到95%左右。

这就是行业的现状:没有一家公司能做到“通吃”,大家都是在自己擅长的领域深耕,牺牲某些场景的能力来强化核心场景的表现。

4.4 关于准确率的真相

经常有客户问:“你们的比对准确率是多少?”

这个问题看似简单,其实很难回答。因为准确率是和场景强相关的。

同一个产品,在标准打印合同上可能做到99.9%的准确率;换成扫描件,可能降到98%;换成手写文档,可能降到95%;换成外语文档,可能降到90%。

所以,永远不要跟客户说“我们的准确率是100%”。

100%是不存在的。任何一个诚实的从业者都会告诉你,文档比对是一个”尽可能准确”而不是”绝对准确”的事情。

正确的回答方式是:说明在什么场景下、什么条件下,能达到什么样的准确率。比如:“在标准打印的中文合同场景下,我们的比对准确率可以达到99%以上;如果是扫描件或者手写内容,准确率会有所下降,具体取决于文档质量。”

这才是对客户负责的态度,也是对自己产品的清醒认知。

五、逐字比对的技术原理

讲完了行业现状,现在来聊聊技术原理。逐字比对看起来简单,背后其实有不少学问。

5.1 最长公共子序列(LCS)算法

逐字比对最经典的算法是LCS(Longest Common Subsequence,最长公共子序列)算法。

简单来说,LCS算法的思路是:找到两个字符串中最长的公共部分,剩下的就是差异。

举个例子:

  • 字符串A:ABCDEFG
  • 字符串B:ABXDEFG

LCS算法会找到最长公共子序列:ABDEFG(注意,C和X不在公共子序列里)

然后通过比较,得出结论:A中的C被替换成了B中的X。

LCS算法的时间复杂度是O(m×n),其中m和n分别是两个字符串的长度。对于短文本来说,这个复杂度完全可以接受;但对于长文档(比如几十页的合同),直接用LCS可能会比较慢。

5.2 Myers差异算法

1986年,Eugene W. Myers发表了一篇论文,提出了一种更高效的差异算法,后来被称为Myers算法。Git的diff功能就是基于Myers算法实现的。

Myers算法的核心思想是:把差异比对问题转化为图论中的最短路径问题。它能够在O((m+n)×d)的时间复杂度内完成比对,其中d是两个字符串之间的差异数量。当差异较小时(这是大多数实际场景),Myers算法比LCS快很多。

5.3 字符级 vs 词级 vs 行级

在实际应用中,逐字比对并不总是最佳选择。根据不同的场景,可能需要选择不同的比对粒度:

  • 字符级比对:精度最高,能发现任何细微的变化。适合对精确性要求极高的场景,如合同条款、法律文书、代码等。缺点是输出结果可能过于琐碎,比如一个单词改了一个字母,会显示为”删除一个字符+新增一个字符”,而不是”修改了一个单词”。
  • 词级比对:以单词为单位进行比对。适合英文文本,能够更好地展示”哪个单词变了”。但对中文不太适用,因为中文没有明确的单词边界。
  • 行级比对:以行为单位进行比对。适合代码、配置文件等以行为逻辑单位的文本。优点是结果清晰,缺点是粒度较粗,如果一行中只改了一个字符,整行都会被标记为”已修改”。

在实际的合同比对系统中,通常会采用”多层比对”的策略:先做行级比对,找出有差异的行;然后对有差异的行再做字符级比对,精确定位变化的字符。这样既保证了效率,又保证了精度。

5.4 处理中文的特殊考虑

中文逐字比对有一些特殊的考虑:

  • 编码问题。中文字符在不同编码下(UTF-8、GBK、GB2312等)的字节表示不同。比对前需要统一编码,否则可能出现乱码或误判。
  • 标点符号。中文有全角和半角两套标点符号,比如中文逗号「,」和英文逗号「,」。在比对时,需要决定是否把它们视为相同字符。
  • 空白字符。中文文本中可能混杂着全角空格、半角空格、制表符、换行符等各种空白字符。这些空白字符在视觉上可能看不出区别,但在逐字比对时会被识别为差异。通常需要做预处理,统一空白字符的表示。
  • 形近字。中文有很多形近字,如”己已巳”、“戊戌戍”、”日曰”等。这些字在视觉上非常相似,人眼很难分辨,但逐字比对可以准确识别。这也是逐字比对在中文合同场景中特别有价值的原因之一。

六、逐字比对的优缺点

任何技术都有其适用场景和局限性,逐字比对也不例外。

6.1 优点

  • 精度高。逐字比对是所有比对技术中精度最高的,能够发现任何字符级别的变化,不会遗漏任何差异。
  • 无歧义。比对结果是确定性的,同样的输入一定得到同样的输出。不存在”可能是这样,也可能是那样”的模糊情况。
  • 可追溯。每一个差异都有明确的位置信息,可以精确定位到第几个字符。这对于后续的审阅和处理非常重要。
  • 技术成熟。逐字比对的算法已经发展了几十年,非常成熟稳定。各种编程语言都有现成的库可以使用。

6.2 缺点

  • 结果可能过于琐碎。如果两个版本差异较大,逐字比对的结果可能会非常长,充满了”删除一个字符””新增一个字符”这样的细节,让人看得眼花缭乱。
  • 不理解语义。逐字比对只关心字符本身,不理解字符的含义。比如把”5%“改成”五percent”,逐字比对会认为这是很大的变化(删除了2个字符,新增了9个字符),但从语义上看,这可能只是格式的调整,实际含义没变。
  • 对格式敏感。如果两个版本的格式不同(比如一个用全角标点,一个用半角标点),逐字比对会把这些格式差异也识别出来,产生大量”噪音”。
  • 性能问题。对于非常长的文本,逐字比对的计算量可能很大。虽然有各种优化算法,但在极端情况下仍然可能遇到性能瓶颈。

6.3 什么时候用逐字比对?

基于以上优缺点,逐字比对最适合以下场景:

  • 对精确性要求极高的文本。如合同条款、法律文书、财务报表、技术规格书等。这些文本”差一个字都不行”,必须用最精细的比对方式。
  • 文本长度适中。几页到几十页的文档,逐字比对的性能完全可以接受。如果是几百页的长文档,可能需要结合其他技术(如先做段落级比对,再对有差异的段落做逐字比对)。
  • 需要精确定位差异位置。如果后续需要对差异进行逐一审阅、批注、确认,那么逐字比对提供的精确位置信息就非常有价值。
  • 版本之间差异较小。如果两个版本差异很大(比如重写了一半以上的内容),逐字比对的结果可能会很乱,这时候可能需要结合段落级比对来展示。

七、逐字比对与其他比对技术的关系

在前面的规划中,我们提到了多种比对技术:逐字比对、逐行比对、段落比对、结构化比对、语义比对等。这些技术不是互相替代的关系,而是互相补充的关系。

7.1 比对技术的”金字塔”

可以把各种比对技术想象成一个金字塔:

  1. 最底层是逐字比对(字符级)。这是最基础、最精细的比对,所有其他比对技术最终都要落到字符级别。
  2. 往上一层是逐词比对(词级)。以单词为单位,适合英文文本。
  3. 再往上是逐行比对(行级)。以行为单位,适合代码和配置文件。
  4. 再往上是段落比对(段落级)。以段落为单位,适合长文档的快速浏览。
  5. 再往上是结构化比对。理解文档的结构(标题、章节、条款),在结构层面进行比对。
  6. 最顶层是语义比对。理解文本的含义,判断语义是否发生了变化。

越往上,粒度越粗,但”智能程度”越高;越往下,粒度越细,但更加”机械”。

7.2 多层比对的实践

在实际的文档比对系统中,通常不会只用一种比对技术,而是多种技术结合使用。

一个典型的流程是:

第一步,结构化比对。先识别文档的结构,把文档拆分成章节、条款、段落等逻辑单元。

第二步,段落级比对。在结构对齐的基础上,比较对应段落是否有差异。

第三步,行级比对。对有差异的段落,进一步比较每一行。

第四步,字符级比对。对有差异的行,精确到每一个字符。

这种”由粗到细”的比对策略,既保证了效率(大部分没有差异的内容可以快速跳过),又保证了精度(有差异的地方会被精确定位)。

7.3 逐字比对是”地基”

在这个金字塔中,逐字比对是最底层的”地基”。

无论上层用什么技术,最终展示给用户的差异,都需要落到字符级别。用户想知道的不只是”这一段有变化”,而是”这一段的第几个字变成了什么”。

所以,逐字比对虽然”笨”,但不可或缺。它是整个比对体系的基础能力。

八、常见误区

在实际工作中,很多人对逐字比对存在一些误解。

8.1 “逐字比对就是简单的字符串比较”

这是最常见的误解。

简单的字符串比较只能告诉你”两个字符串是否相等”,不能告诉你”哪里不同、怎么不同”。

逐字比对不只是比较,还要输出差异的详细信息:哪些字符被删除了、哪些字符被新增了、哪些字符被修改了、每个差异的位置在哪里。

这需要用到专门的差异算法(如LCS、Myers等),比简单的字符串比较复杂得多。

8.2 “逐字比对能解决所有比对问题”

逐字比对精度高,但不是万能的。

  • 对于格式复杂的文档(如Word、PDF),需要先做格式解析,才能进行逐字比对。
  • 对于扫描件,需要先做OCR识别,才能得到可比对的文本。
  • 对于语义层面的变化(如同义词替换、句式调整),逐字比对可能会产生大量”噪音”,需要结合语义比对技术。

逐字比对是基础能力,但不是全部能力。一个完整的文档比对系统,需要多种技术的配合。

8.3 “逐字比对的结果就是最终结果”

逐字比对的原始结果通常需要进一步处理,才能呈现给用户。

比如:

  • 连续的字符变化可以合并成一个”修改块”,而不是显示为一堆零散的”删除+新增”。
  • 无意义的格式差异(如空格、换行)可以被过滤掉,只保留有意义的内容差异。
  • 关键字段的变化(如金额、日期、主体)可以被特别标注,提醒用户重点关注。

这些”后处理”工作,是把逐字比对从”技术能力”变成”用户体验”的关键。

九、小结:一句话记住”逐字比对”

最后,用一句话来总结:

逐字比对(Character-level Diff)是以单个字符为最小单位的文本差异检测技术,能够精确识别两段文本之间每一个字符的新增、删除和修改,是所有比对技术中精度最高的基础能力。

它的核心价值在于两个字:精确

  • 在代码的世界里,它帮助程序员发现每一个字符的变化,避免因为一个分号、一个空格而引发的Bug。
  • 在合同的世界里,它帮助法务和业务发现每一个字的变化,避免因为一个”不”字、一个小数点而引发的损失。

逐字比对看起来”笨”,但正是这种”笨功夫”,构成了整个比对体系的地基。没有它,上层的所有”智能”都是空中楼阁。

在接下来的文章里,我们会继续介绍其他比对技术:逐行比对、段落比对、结构化比对、语义比对……一步步把这套比对技术体系搭建完整。

本文由 @合同管理吴彦祖 原创发布于人人都是产品经理。未经作者许可,禁止转载

题图来自 Unsplash,基于CC0协议

该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
评论
评论请登录
  1. 目前还没评论,等你发挥!