Python Unicode 规范化:看起来一样的文字,为什么比较不相等

10-01 4阅读

从网页复制的名字与数据库里的名字看起来一致,等号却返回假,未必是编码损坏。有些文字能用一个预组合码点表示,也能用基础字母加组合符号表示。Python 字符串比较检查码点序列,不会替你决定这些形式是否应视作同一个业务名称。本文示例已在 Python 3.12.14、Unicode 数据库 15.0.0 上执行。

Python Unicode 规范化:看起来一样的文字,为什么比较不相等

AI生成概念图:不同组合的图形经过统一规则,汇合为相同表示。仅作概念说明,不代表实际界面或实测数据。

先观察差异,再决定是否转换

下面用转义形式构造两种咖啡单词,避免编辑器替我们改变样本。第一种末尾是预组合字母,第二种末尾是普通字母和组合重音。原始比较为假,长度分别为四和五;两边都做 NFC 后才相等。这里的长度是码点数量,不能据此推断屏幕宽度或用户眼中的字符数。

import unicodedata as ud

a = "caf\u00e9"
b = "cafe\u0301"
print("raw:", a == b, len(a), len(b))
print("NFC:", ud.normalize("NFC", a) == ud.normalize("NFC", b))
for value in ["ABC", "①", "Ⅳ"]:
    print(repr(value), repr(ud.normalize("NFC", value)),
          repr(ud.normalize("NFKC", value)))

names = [a, b, "cafe"]
groups = {}
for original in names:
    key = ud.normalize("NFC", original)
    groups.setdefault(key, []).append(original)
assert len(groups) == 2
assert ud.normalize("NFC", b) == a
assert ud.normalize("NFKC", "ABC①Ⅳ") == "ABC1IV"
assert ud.normalize("NFC", "A") != ud.normalize("NFC", "А")
print("group sizes:", [len(values) for values in groups.values()])
print("Unicode database:", ud.unidata_version)

代码同时建立分组,保留每个原始名称。输出组大小为二和一,说明两种带重音写法被归到一起,而没有重音的 cafe 仍单独存在。这个实验适合放进导入流程验收:输入一组已知差异,检查分组结果,而不是只看一条正常英文能否通过。

NFC 与 NFKC 改变的范围不同

NFC 处理规范等价形式;它不承诺把所有看着相似的文字折叠起来。示例里的全角拉丁字母、带圈数字和罗马数字在 NFC 下保持原样。NFKC 还处理兼容等价形式,输出分别变成 ABC、普通数字一和两个拉丁字母 IV,因此它会主动丢掉部分排版或表现形式的差异。

这不是越强越好的开关。站内搜索也许希望全角输入能找到半角商品编号;排版工具、历史原文或具有特定格式意义的字段却可能需要保留差别。先为字段写一句相等规则,再选形式。如果产品没有决定带圈数字和普通数字是否同义,技术层不应自行把它们覆盖成一个值。

比较键和显示文本分开保存

实用做法是保存原文,另外生成用途明确的比较键。新增唯一约束之前,先对存量数据试算该键,导出一对多碰撞清单,交由业务处理。直接批量规范化并覆盖原列,会让原来不同的记录突然撞上同一名称,也会失去解释变化的依据。示例的列表分组就是这一检查的缩小版。

如果还要忽略大小写、空格或标点,应把这些步骤写成独立策略,并固定处理顺序。规范化本身不会自动去空格,也不会替你完成语言相关的搜索规则。给接口、离线导入与查询端共用同一实现,避免新增记录按一种规则生成键,查找时却使用另一种规则。

相似外观仍需要其他检查

最后一条断言比较拉丁字母 A 与西里尔字母 А,后者码点是 U+0410。它们经 NFC 后仍不同。这提醒我们,规范化不是仿冒字符检测器,更不是账号安全策略。识别码、签名输入和协议字段应遵循各自规范,不能因为显示相似就擅自改写。

排查记录应包含原始码点、规范化形式、运行时与 Unicode 数据库版本。升级解释器后,在代表性样本上重跑分组,观察是否新增碰撞。验收目标不是“所有文字都变短”,而是规定应相等的样本能一致命中,不应合并的样本仍然可区分,原始文本也仍可追溯。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。