四种形式与两类等价
归一化形式的差异来自它们处理哪一类等价。规范等价关注「同一字符的不同表示」,例如预组合重音字母与基字母加组合标记;兼容等价关注「语义相近但因排版或历史原因单独编码的字符」,例如全角字母、连字、上标数字、带格式的圆符号。NFC/NFD 只处理前者,保持字符「身份」不变;NFKC/NFKD 还会把兼容字符折叠成基本形式,因此会改变字符本身(全角 A 变成半角 A),不可用于需要保留原貌的存储。
基本信息
| NFC | 规范等价合成,存储与交换推荐 |
|---|---|
| NFD | 规范等价分解,检索与渲染处理常用 |
| NFKC | 兼容等价合成,清洗与检索折叠常用 |
| NFKD | 兼容等价分解,需要彻底展开时使用 |
| 规范等价 | 同一字符的不同表示,语义完全一致 |
| 兼容等价 | 语义相近但形态不同,折叠会改变字符外观 |
典型使用场景
- 存储与展示:使用 NFC,保留字符原始形态,长度较稳定
- 检索与匹配:使用 NFKC,把全角、连字、上标折叠成通用形式,提高召回
- 数据清洗:统一导入数据中的兼容字符,减少重复条目
- 风控与安全:在做黑名单匹配前折叠兼容形式,防止用全角字符绕过
- 排序与分组:先归一化再按语言规则排序,减少同内容不同序的情况
四种形式对比
四种形式对比
python -c "import unicodedata as u; print([u.normalize(f,'\uff21') for f in ('NFC','NFD','NFKC','NFKD')])" # 全角 A:仅 NFKC/NFKD 折叠为 A
python -c "import unicodedata as u; print([u.normalize(f,'\ufb01') for f in ('NFC','NFD','NFKC','NFKD')])" # 连字 fi
python -c "import unicodedata as u; print([u.normalize(f,'\u00b2') for f in ('NFC','NFD','NFKC','NFKD')])" # 上标 2
python -c "import unicodedata as u; print(hex(ord(u.normalize('NFKC','\uff21'))))" # 0x41
# 检索侧折叠:查询与索引都按 NFKC 处理,展示仍用原字符串场景与形式选择
| 场景 | 推荐形式 | 原因 |
|---|---|---|
| 数据库存储 | NFC | 保留字符身份,长度较稳定 |
| URL 与协议交换 | NFC | 与 Web 生态一致,避免同义不同形 |
| 全文检索索引 | NFKC(或另建折叠列) | 提高召回,兼容全角与连字 |
| 用户名校验 | NFKC 前置 + 规则校验 | 避免用兼容字符构造视觉相同的账号 |
| 文件与目录名 | NFC | 跨系统兼容性更好 |
| 显示与排版 | 保持原形式或 NFC | 折叠会改变字符外观 |
折叠是有损的:NFKC 会把全角字符、连字、上标等变成基本形式,之后无法还原原始写法。因此不要在存储层做兼容折叠,通常做法是保留原值,另外维护一个折叠后的列用于检索与唯一性判断。
兼容折叠会影响安全判断:某些字符折叠后与受限字符等价,用于绕过黑名单。安全校验应在归一化之后执行,并明确使用哪一级归一化,避免「校验用一套、使用用另一套」。
形式选择要与业务语义绑定:需要「保留用户输入原貌」的字段用 NFC 存储并单独维护检索列;需要「严格比较」的标识类字段(用户名、邮箱)可用折叠后的值做唯一性约束。具体规则以 Unicode 官方文档为准。