两个 é,为什么不相等
先看这两个小片段:é 和 é。它们都表示右上方带一撇的字母 e,正确显示时应当有相同的外观。把字放大,并不能找到这里的区别。区别藏在保存文字的方式里。
Unicode 用码位标识字符。第一种写法只用一个码位 U+00E9,把字母和附加符号作为整体编码;第二种先写 U+0065,也就是普通的 e,再接 U+0301,表示组合用的尖音符。读者看到一个字母,数据里却可以是一项,也可以是两项。
我按这两种序列构造文本,做了一次直接比较:按编码序列检查,结果是不相等。再把它们分别编码成 UTF-8,第一份得到两个字节,第二份得到三个字节。这几个数字只描述眼前的例子;它们已经足够说明,“几个字”与“占多少字节”不能混着问。
这件事很容易被误诊。假设有一个只做精确序列匹配的书目工具,书名里存的是第一种 é,查询里填的是第二种,其余内容完全相同,它仍会漏掉结果。多盯着屏幕看几遍无济于事,重新输入也未必能解释原因。需要检查的是文本序列,以及搜索究竟按什么规则认定相同。
Unicode 把这两种表示之间的关系称为“规范等价”。规范化提供了一种处理办法:给等价的文本选定一致的表示,再比较。这里既没有错别字,也没有哪一种写法先天不合格。若把其中一份笼统地叫作“脏数据”,修复就很容易走偏。
采用 NFC 规范化,两份文本都会成为 U+00E9;采用 NFD,则都会成为字母 e 后接组合符号的序列。我也核对了这两种结果。一个选择合成形式,一个选择分解形式,在这个例子里都能让后续的精确比较得到相等。选较短的那份,只是表示方式的选择。
但不能由此推出:NFC 会把所有看起来像一个字的组合,都压成一个码位。规范化有自己的分解、排序和组合规则,并不是计算屏幕上的字形,更不是通用压缩。它也不会把这里的 é 变成普通 e;上面那一撇仍然保留。
读到这里,很自然会想再放宽一点:既然目的是方便匹配,能不能消除更多差别?另一种形式 NFKC 确实会处理兼容差异。我用两个小例子检查:圈起来的 ① 会变成 1,上标 ² 会变成 2。而 NFC 会保留这两处区别。
这时,“统一”开始涉及内容的取舍。假设只是按编号查找条目,忽略数字外面的圈,可能正合需要;若在保存一段用上标表示平方的算式,把上标直接换成普通数字,就可能损失意思。Unicode 的说明也明确提醒,兼容规范化可能移除对语义有用的区别,不能不分场合地套用。
我在意的是同一个动作里的两种代价:留着差异,搜索可能漏掉本该找到的东西;抹去差异,原文可能失去本该保留的东西。选择比较规则之前,需要先说清楚正在做书目检索、编号查找,还是保存原始文本。“更宽松”本身不能证明结果更好。
如果我来设计前面那个书目工具,会保留收到的原文,另外生成用于比较的文本,并记录所用的规范化形式。查找两种 é 时,可以共用同一个比较结果;需要展示或导出时,仍能拿回原来那份文字。将来若发现比较规则不合适,也还有重新处理的材料。
参考:Unicode 标准附录 #15:规范化形式;Unicode 规范化问答。文中的字节数与字符转换结果另经构造文本验证。
—— M-C