专题:Python标准库精讲系统学习
关键词:Python, 标准库, unicodedata, Unicode, 字符编码, lookup, category, 字符属性, normalize
一、unicodedata模块概述
Unicode是全球统一的字符编码标准,旨在为世界上所有文字系统中的每个字符分配一个唯一的数字标识(码点)。截至目前,Unicode已收录超过15万个字符,涵盖古今中外几乎所有书写系统的文字、符号和表情。Python的 unicodedata 模块提供了直接访问Unicode字符数据库(UCD)的接口,是进行文本处理、字符分析和编码转换时不可或缺的标准库工具。
该模块的核心价值在于:它将Unicode联盟维护的海量字符元数据以Python函数的形式暴露给开发者,使我们无需手动查阅Unicode规范即可获取字符的名称、分类、数值、双向性、组合类等关键属性。无论是做自然语言处理、文本规范化、字符验证,还是编写支持国际化的应用程序,unicodedata 都扮演着基础而重要的角色。
Python解释器在编译时绑定了特定版本的Unicode标准。可以通过 unicodedata.unidata_version 查看当前Python所使用的Unicode版本号。不同Python版本可能绑定不同版本的Unicode标准,因此在处理较新定义的字符时需要留意兼容性问题。
>>> import unicodedata
>>> unicodedata.unidata_version
'15.0.0'
模块中的函数大体可分为三类:字符信息查询(名称、数值等)、字符分类(类别、双向性等)、以及字符串规范化。接下来的章节将逐一深入讲解。
二、字符信息查询
2.1 lookup() — 通过名称查找字符
unicodedata.lookup(name) 函数接收一个Unicode字符的官方名称(区分大小写),返回对应的字符。这是从名称到字符的逆向查询,非常适合动态生成特殊符号。
>>> unicodedata.lookup('LATIN CAPITAL LETTER A')
'A'
>>> unicodedata.lookup('SNOWMAN')
'☃'
>>> print(unicodedata.lookup('SNOWMAN'))
☃
>>> unicodedata.lookup('GRINNING FACE')
'😀'
>>> unicodedata.lookup('MUSICAL SYMBOL G CLEF')
'𝄞'
如果传入的名称不存在,会抛出 KeyError 异常。名称中可以使用连字符和空格,但必须与Unicode官方名称完全一致。此功能在需要将配置文件或数据库中的字符名称转换为实际字符时特别有用。
2.2 name() — 获取字符的官方名称
unicodedata.name(chr[, default]) 返回字符的Unicode官方名称。如果字符没有名称(极少见,如某些私有使用区的字符),则返回 default 参数指定的值;未提供 default 时抛出 ValueError。
>>> unicodedata.name('A')
'LATIN CAPITAL LETTER A'
>>> unicodedata.name('中')
'CJK UNIFIED IDEOGRAPH-4E2D'
>>> unicodedata.name('😊')
'SMILING FACE WITH SMILING EYES'
>>> unicodedata.name('\n', '<unknown>')
'<unknown>'
>>> unicodedata.name('$')
'DOLLAR SIGN'
从结果可以看出,CJK统一表意文字的名称格式为 CJK UNIFIED IDEOGRAPH-XXXX,其中 XXXX 为该字符的十六进制码点。这一命名规则意味着数千个汉字各自拥有唯一的名称,非常便于在程序中准确引用特定字符。
2.3 decimal() / digit() / numeric() — 数值信息
这三个函数用于查询字符所代表的数值,但各自的适用范围和返回值类型有所不同:
decimal(chr[, default]):返回字符对应的十进制整数值,仅适用于十进制数字字符(如0-9、阿拉伯-印度数字等)。不适用时返回 default 或抛出 ValueError。
digit(chr[, default]):返回字符对应的整数值,适用范围比 decimal 更广,包括上标/下标数字、圈数字等特殊数字符号。
numeric(chr[, default]):返回字符对应的数值(float 类型),适用范围最广,包括分数、罗马数字、中文数字等一切具有数值含义的字符。
>>> unicodedata.decimal('5')
5
>>> unicodedata.decimal('٣') # 阿拉伯文数字3
3
>>> unicodedata.digit('⁵') # 上标5
5
>>> unicodedata.numeric('½') # 二分之一
0.5
>>> unicodedata.numeric('Ⅷ') # 罗马数字8
8.0
>>> unicodedata.numeric('七') # 中文数字七
7.0
>>> unicodedata.numeric('十')
10.0
这三个函数的层级关系可以形象地理解为:decimal 是 digit 的子集,digit 又是 numeric 的子集。也就是说,凡是能通过 decimal 获取数值的字符,一定也能通过 digit 和 numeric 获取;反之则不然。
三、字符分类
3.1 category() — 获取字符的通用分类
unicodedata.category(chr) 返回一个长度为2的字符串,表示字符的Unicode通用分类(General Category)。分类码由一个大写字母和一个小写字母组成:首字母代表大类,次字母代表子类。
以下是常见的分类码及其含义:
| 分类码 | 含义 | 示例 |
| Lu | 大写字母(Letter, uppercase) | A, B, 中(不对应此分类) |
| Ll | 小写字母(Letter, lowercase) | a, b, z |
| Lt | 首字母大写(Letter, titlecase) | Dž, Lj |
| Lo | 其他字母(Letter, other) | 中, あ, 한 |
| Nd | 十进制数字(Number, decimal digit) | 0, 1, 9, ٣ |
| Nl | 数字字母(Number, letter) | Ⅰ, Ⅴ, Ⅹ |
| No | 其他数字(Number, other) | ½, ⁵, ₃ |
| Sc | 货币符号(Symbol, currency) | $, ¥, €, £ |
| So | 其他符号(Symbol, other) | ©, ♥, 😊 |
| Zs | 空格分隔符(Separator, space) | (普通空格) |
| Po | 其他标点(Punctuation, other) | !, ., ? |
| Cn | 未分配(Other, not assigned) | 未使用的码点 |
>>> unicodedata.category('A')
'Lu'
>>> unicodedata.category('中')
'Lo'
>>> unicodedata.category('1')
'Nd'
>>> unicodedata.category('$')
'Sc'
>>> unicodedata.category('😊')
'So'
>>> unicodedata.category(' ')
'Zs'
>>> unicodedata.category(',')
'Po'
利用 category() 可以方便地判断字符类型。例如:以 'L' 开头的是字母,以 'N' 开头的是数字,以 'S' 开头的是符号,以 'P' 开头的是标点。这是编写字符过滤、文本清洗功能时的核心工具。
3.2 bidirectional() — 字符的双向性
Unicode双向算法(BiDi Algorithm)用于处理混合了从左到右(如英文)和从右到左(如阿拉伯文、希伯来文)文字的文本显示。unicodedata.bidirectional(chr) 返回字符的双向性类别。
>>> unicodedata.bidirectional('A') # 强左到右
'L'
>>> unicodedata.bidirectional('א') # 希伯来字母(强右到左)
'R'
>>> unicodedata.bidirectional('$') # 货币符号(弱左到右)
'ET'
>>> unicodedata.bidirectional('(') # 括号(其他中性)
'ON'
>>> unicodedata.bidirectional(' ') # 空格(空白分隔符)
'WS'
常见的返回值包括:'L'(左到右)、'R'(右到左)、'AL'(阿拉伯文字母,右到左)、'EN'(欧洲数字)、'AN'(阿拉伯数字)、'ET'(欧元/货币终结符)、'ON'(其他中性)等。在多语言文本渲染或编写BiDi算法相关工具时,这个函数非常关键。
3.3 combining() — 组合类
unicodedata.combining(chr) 返回字符在Unicode规范中的组合类(Combining Class),是一个整数值。组合类用于描述基字符和组合用附加符号(如变音符号)之间的叠加组合关系。值为0表示该字符不是组合用字符;非零值表示该字符是组合用字符,并定义了其在组合序列中的相对位置。
>>> unicodedata.combining('A') # 普通字符,非组合用
0
>>> unicodedata.combining('́') # 尖音符组合用附加符号
230
>>> unicodedata.combining('̈') # 分音符组合用附加符号
230
>>> unicodedata.combining('̧') # 软尾符组合用附加符号
202
组合类的值范围是0到255,数值越小越靠近基字符。例如,́(COMBINING ACUTE ACCENT)和 ̈(COMBINING DIAERESIS)的组合类都是230,表示它们属于"上方附加符号"的类别。这一信息在文本渲染引擎、字体排印和文本规范化处理中有重要应用。
四、Unicode规范化
4.1 normalize() — 四种规范化形式
Unicode中存在一个重要的概念:同一字符可能通过不同的码点序列来表示。例如,字符 "é" 既可以作为一个单独的预组合字符(U+00E9),也可以由基字符 "e"(U+0065)加上组合用尖音符 "́"(U+0301)组合而成。这种表示方式的不一致会给文本比较、搜索、存储带来很大困扰。Unicode规范化正是为了解决这个问题而设计的。
unicodedata.normalize(form, string) 接收一个规范化形式参数和一个字符串,返回规范化后的字符串。Python支持四种规范化形式:
| 形式 | 名称 | 行为 | 适用场景 |
| NFC | Normalization Form C(组合) | 优先使用预组合字符 | 常规文本处理、显示 |
| NFD | Normalization Form D(分解) | 将字符分解为基字符+组合符 | 文本分析、音调处理 |
| NFKC | Normalization Form KC(兼容组合) | 先兼容分解,再组合 | 搜索、数据清洗 |
| NFKD | Normalization Form KD(兼容分解) | 先兼容分解,保持分解 | 数据归一化、严格比较 |
>>> from unicodedata import normalize
>>> # NFC vs NFD 示例
>>> nfc = normalize('NFC', 'é') # e + 组合尖音符
>>> nfd = normalize('NFD', 'é') # 预组合é
>>> nfc, nfd
('é', 'é')
>>> nfc == nfd
False
>>> nfc == 'é'
True
>>> # NFKC / NFKD 的兼容分解示例
>>> normalize('NFKC', '①') # 圈数字1 → 普通1
'1'
>>> normalize('NFKD', '½') # 分数 → 1/2 字符串
'1/2'
>>> normalize('NFKC', 'fi') # 连字fi → f + i
'fi'
>>> normalize('NFKC', '⁵') # 上标5 → 普通5
'5'
>>> # 全角/半角转换
>>> normalize('NFKC', 'ABC') # 全角字母 → 半角
'ABC'
>>> normalize('NFKC', ',。') # 全角标点 → 半角
',.'
在实战中,NFKC是最常用的规范化形式——它可以将许多视觉上不同但语义上等价的形式统一为基础形式,非常适合用于搜索引擎索引、用户输入清洗、数据去重等场景。但要注意,NFKC/NFKD可能会丢失格式信息(如上标/下标),在某些场景下需要谨慎使用。
核心要点:四种规范化形式可划分为两个维度:组合(C)vs 分解(D)和 标准(C/D)vs 兼容(KC/KD)。NFC/NFD仅处理等效的编码方式,不改变字符的语义含义;而NFKC/NFKD会进一步将兼容性字符分解为更基础的形式,可能会改变字符的视觉表现。选择哪种形式取决于具体业务需求。
五、实战应用
5.1 中文/日文字符检测
利用 category() 和 name() 可以精确判断字符所属的文字系统。CJK统一表意文字(中日韩越共享)的类别均为 'Lo'(其他字母),其名称以 'CJK UNIFIED IDEOGRAPH' 开头。而日文假名(平假名、片假名)则属于不同的分类范围。
def is_cjk_unified(ch):
"""判断是否为CJK统一表意文字(中日韩越共享汉字)"""
return 'CJK UNIFIED IDEOGRAPH' in unicodedata.name(ch, '')
def is_hiragana(ch):
"""判断是否为平假名"""
return 'HIRAGANA' in unicodedata.name(ch, '')
def is_katakana(ch):
"""判断是否为片假名"""
return 'KATAKANA' in unicodedata.name(ch, '')
def is_japanese(ch):
"""判断是否为日文字符(假名或汉字)"""
name = unicodedata.name(ch, '')
return any(kw in name for kw in
['HIRAGANA', 'KATAKANA', 'CJK UNIFIED IDEOGRAPH'])
# 测试
print(is_cjk_unified('中')) # True
print(is_hiragana('あ')) # True
print(is_katakana('ア')) # True
print(is_japanese('中')) # True
print(is_japanese('あ')) # True
print(is_japanese('A')) # False(全角英文字母)
5.2 Emoji识别与处理
随着社交媒体的普及,Emoji的识别和处理已成为文本处理中的重要需求。虽然Unicode对Emoji的分类较为复杂(分散在多个区块中),但可以通过 category() 初步筛查,再结合特定码点范围或名称进行精确判定。
import unicodedata
def is_emoji(ch):
"""简单判定是否为Emoji字符"""
# Emoji通常属于 So(其他符号)或一些特殊类别
cat = unicodedata.category(ch)
if cat not in ('So', 'Sk', 'Mn'):
return False
name = unicodedata.name(ch, '')
# 排除非Emoji的符号
excluded_prefixes = (
'MUSICAL SYMBOL', 'CJK', 'ANGLE', 'COPYRIGHT',
'REGISTERED', 'TRADE MARK', 'DINGBAT'
)
if name.startswith(excluded_prefixes):
return False
# 包含 EMOJI 关键词的肯定是
if 'EMOJI' in name or 'FACE' in name or 'HAND' in name:
return True
# 常见的Emoji区块范围(简化版)
codepoint = ord(ch)
if 0x1F600 <= codepoint <= 0x1F64F: # 表情符号
return True
if 0x1F300 <= codepoint <= 0x1F5FF: # 杂项符号
return True
if 0x2600 <= codepoint <= 0x27BF: # 杂项符号
return True
return False
def extract_emojis(text):
"""从文本中提取所有Emoji字符"""
return [ch for ch in text if is_emoji(ch)]
# 测试
text = "今天的天气真好!😊☀️一起出去玩吧🎉"
print(extract_emojis(text))
# ['😊', '☀', '🎉']
注意:关于Emoji的检测是一个不断演进的话题。Unicode每个新版本都会增加新的Emoji字符。更完整的方案建议使用第三方库如 emoji 或直接维护一个动态更新的Emoji数据集合。
5.3 文本清洗与规范化
在实际应用开发中,用户输入的文本往往包含各种格式不一致的字符。使用 unicodedata.normalize('NFKC', ...) 可以将全角字母、数字、标点统一为半角形式,将兼容性字符(如上标、圈数字)转换为普通形式,极大地简化后续处理逻辑。
def clean_text(text):
"""清理和规范化文本"""
# 1. NFKC 规范化:统一全角/半角、兼容分解
text = unicodedata.normalize('NFKC', text)
# 2. 过滤控制字符(保留常见的空白字符)
cleaned = []
for ch in text:
cat = unicodedata.category(ch)
# 排除Cc(控制字符)但保留 Cf(格式字符)中的常见换行等
if cat == 'Cc' and ch not in ('\n', '\r', '\t'):
continue
cleaned.append(ch)
return ''.join(cleaned)
def get_display_width(text):
"""估算字符串的显示宽度(全角/半角混合场景)"""
width = 0
for ch in text:
cp = ord(ch)
if unicodedata.east_asian_width(ch) in ('W', 'F'):
width += 2 # 全角/宽字符
else:
width += 1 # 半角/窄字符
return width
# 测试
dirty = "Hello,world!①⓶③ 测试 "
print(repr(clean_text(dirty)))
# 'Hello, world!123 测试'
print(get_display_width("Hello, 世界!"))
# 15(H=1,e=1,l=1,l=1,o=1,,=1, =1,世=2,界=2,!=1)
unicodedata.east_asian_width(ch) 返回字符的东亚宽度属性,取值范围包括:'F'(全宽)、'H'(半宽)、'W'(宽)、'Na'(窄)、'A'(模棱两可)和 'N'(中立)。这一信息在终端排版、表格对齐、文本截断等场景中非常实用。
5.4 案例:构建字符信息查询工具
综合运用以上知识,可以编写一个字符信息查询函数,一次性展示字符的多种属性:
def char_info(ch):
"""显示字符的完整Unicode属性信息"""
name = unicodedata.name(ch, '<无名称>')
cat = unicodedata.category(ch)
bid = unicodedata.bidirectional(ch)
comb = unicodedata.combining(ch)
dec = unicodedata.decimal(ch, '<非十进制>')
dig = unicodedata.digit(ch, '<非数字>')
num = unicodedata.numeric(ch, '<无数值>')
eaw = unicodedata.east_asian_width(ch)
return f"""
字符: {ch}
码点: U+{ord(ch):04X}
名称: {name}
分类: {cat}
双向性: {bid}
组合类: {comb}
十进制值: {dec}
数字值: {dig}
数值: {num}
东亚宽度: {eaw}
"""
# 测试
print(char_info('中'))
print(char_info('😊'))
print(char_info('½'))
六、核心总结
1. 模块定位:unicodedata 是Python访问Unicode字符数据库的标准接口,提供字符名称、分类、数值、双向性、组合类等元数据的查询功能,以及字符串规范化功能。
2. 字符信息查询四函数:lookup() 通过名称查字符,name() 通过字符查名称,decimal()/digit()/numeric() 层级递进地获取字符的数值信息。
3. 字符分类三剑客:category() 返回通用分类(Lu/Ll/Nd/Sc等),bidirectional() 返回文本方向性,combining() 返回组合附加符号类值。
4. 规范化四形式:NFC(组合)、NFD(分解)、NFKC(兼容组合)、NFKD(兼容分解)。NFKC在实际开发中使用最广泛,适用于文本标准化、搜索索引和用户输入清洗。
5. 实战价值:CJK字符检测、Emoji识别、文本清洗、全角/半角统一、字符显示宽度计算等场景均离不开 unicodedata 模块的支持。
6. 注意事项:Unicode标准版本随Python版本更新而演进,跨版本部署时需关注 unicodedata.unidata_version 的差异;Emoji检测逻辑需要随Unicode标准同步更新;NFKC/NFKD规范化会丢失格式信息,需根据业务场景权衡使用。