开始前,会这些就够了
- 会运行一小段 Python,认识字符串、列表和条件判断即可
遇到不熟悉的词,先看这 3 个解释
- Unicode
- 它是一套统一处理世界文字和符号的标准,为字符分配编号,并规定相关属性与处理规则。程序因此能交换多种语言的文本,不必为每种语言另建互不相通的编号。
- 码点
- 码点是字符编号空间中的一个数值位置,可以分配给文字、符号或其他用途。它帮助程序识别文本中的基本成员,但不是屏幕宽度,也不是文件里实际使用的字节数。
- 文字规范化
- 同一个文字有时能用不同的字符序列表示。规范化按指定规则整理这些序列,方便比较。NFC 处理规范等价关系;NFKC 还会合并一些为了兼容旧表示而保留的差别。
你从网页复制一个名字,再手动输入一遍,看起来完全一样,查重程序却留下两条记录。问题可能不在眼睛:一个带重音的字母,有时用一个编号表示,有时用字母加重音两个编号组合显示。
本站的字符计数文章问的是“有几个单位”,这里换成“哪些写法应该视作相等”。Unicode 规范化能统一一些等价表示,但要先说清采用哪条规则。它不是把所有看着像的字都变成同一个字。
看起来一样,存下来的编号也一样吗?
左边
é → é
原编号:U+00E9
处理后:U+00E9
右边
é → é
原编号:U+0065 U+0301
处理后:U+00E9
原始字符串:不相等。两边都做 NFC 后:相等。
编号按 Unicode 码点展示,不是 UTF-8 字节。NFKC 可能抹去原来的表现差别;规范化不会自动统一大小写,也不是识别冒名文字或防止攻击的完整方案。
先看 é:一个整体,或两个部分
预组合的 é 是一个码点;e 后跟组合重音,是两个码点。码点可以理解为文字元素的编号。合适的字体可能把两种序列画得一样,但程序逐个比较编号时会发现不同。这与哪种写法“更正确”不是一回事。
NFC 是一种规范化形式,会按 Unicode 的规则分解、重新组合,让规范等价的序列得到一致表示。对这对例子,双方都做 NFC 后可以相等。不要只规范化输入的一边,另一边仍按原始序列比较,再怪算法不稳定。
NFKC 还会合并更多差别,代价也更大
全角的 A 和普通 A、带圈的 ① 和普通 1,并不属于刚才那种完全相同的编码关系。NFKC 会进一步进行兼容规范化,例如把“A①”变成“A1”;NFC 则会保留这两个字符。两种规则服务的需求不同。
如果你做搜索辅助,这类合并可能很方便;如果你保存教材原文或需要保留标号样式,直接覆盖原始文字就可能损失信息。一个可审查的设计是保留原文,另生成明确用途的比较键,并让搜索和保存遵守已约定的规则。
同时打印外观和编号,别只凭字体判断
下面先比较重音字母,再观察 NFC 与 NFKC 的不同。最后两行特意设置失败边界:拉丁 A 与西里尔 А 很像,却不是同一个文字;大写 A 与小写 a 也不会仅因为 NFKC 就相等。
代码用转义序列写出容易混淆的输入,复制时更不容易把它们误改成同一个字。输出码点的那两行是一份可复现记录,不依赖你的字体是否把重音位置画得漂亮。
import unicodedata as ud
left = "\u00e9"
right = "e\u0301"
print(left == right)
print([f"U+{ord(ch):04X}" for ch in left])
print([f"U+{ord(ch):04X}" for ch in right])
print(ud.normalize("NFC", left) == ud.normalize("NFC", right))
sample = "\uff21\u2460"
print("NFC", ud.normalize("NFC", sample))
print("NFKC", ud.normalize("NFKC", sample))
print(ud.normalize("NFKC", "A") == ud.normalize("NFKC", "\u0410"))
print(ud.normalize("NFKC", "A") == ud.normalize("NFKC", "a"))怎样运行:保存为 normalization_demo.py,在一个自己的练习目录中运行 python normalization_demo.py。使用 Python 3.9 或更高版本,仅用标准库。
运行后对照结果
False
['U+00E9']
['U+0065', 'U+0301']
True
NFC A①
NFKC A1
False
False规范化不能替你判断身份或过滤风险
“这个名字的比较键相同”只是你的文字规则给出的结果,不能证明两次输入来自同一个人。相似字母、空白、大小写、语言习惯,还可能需要单独制定政策。大小写折叠是另外的操作,不要把它偷偷算在规范化头上。
同样,NFKC 不是安全过滤器。规范化之后的文本仍是外来数据,放进网页或查询语句时仍需使用对应的安全处理方法。更实际的自查问题是:哪类差异应被保留,哪类应合并,以及合并后发生重名时产品如何解释。
记住这三点
- 看起来一样不保证原始码点序列相同。
- NFC 统一规范等价写法;NFKC 还会折叠一些兼容差异。
- 规范化、大小写处理、视觉相似判断和身份验证是不同任务。
别把例子用过头
- NFKC 可能丢失有意义的格式差异,不能不加区分地覆盖所有原文。
- 规范化后仍不同的相似文字,不意味着规范化实现失效。
自己试一小步
为一个书签搜索框写出可解释的文字比较规则。
- 准备重音字母、全角字符、带圈数字和大小写各一对输入。
- 分别记录原始比较、双方 NFC、双方 NFKC 的结果。
- 决定哪些差异允许搜索时合并,并说明原始标题是否保留。
做到这些,就算完成
- 记录的是具体码点序列,不只是一张看起来相同的截图。
- 没有把 NFKC 宣称为大小写折叠或识别假冒账号的完整方案。
- 能举出至少一种不应悄悄丢掉的文字差别。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- UAX #15: Unicode Normalization Forms (新标签页)↗
支持规范等价与兼容等价、NFC
/NFKC的区别,以及不可盲目对任意文字使用兼容规范化。 - unicodedata.normalize (新标签页)↗
支持 Python 中四种规范化形式的调用及具体语义。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
MDN 中文:看起来一样的文字,怎样比较 (新标签页)↗
从两个 ñ 的写法开始,比较 NFC 前后结果,再读兼容规范化为什么可能改变显示形式。
查看学习准备与访问说明 →