☰
Python处理Unicode字符串的基本方法详解
2026/10/13 10:32:50 网站建设 项目流程

前言


会写s.upper()、s.split()不等于会处理 Unicode 字符串。真正难的场景是这些:两个看起来一字不差的字符串,==却返回False;对一批中文按「拼音顺序」排,sorted()排出来的完全不是那个顺序;从不同系统拿到的同一条数据,一个带 BOM 一个不带,拼接后多了个看不见的字符。


这些问题的根子在于:Unicode 里同一个「看得见的字符」,可以有多种码位表示法。本文讲四组工具——ord/chr、unicodedata的属性查询、规范化(NFC / NFD)、以及宽度与排序——它们就是应付这类问题的基本手段。


前置说明:本机无 Python 解释器,示例为逐行推演,未运行。所引用的unicodedata函数名与行为均对照 Python 官方unicodedata文档。


版本前提也要讲清:Python 2.7 已于 2020 年 1 月 1 日停止维护。Python 2 里str是字节串、unicode才是文本,写文本要加u"..."前缀,ord()对字节串返回的是字节值而非码位——这些在 Python 3 里都变了:str统一为文本,u"..."前缀不再有意义。本文全部按 Python 3 表述。


一、ord 与 chr:在码位层面读写


# 适用于 Python 3.8+
print(ord("A")) # 65
print(chr(65)) # 'A'
print(hex(ord("中"))) # 0x4e2d
print(ord("中")) # 20013


  • ord(c)接受一个长度为 1 的字符串,返回它的码位整数。

  • chr(i)接受一个整数,返回对应的单字符字符串。


两者是一对逆运算。注意ord的参数必须是长度恰好为 1的字符串——传"ab"会抛TypeError。另外len("中") == 1,因为 Python 3 的len()数的是码位,不是字节;想知道字节数得len("中".encode("utf-8"))(结果为 3)。


二、unicodedata:查询字符属性


unicodedata是标准库模块,提供对 Unicode 字符数据库(UCD)的查询。常用函数:




函数返回例子



name(chr)字符的官方名字name("A")为'LATIN CAPITAL LETTER A'

category(chr)双字母的通用类别category("A")为'Lu'

east_asian_width(chr)东亚宽度east_asian_width("中")为'W'

decomposition(chr)分解映射(字符串)decomposition("Ã")为'0041 0303'

combining(chr)组合类(整数)无组合类时为0

numeric(chr)数值(浮点)numeric("½")为0.5

normalize(form, unistr)规范化后的字符串见下一节

is_normalized(form, unistr)是否已是该规范形需要 Python 3.8+



category()返回的两个字母含义很实用:第一个字母是大类(L字母、N数字、P标点、Z分隔符、C其他……),第二个是子类(u小写、l大写、d十进制数字……)。所以category("A")是'Lu',读作「Letter, uppercase」。


# 适用于 Python 3.8+
import unicodedata as ud

print(ud.name("A")) # LATIN CAPITAL LETTER A
print(ud.category("A")) # Lu
print(ud.category("中")) # Lo —— Letter, other
print(ud.decomposition("Ã")) # 0041 0303

decomposition("Ã")返回'0041 0303':说明「Ã」可以分解成 U+0041(A)加 U+0303(组合波浪号)。这正是下一节要讲的「一个字符、多种表示」的来源。


三、规范化:NFC 与 NFD


规范化(normalization)就是把这些等价的表示法统一成一种标准形式。unicodedata.normalize(form, unistr)的form有四个合法值,官方文档的定义是:




形式全称做什么



NFDNormalization Form D规范分解:把预组合字符拆成基字符 + 组合字符

NFCNormalization Form C先规范分解,再把可组合的重新组合

NFKDNormalization Form KD兼容分解:连兼容字符(如罗马数字、全角形式)也替换成等价物

NFKCNormalization Form KC先兼容分解,再规范组合



关键区别在「规范」与「兼容」:



  • 规范等价(NFC / NFD 关心的):两个表示就是同一个字符,只是写法不同,规范后必然相等。

  • 兼容等价(NFKC / NFKD 关心的):两个字符在某些用途下可以互换,但语义上未必相同。官方文档举的例子是罗马数字「Ⅰ」(U+2160)和字母「I」(U+0049)——它们看起来一样,但罗马数字是「为了兼容旧字符集(如 GB2312)」才保留的独立字符。用 NFKC 会把「Ⅰ」变成「I」,这会改变语义,要谨慎。


# 适用于 Python 3.8+
import unicodedata as ud

s1 = "é" # 预组合:单个码位 U+00E9
s2 = "é" # 分解形式:e 加组合重音符 U+0301

print(s1 == s2) # False —— 看起来一样,却不相等
print(len(s1), len(s2)) # 1 2 —— 长度也不同

print(ud.normalize("NFC", s1) == ud.normalize("NFC", s2)) # True
print(ud.normalize("NFD", s1) == ud.normalize("NFD", s2)) # True

这就是「看起来一样但不相等」的经典陷阱:要先统一规范形,再比较。同一份数据在不同系统、不同输入法、不同复制来源下,可能是 NFC 也可能是 NFD。


is_normalized(form, unistr)(3.8+)用来判断一个字符串是否已经是某个规范形,可以在做昂贵的normalize之前先探测:


# 适用于 Python 3.8+
if not ud.is_normalized("NFC", s):
s = ud.normalize("NFC", s)

四、中文排序与东亚宽度


4.1 默认排序按码位


Python 的sorted()对字符串比较时,用的是码位顺序。对中文来说,这意味着它既不按拼音、也不按笔画,而是按 Unicode 表里的编号:


# 适用于 Python 3.8+
words = ["中", "啊", "兵"]
print([hex(ord(w)) for w in words]) # ['0x4e2d', '0x554a', '0x5175']
print(sorted(words)) # ['中', '兵', '啊'] —— 码位顺序

按码位,中(0x4E2D)<兵(0x5175)<啊(0x554A)。如果按拼音,顺序应该是「啊(a) < 兵(bing) < 中(zhong)」,两者对不上。想要拼音排序,标准库本身不提供,需要引入第三方拼音库或自建映射表(具体用法以该库官方文档为准)。


标准库里有locale.strxfrm/locale.strcoll可以做区域相关的比较,但它们依赖locale.setlocale设置的全局区域,行为随平台和系统区域变化,且设置 locale 会影响整个进程,不是线程安全的。用它之前要清楚这些限制。


4.2 东亚宽度:对齐和截断都要用


east_asian_width()返回字符在东亚语境下的显示宽度分类(对应 Unicode 标准的 East_Asian_Width 属性):




值含义



"W"Wide,宽(如中日韩汉字)

"F"Fullwidth,全角(如全角标点 U+FF01)

"H"Halfwidth,半角(如半角片假名)

"Na"Narrow,窄(如 ASCII)

"A"Ambiguous,歧义(在东亚语境下按宽算)

"N"Neutral,中立(如一般符号)



# 适用于 Python 3.8+
import unicodedata as ud

print(ud.east_asian_width("中")) # 'W'
print(ud.east_asian_width("A")) # 'Na'
print(ud.east_asian_width(",")) # 'W' —— 全角逗号按东亚宽度算宽

用处很直接:在等宽终端里排表格,或者按「显示宽度」截断字符串时,汉字要按 2 个字符宽度算,ASCII 按 1 个。如果按len()截断,一个汉字占一个len却占两个显示位,表格会歪。注意"A"(Ambiguous)这一类的处理是有语境依赖的,需要你自己决定按 1 还是按 2 算。


常见坑点



  1. 直接比较两个「看起来一样」的字符串


❌"é" == "é"返回False,还以为数据有问题。


✅ 双方都unicodedata.normalize("NFC", s)之后再比。



  1. 用len()当「可见字符数」


❌ 用len(s)去截断显示文本,遇到组合字符会把一个字符截半。


✅ 需要「用户感知的字符数」时要考虑组合字符,长度问题用east_asian_width/ 规范化配合处理。



  1. 不加区分地用 NFKC


❌ 对全文做 NFKC,把「Ⅰ」「①」「f」这类兼容字符全改掉,语义丢失。


✅ 只在明确需要兼容化(如搜索索引)时用 NFKC;一般比较用 NFC。



  1. 以为sorted()会按拼音排中文


❌sorted(["中","啊","兵"])期待「啊兵中」。


✅ 默认是码位顺序;拼音排序需第三方库或自建映射。



  1. ord传了多字符字符串


❌ord("ab")抛TypeError。


✅ord只接受长度为 1 的字符串。



  1. 把str长度当字节数


❌ 用len(s)估算缓冲区大小,中文被截半。


✅ 字节问题用len(s.encode("utf-8"))。



  1. 忘了 BOM 也会变成一个字符


❌ 用utf-8解码带 BOM 的文本,开头多一个不可见的 U+FEFF。


✅ 用utf-8-sig解码,它会跳过 BOM。



  1. 依赖is_normalized的版本


❌ 在 Python 3.7 上调用unicodedata.is_normalized,报AttributeError。


✅ 它需要 Python 3.8 及以上;更早版本只能比较normalize前后的结果。


总结




工具用途关键点



ord/chr码位与字符互转只处理长度 1 的字符串

unicodedata.category判断字符大类两个字母,如Lu

unicodedata.normalize统一表示法NFC / NFD 是规范,NFKC / NFKD 是兼容

unicodedata.east_asian_width算显示宽度汉字是W,ASCII 是Na

sorted排序默认按码位,不是拼音



处理 Unicode 字符串的核心,是承认「同一个字符可以有多种表示」。一旦接受这点,比较前先规范化、截断时看显示宽度、排序时别指望拼音,这些做法就都顺理成章了。反过来,把str当成「一个字符就是一个位置」的朴素数组,迟早会在某个带组合字符或 BOM 的输入上栽跟头。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询