Unicode 规范化能让看起来相同但编码方式不同的文字更容易比较;兼容规范化还能处理部分全角和特殊符号。用于 AI 预处理时,应先决定哪些差异可以消除。下面对比 NFC 与 NFKC,并用编号碰撞和实体位置示例说明为何不能全库无条件转换。
NFC 与 NFKC 分别保留什么
NFC 处理规范等价形式并尽可能组合字符。例如拉丁字母 e 加组合重音可以组合为 é。NFKC 在此基础上处理兼容等价形式,例如全角字母和带圈数字。它们不负责繁简转换、拼写校正,也不等于模型分词。

普通搜索字段可能希望统一全角与半角;编号、数学符号、品牌拼写或原文引证却可能依赖这些差异。应按字段设规则,不能把所有列都按一条 NFKC 管道覆盖。本文只展示标准函数行为,没有给任何业务数据指定默认政策。
环境与代码
示例在 Windows、Python 3.11.15 的本地 CPU 环境实际运行。新建空文件夹,将后面完整代码保存为 04-unicode-normalize.py,在该目录用 PowerShell 执行:
python -m venv .venv
.\.venv\Scripts\python.exe 04-unicode-normalize.py
最后一行在保存代码后执行。macOS 或 Linux 使用 ./.venv/bin/python。输出文件留在当前目录,先用教学样本核对;代码不调用大模型 API,也不修改你的原始业务文件。
示例使用 Python 的 unicodedata,日志保存该运行时的 Unicode 数据库版本。len 计算 Python 字符串的码点数量,用户看到的一个字形可能由多个码点组成;它不等于字节数,也不等于模型 token 数。
把完整代码保存为 04-unicode-normalize.py 后运行,逐行比较 source、NFC、NFKC 和字符数,再查看编号碰撞及实体位置错位的结果。原文和转换版本一起保存在 normalization_audit.json。
import unicodedata as ud
from pathlib import Path
import json
samples=['e\u0301', 'AI007', '① ㎏', 'A①', 'A1']
rows=[]
for text in samples:
nfc=ud.normalize('NFC',text)
nfkc=ud.normalize('NFKC',text)
rows.append({'source_text':text,'NFC':nfc,'NFKC':nfkc})
print(repr(text),'NFC=',repr(nfc),'NFKC=',repr(nfkc),
'lengths=',(len(text),len(nfc),len(nfkc)))
assert ud.normalize('NFKC',nfkc)==nfkc
ids=['A①','A1']
normalized=[ud.normalize('NFKC',x) for x in ids]
print('id collision:',len(set(normalized))<len(set(ids)))
text='Cafe\u0301 上海'
start=text.index('上海'); end=start+len('上海')
norm=ud.normalize('NFC',text)
print('source entity:',(start,end),text[start:end])
print('reuse old offsets:',repr(norm[start:end]))
print('new entity start:',norm.index('上海'))
assert text[start:end]=='上海' and norm[start:end]!='上海'
Path('normalization_audit.json').write_text(
json.dumps({'rows':rows,'unicode_version':ud.unidata_version,
'source_text':text,'normalized_text':norm,
'source_span':[start,end],'new_span':[norm.index('上海'),norm.index('上海')+2]},
ensure_ascii=False,indent=2),encoding='utf-8')
print('saved normalization_audit.json')
实际输出:相同、合并与位置变化
'é' NFC= 'é' NFKC= 'é' lengths= (2, 1, 1)
'AI007' NFC= 'AI007' NFKC= 'AI007' lengths= (5, 5, 5)
'① ㎏' NFC= '① ㎏' NFKC= '1 kg' lengths= (3, 3, 4)
'A①' NFC= 'A①' NFKC= 'A1' lengths= (2, 2, 2)
'A1' NFC= 'A1' NFKC= 'A1' lengths= (2, 2, 2)
id collision: True
source entity: (6, 8) 上海
reuse old offsets: '海'
new entity start: 5
saved normalization_audit.json
分解形式 e 加组合重音从两个字符组成的序列变成 é;NFKC 把“AI007”变为“AI007”,把“① ㎏”变为“1 kg”。A① 与 A1 归一后碰撞;沿用原实体位置 (6,8) 只取到“海”。
“① ㎏”由三个码点变成四个码点,因为兼容符号展开为字符序列。某些输入长度变短,有些变长;只比较输入输出字符数相同也不能证明文本含义和标识符保持不变。
Cafe 加组合重音的示例中,上海在原文从位置 6 开始;NFC 后从位置 5 开始。代码刻意沿用旧位置,实际只得到“海”。这与分词器的子词位置不同,是字符串本身已经变更。
用于训练或检索前怎样验收
NFKC 会消除部分兼容字形差异,可能让不同编号、公式或标记合并;规范化还可能改变字符数量,已有实体和答案位置不能直接照搬。
保留 source_text、normalized_text、规范化形式和版本。编号列原则上先按业务含义判断,转换后检查唯一值数量和碰撞清单;碰撞不能通过“留下第一条”来静默解决。对内容字段,可以抽查兼容符号与专业术语,确认变化符合任务。
已经有实体标注时,可选择先在规范化文本上重新标注,或构建并验证位置映射。代码只在一个简单示例里重新找到了实体,不提供通用映射;同一实体多次出现时不能靠一次 index 定位全部实例。
训练和实际输入采用相同政策。不要在训练阶段保留全角字形、预测阶段统一成半角后,又把表现变化归因于模型版本。规范化的幂等性检查有助于确认同一形式再次应用不会改变结果,但它不能证明业务语义无损。
官方资料与核对范围
资料读取日期为 2026 年 10 月 1 日。接口行为依照以下官方文档或项目说明核对,输出来自上述本地教学代码。
- Python unicodedata:normalize的四种形式和码点数据库版本。
- Unicode Standard Annex #15:规范等价、兼容等价与规范化形式定义。
Ai菜鸟网。发布者:AI小管家,转载请注明出处:https://www.alyyhw.com/31813.html