第四章 Unicode 文本和字节序列
定义
Unicode
字符串是字符的序列,但字符的准确定义依赖于 Unicode 标准。在 Python 3 中,str 类型表示 Unicode 字符串,每个元素是一个 Unicode 字符。
Unicode 将字符的身份与存储形式分离,码位(Code Point) 是字符的唯一标识,用 U+XXXX(4–6 位十六进制)表示,范围为 U+0000 到 U+10FFFF(共 1,114,112 个可能值),如'A' → U+0041 、'€' → U+20AC '𝄞'(高音谱号)→ U+1D11E。
编码(Encoding) 是将码位转换为字节序列的规则。不同编码方式(如 UTF-8、UTF-16)对同一字符可能产生不同字节表示。 例如字符 'A'(U+0041),UTF-8 → \x41(1 字节);UTF-16LE → \x41\x00(2 字节)。
编码与解码
- 编码(encode):
str→bytes(用于存储或传输) - 解码(decode):
bytes→str(用于人类可读)
s = 'café' # 4 个 Unicode 字符
b = s.encode('utf8') # 转为 bytes,长度为 5('é' 占 2 字节)
decoded = b.decode('utf8') # 还原为原始字符串
字节基础
特性
Python 3 引入**bytes,不可变**。bytes中的每个元素是 0–255 的整数(不是字符)。对bytes进行切片操作返回同类型对象,即使长度为 1。
cafe = bytes('café', encoding='utf-8') # b'caf\xc3\xa9'
cafe # b'caf\xc3\xa9' 前三个字节 `b'caf'` 属于可打印 ASCII 范围,而后两个字节则不是 参考下文字面量显示规则
cafe[0] # 99 是一个整数
cafe[:1] # b'c' 仍是一个bytes对象
字面量显示规则
二进制序列的显示方式取决于字节值:
- 32–126(可打印 ASCII):直接显示字符(如
b'caf')。 - 特殊控制字符:用转义序列表示(
\t,\n,\r,\\)。 - 引号处理:若同时含
'和",整体用单引号,内部'转义为\'。 - 其他字节:用十六进制转义(如
\xc3,\xa9)。
编码/解码问题处理指南
同一字符串经不同编码会生成完全不同的字节表示:
for codec in ['latin_1', 'utf_8', 'utf_16']:
print(codec, 'El Niño'.encode(codec), sep='\t')
输出:
latin_1 b'El Ni\xf1o'
utf_8 b'El Ni\xc3\xb1o'
utf_16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
ASCII、Latin-1等传统编码无法表示所有 Unicode 字符。 UTF 系列(UTF-8/UTF-16)是唯一能覆盖全部 Unicode 码位的编码方案。
Python 中 Unicode 相关错误主要有三类:
UnicodeEncodeError:str→bytes时,字符无法用目标编码表示。UnicodeDecodeError:bytes→str时,字节序列不符合目标编码规则。SyntaxError:加载.py源文件时,文件编码与 Python 默认(UTF-8)不符且未声明。
UnicodeEncodeError
非 UTF 编码(如 cp437)仅支持有限字符集。当字符无法编码时,默认抛出异常,但可通过 errors 参数指定策略:
city = 'São Paulo'
# 成功编码
print(city.encode('utf_8')) # b'S\xc3\xa3o Paulo'
print(city.encode('utf_16')) # b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
print(city.encode('iso8859_1')) # b'S\xe3o Paulo'
# 失败与错误处理
try:
city.encode('cp437') # 抛出 UnicodeEncodeError
except UnicodeEncodeError as e:
print("Error:", e)
# 跳过无法编码的字符 会导致静默的数据丢失
print(city.encode('cp437', errors='ignore')) # b'So Paulo'
# 用 ? 替换无法编码的字
print(city.encode('cp437', errors='replace')) # b'S?o Paulo'
# 用 XML 字符实体(如 ã)替换无法编码的字符
print(city.encode('cp437', errors='xmlcharrefreplace')) # b'São Paulo'
UnicodeDecodeError
UTF-8/UTF-16结构严格,非法字节会抛出 UnicodeDecodeError。传统 8 位编码(如 cp1252, koi8_r)可解码任意字节,静默生成乱码(mojibake)。
octets = b'Montr\xe9al' # 实际为 latin1 编码的 "Montréal"
# 使用错误的 8 位编码会无提示地产生错误文本
print(octets.decode('cp1252')) # 'Montréal'(正确)
print(octets.decode('iso8859_7')) # 'Montrιal'(希腊语乱码)
print(octets.decode('koi8_r')) # 'MontrИal'(俄语乱码)
try:
octets.decode('utf_8') # 抛出 UnicodeDecodeError
except UnicodeDecodeError as e:
print("UTF-8 decode error:", e)
# 使用 Unicode 官方的替换字符 码位 U+FFFD表示未知
print(octets.decode('utf_8', errors='replace')) # 'Montral'
源文件编码与 SyntaxError
Python 3 默认源码编码为 UTF-8(跨平台一致)。若文件含非 UTF-8 字节且未声明编码,会报 SyntaxError。
SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line 1,
but no encoding declared; see https://python.org/dev/peps/pep-0263/ for details
最好的修复方式是将文件转为 UTF-8,也可以在文件顶部添加编码声明来进行临时处理。
# coding: cp1252
print('Olá, Mundo!')
检测未知编码
**未知编码无法 100% 确定,必须被告知。**但可借助线索:
- HTTP/XML 等协议常在头部声明编码;
- ASCII编码中不包含大于127的字节值;
- UTF-8 结构严谨,能成功解码通常就是 UTF-8;
- 频繁出现
b'\x00'→ 可能是 UTF-16/32; b'\x20\x00'→ 可能是 UTF-16LE 的空格
chardet 库(基于统计模型)可以用于编码检测
$ chardetect myfile.txt
myfile.txt: utf-8 with confidence 0.99
BOM
BOM(Byte Order Mark,字节顺序标记)是一个特殊的 Unicode 字符:U+FEFF(ZERO WIDTH NO-BREAK SPACE)。被用作字节序指示器,主要出现在 UTF-16 和 UTF-32 编码的文件开头。由于 Unicode 未分配 U+FFFE(即 BOM 的字节反转形式),编解码器可通过检测 b'\xff\xfe' 或 b'\xfe\xff' 来判断字节序。
u16 = 'El Niño'.encode('utf_16')
print(u16) # b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
print(list(u16)) # [255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
开头的 b'\xff\xfe' 表示小端序(Little Endian),即低字节在前。字母 'E'(U+0045)被编码为 [69, 0],而非大端序的 [0, 69]。
为避免依赖 BOM,Unicode 定义了两种显式字节序的编码格式:
utf_16le:强制

最低0.47元/天 解锁文章
1398

被折叠的 条评论
为什么被折叠?



