NFC 做什么
NFC(Normalization Form Canonical Composition)先按规范做分解,再按规范顺序重新组合可合并的部分,得到「尽量合成」的结果。例如拉丁字母 e 加组合尖音符经 NFC 后变成单个预组合字符;汉字与韩文音节也会被组合成编码表里已有的形式。它的价值在于把同一语义的多种码点写法收敛成一种,使字符串比较、长度统计、索引与去重更稳定。Unicode 官方推荐在网络协议与外部存储中使用 NFC。
基本信息
| 定义 | 规范等价下的合成形式(Canonical Composition) |
|---|---|
| 典型效果 | 基字母与组合标记合并为一个预组合码点 |
| 是否改变语义 | 不应改变(规范等价),但会改变码点序列与长度 |
| 官方建议 | Web 与协议场景推荐使用 NFC |
| 不涉及 | 兼容等价(全角/半角、连字等)不会被 NFC 处理 |
| 常用接口 | Python unicodedata.normalize('NFC', s)、多数语言的 normalize |
典型使用场景
- 用户输入归一:搜索、去重、用户名比对前统一成 NFC,避免同形不同码点
- 数据入库:存储前归一化,减少同一内容出现多种字节形式
- 索引与搜索:归一化后建立索引,提升召回一致性
- 跨系统交换:与外部系统交互前统一形式,减少对方环境的差异
- 长度校验:归一化后再统计长度,避免「看起来一样却长度不同」
归一化示例
归一化示例
python -c "import unicodedata as u; s='e\u0301'; print(len(s), [hex(ord(c)) for c in s])" # 2 个码点
python -c "import unicodedata as u; s=u.normalize('NFC','e\u0301'); print(len(s), [hex(ord(c)) for c in s])" # 1 个码点
python -c "import unicodedata as u; print(u.normalize('NFC','e\u0301') == u.normalize('NFD','\u00e9'))" # False:形式不同
python -c "import unicodedata as u; a=u.normalize('NFC','e\u0301'); b=u.normalize('NFC','\u00e9'); print(a==b)" # True:同形式后相等
# 比较前统一形式是标准做法:先 normalize 再比较或建索引NFC 的表现与影响
| 对象 | 归一化前 | 归一化后 |
|---|---|---|
| 拉丁重音字母 | 基字母 + 组合标记(2 码点) | 预组合字符(1 码点) |
| 韩文音节 | 字母序列 | 组合成音节码点 |
| 码点总数 | 可能更多 | 通常更少 |
| 字节长度 | 随码点变化 | 通常变短 |
| 语义 | 不变 | 不变(规范等价) |
NFC 只处理规范等价,不处理兼容等价:全角字符、连字、上标数字这类「兼容形式」不会被 NFC 变成普通形式,需要 NFKC。做检索或校验时要想清楚是否需要兼容形式归一。
归一化不是安全措施的反面也不是万能解:某些历史漏洞正是利用归一化时机差(先校验后归一化)绕过过滤。安全校验应在归一化之后进行,并保证「校验用的字符串」与「最终使用的字符串」是同一个。
归一化顺序要固定:多个系统间传递内容时,若一方归一化另一方不归一化,就会出现「内容看起来相同但字节不同」的比对失败。约定统一使用 NFC 并在入口处执行,是成本最低的做法。