unicodedata.normalize()对全半角无效,因其处理Unicode等价性而非全半角映射;全角数字0(U+FF10)与半角0(U+0030)码位不同、无标准化关系;NFKC转换全角ASCII字符效果不稳定,需用east_asian_width或ord范围显式判断并映射。

全角半角混杂的文本会破坏字符串匹配、分词、正则提取和模型输入一致性——unicodedata 是 Python 标准库中唯一无需额外依赖就能做底层 Unicode 归一化处理的模块,但直接调用 normalize() 并不能解决全半角问题,必须配合字符映射或条件转换。
为什么 unicodedata.normalize() 对全半角无效
unicodedata.normalize() 处理的是 Unicode 等价性(如 NFC/NFD 中的组合字符),而全角数字 0(U+FF10)和半角 0(U+0030)在 Unicode 中属于不同码位、无标准化关系。试图用 normalize('NFKC', s) 虽然能转换部分全角标点(如全角逗号 → 半角),但对全角 ASCII 字符(字母、数字)效果不稳定,尤其在 CPython 3.9+ 后部分全角拉丁字符不再被 NFKC 自动转为半角。
实际清洗必须显式判断字符类别并映射:
- 用
unicodedata.east_asian_width(c)判断是否为全宽字符(返回'F'或'W') - 用
ord(c)范围判断是否属于全角 ASCII 区(0xFF01–0xFF5E对应 !–~,0xFF10–0xFF19对应 0–9) - 避免依赖
str.translate()预制表——不同项目对“是否转换全角字母”需求不同(如人名保留全角 A 更稳妥)
如何安全地只转换全角数字和常用标点
多数 NLP 场景只需统一数字和基础符号,保留全角汉字与英文字母(避免把「ABC」错误转成「ABC」导致 OCR 校验失败)。推荐按需构造映射字典,而非全局替换:
立即学习“Python免费学习笔记(深入)”;
def halfwidth_digits_punct(s):
result = []
for c in s:
o = ord(c)
# 全角数字 0-9 → 半角 0-9
if 0xFF10 <= o <= 0xFF19:
result.append(chr(o - 0xFEE0))
# 全角 ASCII 标点(!-~)→ 半角
elif 0xFF01 <= o <= 0xFF5E:
result.append(chr(o - 0xFEE0))
else:
result.append(c)
return ''.join(result)
注意:0xFEE0 是全角 ASCII 区到半角区的固定偏移量(0xFF01 - 0x0021 == 65248 == 0xFEE0),但该偏移不适用于全角平假名/片假名或中文字符——它们没有对应半角形式,强行减会得到乱码。
用 unicodedata.category() 辅助识别可安全转换的字符
比硬编码范围更健壮的方式是结合 Unicode 类别判断:全角数字属 'Nd'(Number, decimal digit),全角标点多属 'Pc'(connector)、'Pd'(dash)、'Pe'(close punctuation)等。可先过滤出这些类别再做宽度判断:
import unicodedata
<p>def smart_halfwidth(s):
result = []
for c in s:
cat = unicodedata.category(c)
if cat == 'Nd' and unicodedata.east_asian_width(c) in ('F', 'W'):</p><h1>只对全宽数字转半角</h1><pre class="brush:php;toolbar:false;"> result.append(chr(ord(c) - 0xFEE0))
elif cat in ('Pc', 'Pd', 'Pe', 'Pf', 'Pi', 'Po', 'Ps') and unicodedata.east_asian_width(c) == 'F':
# 全宽标点转半角(不含中文引号等 W 宽度标点)
result.append(chr(ord(c) - 0xFEE0))
else:
result.append(c)
return ''.join(result)
关键细节:east_asian_width(c) == 'F' 比 in ('F', 'W') 更严格——'W'(wide)包含汉字和日文平假名,不应参与转换;而 'F'(fullwidth)才是真正的全角 ASCII 映射区。
常见错误:误将中文引号、破折号当标点清洗
全角中文标点如「“」(U+201C)、」(U+201D)、——(U+2014)虽属 'Pi'/'Pf'/'Pd' 类别,但 east_asian_width() 返回 'W' 而非 'F',因此上例中的 smart_halfwidth() 不会动它们——这是正确行为。若业务强制要求统一为英文引号,必须单独处理,不能依赖宽度判断。
另一个坑是正则匹配:r'[0-9]+' 这种写法在 Python 中无法匹配全角数字区间(Unicode 码位不连续),必须用 r'[\uFF10-\uFF19]+' 或预编译 re.compile(r'[\uFF10-\uFF19]+')。
真正难处理的是混合场景:比如「价格:123元」——数字要转半角,但「元」字不能动,冒号如果是全角也要转。这时候单一规则不够,得先分词或用 regex 模块支持 \p{Nd} 等 Unicode 属性匹配,不过那就超出 unicodedata 职责了。


















