第四章 文本和字节序列
人类使用文本,计算机使用字节序列。——Esther Nam 和 Travis Fischer1
Python3明确区分了人类可读的文本字符串和原始的字节序列。隐式地把字节序列转换成Unicode
文本已成过去。本章将要讨论Unicode
字符串、二进制序列,以及在二者之间转换时使用的编码。
深入理解Unicode
对你可能十分重要,也可能无关紧要,这取决于Python编程的场景。说到底,本章涵盖的问题对只处理ASCII
文本的程序员没有影响。但是即便如此,也不能避而不谈字符串和字节序列的区别。此外,你会发现专门的二进制序列类型所提供的功能,有些Python 2中“全功能”的str
类型不具有的。
- 字符、码位和字节表述
bytes
、bytearray
和memoryview
等二进制序列的独特特性- 全部
Unicode
和陈旧字符集的编解码器 - 避免和处理编码错误
- 处理文本文件的最佳实践
- 默认编码的陷阱和标准 I/O 的问题
- 规范化
Unicode
文本,进行安全的比较
字符问题
“字符串”是个相当简单的概念:一个字符串是一个字符序列。问题出在“字符”的定义上。
在 2015 年,“字符”的最佳定义是 Unicode
字符。因此,从 Python 3 的str
对象中获取的元素是Unicode
字符,这相当于从Python 2的unicode
对象中获取的元素,而不是从Python 2的 str
对象中获取的原始字节序列。
Unicode
标准把字符的标识和具体的字节表述进行了如下的明确区分。
- 字符的标识,即码位,是
0~1 114 111
的数字(十进制),在Unicode
标准中以4~6
个十六进制数字表示,而且加前缀“U+”。例如,字母 A 的码位是U+0041
,欧元符号的码位是U+20AC
,高音谱号的码位是U+1D11E
。在Unicode
6.3 中(这是 Python 3.4 使用的标准),约10%的有效码位有对应的字符。 - 字符的具体表述取决于所用的编码。编码是在码位和字节序列之间转换时使用的算法。在
UTF-8
编码中,A(U+0041)的码位编码成单个字节\x41
,而在UTF-16LE
编码中编码成两个字节\x41\x00
。再举个例子,欧元符号(U+20AC
)在 UTF-8 编码中是三个字节——\xe2\x82\xac
,而在UTF-16LE
中编码成两个字节:\xac\x20
。
把码位转换成字节序列的过程是编码;把字节序列转换成码位的过程是解码。
编码和解码:
s = 'café'
print(len(s))
b = s.encode('utf8')
print(b)
b'caf\xc3\xa9'
print(len(b))
b.decode('utf8')
print('café')
如果想帮助自己记住 .decode()
和 .encode()
的区别,可以把字节序列想成晦涩难懂的机器磁芯转储,把 Unicode
字符串想成“人类可读”的文本。那么,把字节序列变成人类可读的文本字符串就是解码,而把字符串变成用于存储或传输的字节序列就是编码。
虽然 Python 3 的str
类型基本相当于 Python 2 的 unicode
类型,只不过是换了个新名称,但是 Python 3 的 bytes
类型却不是把 str
类型换个名称那么简单,而且还有关系紧密的 bytearray
类型。
字节概要
新的二进制序列类型在很多方面与 Python 2 的 str
类型不同。首先要知道,Python 内置了两种基本的二进制序列类型:Python 3 引入的不可变bytes
类型和 Python 2.6 添加的可变 bytearray
类型。(Python 2.6 也引入了 bytes
类型,但那只不过是 str
类型的别名,与 Python 3 的bytes
类型不同。)
bytes
或 bytearray
对象的各个元素是介于 0~255
(含)之间的整数,而不像 Python 2 的 str
对象那样是单个的字符。然而,二进制序列的切片始终是同一类型的二进制序列,包括长度为1的切片。2
包含 5 个字节的 bytes
和 bytearray
对象
cafe = bytes('café', encoding='utf_8')
print(cafe) # b'caf\xc3\xa9'
print(cafe[0]) # 99
print(cafe[:1]) # b'c'
cafe_arr = bytearray(cafe)
print(cafe_arr) # bytearray(b'caf\xc3\xa9')
print(cafe_arr[0]) # 99
print(cafe_arr[-1:]) # bytearray(b'\xa9')
虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有ASCII
文本。因此,各个字节的值可能会使用下列三种不同的方式显示。
- 可打印的
ASCII
范围内的字节(从空格
到~
),使用ASCII
字符本身。 制表符
、换行符
、回车符
和\
对应的字节,使用转义序列\t
、\n
、\r
和\\
。- 其他字节的值,使用十六进制转义序列(例如,
\x00
是空字节)。
因此,在上述示例中,我们看到的是 b'caf\xc3\xa9'
:前 3 个字节b'caf'
在可打印的 ASCII
范围内,后两个字节则不然。
除了格式化方法(format
和 format_map
)和几个处理 Unicode
数据的方法(包括casefold
、isdecimal
、isidentifier
、isnumeric
、isprintable
和 encode
)之外,str
类型的其他方法都支持 bytes
和 bytearray
类型。这意味着,我们可以使用熟悉的字符串方法处理二进制序列,如endswith
、replace
、strip
、translate
、upper
等,只有少数几个其他方法的参数是 bytes
对象,而不是 str
对象。此外,如果正则表达式编译自二进制序列而不是字符串,re
模块中的正则表达式函数也能处理二进制序列。3
a = bytes.fromhex('31 4B CE A9')
print(a) # b'1K\xce\xa9'
构建 bytes
或 bytearray
实例还可以调用各自的构造方法,传入下述参数。
- 一个
str
对象和一个encoding
关键字参数。 - 一个可迭代对象,提供 0~255 之间的数值。
- 一个整数,使用空字节创建对应长度的二进制序列。[Python 3.5 会把这个构造方法标记为“过时的”,Python 3.6 会将其删除。参见“PEP 467—Minor API improvements for binary sequences”。]
- 一个实现了缓冲协议的对象(如
bytes
、bytearray
、memoryview
、array.array
);此时,把源对象中的字节序列复制到新建的二进制序列中。
使用缓冲类对象构建二进制序列是一种低层操作,可能涉及类型转换。
使用数组中的原始数据初始化 bytes
对象
import array
numbers = array.array('h', [-2, -1, 0, 1, 2])
octets = bytes(numbers)
print(octets) # b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'
使用缓冲类对象创建 bytes
或 bytearray
对象时,始终复制源对象中的字节序列。与之相反,memoryview
对象允许在二进制数据结构之间共享内存。如果想从二进制序列中提取结构化信息,struct
模块是重要的工具。下一节会使用这个模块处理 bytes
和 memoryview
对象。
结构体和内存视图
struct
模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的字节序列。struct
模块能处理 bytes
、bytearray
和 memoryview
对象。
memoryview
类不是用于创建或存储字节序列的,而是共享内存,让你访问其他二进制序列、打包的数组和缓冲中的数据切片,而无需复制字节序列,例如 Python Imaging Library(PIL
) 就是这样处理图像的。4
如何使用 memoryview
和 struct
提取一个 GIF
图像的宽度和高度。
import struct
fmt = '<3s3sHH'
with open('filter.gif', 'rb') as fp: # gif为上方的大拇指
img = memoryview(fp.read())
header = img[:10]
print(bytes(header)) # b'GIF89a\x16\x00\x16\x00'
print(struct.unpack(fmt, header)) # (b'GIF', b'89a', 22, 22)
del header
del img
注意,memoryview
对象的切片是一个新 memoryview
对象,而且不会复制字节序列。[ 如果使用 mmap
模块把图像打开为内存映射文件,那么会复制少量字节。本文不会讨论 mmap
,如果你经常读取和修改二进制文件,可以阅读“mmap—Memory-mapped file support”来进一步学习。]
本文不会深入介绍 memoryview
和 struct
模块,如果要处理二进制数据,可以阅读它们的文档:“Built-in Types » Memory Views”和“struct—Interpret bytes as packed binary data”。
基本的编解码器
Python 自带了超过 100 种编解码器(codec
, encoder
/decoder
),用于在文本和字节之间相互转换。每个编解码器都有一个名称,如 'utf_8'
,而且经常有几个别名,如 'utf8'
、'utf-8'
和 'U8'
。这些名称可以传给 open()
、str.encode()
、bytes.decode()
等函数的 encoding
参数。
使用 3 个编解码器编码字符串“El Niño”
,得到的字节序列差异很大:
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'
下图展示了不同编解码器对“A”
和高音谱号
等字符编码后得到的字节序列。注意,后 3 种是可变长度的多字节编码。
12个字符,它们的码位及不同编码的字节表述(十六进制,星号表明该编码不支持表示该字符)
上图星号
表明,某些编码(如 ASCII
和多字节的 GB2312
)不能表示所有 Unicode
字符。然而,UTF
编码的设计目的就是处理每一个Unicode
码位。
了解编解码问题
虽然有个一般性的 UnicodeError
异常,但是报告错误时几乎都会指明具体的异常:UnicodeEncodeError
(把字符串转换成二进制序列时)或 UnicodeDecodeError
(把二进制序列转换成字符串时)。如果源码的编码与预期不符,加载 Python 模块时还可能抛出 SyntaxError
。接下来的几节说明如何处理这些错误。
处理UnicodeEncodeError
多数非 UTF
编解码器只能处理 Unicode
字符的一小部分子集。把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出UnicodeEncodeError
异常,除非把 errors
参数传给编码方法或函数,对错误进行特殊处理。5
编码成字节序列:成功和错误处理
city = 'São Paulo'
print(city.encode('utf_8')) # b'S\xc3\xa3o Paulo'
# 'utf_?'编码能处理任何字符串。
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'
# 'iso8859_1'编码也能处理字符串'São Paulo'。
print(city.encode('cp437'))
# 'cp437' 无法编码 'ã'(带波形符的“a”),抛出异常
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "/.../lib/python3.4/encodings/cp437.py", line 12, in encode
# return codecs.charmap_encode(input,errors,encoding_map)
# UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in
# position 1: character maps to <undefined>
print(city.encode('cp437', errors='ignore')) # b'So Paulo'
# 悄无声息地跳过无法编码的字符
print(city.encode('cp437', errors='replace')) # b'S?o Paulo'
# 把无法编码的字符替换成 '?'
print(city.encode('cp437', errors='xmlcharrefreplace')) # b'São Paulo'
# 把无法编码的字符替换成 XML 实体
处理UnicodeDecodeError
不是每一个字节都包含有效的 ASCII
字符,也不是每一个字符序列都是有效的 UTF-8
或 UTF-16
。因此,把二进制序列转换成文本时,如果假设是这两个编码中的一个,遇到无法转换的字节序列时会抛出UnicodeDecodeError
。
另一方面,很多陈旧的 8 位编码——如 'cp1252'
、'iso8859_1'
和'koi8_r'
——能解码任何字节序列流而不抛出错误,例如随机噪声。因此,如果程序使用错误的 8 位编码,解码过程悄无声息,而得到的是无用输出。6
把字节序列解码成字符串:成功和错误处理:
octets = b'Montr\xe9al'
# 使用latin1编码的“Montréal”;'\xe9' 字节对应“é”。
print(octets.decode('cp1252')) # Montréal
# 可以使用'cp1252'(Windows 1252)解码,因为它是latin1的有效超集。
print(octets.decode('iso8859_7')) # Montrιal
# ISO-8859-7用于编码希腊文,因此无法正确解释'\xe9'字节,而且没有抛出错误。
print(octets.decode('koi8_r')) # MontrИal
# KOI8-R用于编码俄文;这里,'\xe9' 表示西里尔字母“И”。
print(octets.decode('utf_8'))
# 'utf_8' 编解码器检测到octets不是有效的UTF-8字符串,抛出异常
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5:
# invalid continuation byte
print(octets.decode('utf_8', errors='replace')) # Montr�al
# 使用'replace'错误处理方式,\xe9替换成了“�”
使用预期之外的编码加载模块时抛出的SyntaxError
Python 3 默认使用 UTF-8
编码源码,Python 2(从 2.5 开始)则默认使用ASCII
。如果加载的 .py 模块中包含 UTF-8
之外的数据,而且没有声明编码,会得到类似下面的消息:
SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line
1, but no encoding declared; see http://python.org/dev/peps/pep-0263/
for details
GNU/Linux
和OS X
系统大都使用 UTF-8
,因此打开在 Windows 系统中使用cp1252
编码的 .py 文件时可能发生这种情况。注意,这个错误在Windows 版Python中也可能会发生,因为 Python3为所有平台设置的默认编码都是 UTF-8
。7
为了修正这个问题,可以在文件顶部添加一个神奇的 coding
注释:
# ola.py:“你好,世界!”的葡萄牙语版
# coding: cp1252
print('Olá, Mundo!')
Python 3 允许在源码中使用非 ASCII 标识符:
ação = 'PBR' # ação = stock
ε = 10**-6 # ε = epsilon
有些人不喜欢这么做。支持始终使用 ASCII
标识符的人认为,这样便于所有人阅读和编辑代码。这些人没切中要害:源码应该便于目标群体阅读和编辑,而不是“所有人”。如果代码属于跨国公司,或者是开源的,想让来自世界各地的人作贡献,那么标识符应该使用英语,也就是说只能使用 ASCII
字符。
但是,如果你是巴西的一位老师,那么使用葡萄牙语正确拼写变量和函数名更便于学生阅读代码。而且,这些学生在本地化的键盘中不难打出变音符号和重音元音字母。
现在,Python 能解析 Unicode
名称,而且源码的默认编码是 UTF-8
,我觉得没有任何理由使用不带重音符号的葡萄牙语编写标识符。在 Python 2 中确实不能这么做,除非你也想使用 Python 2 运行代码,否则不必如此。如果使用葡萄牙语命名标识符却不带重音符号的话,这样写出的代码对任何人来说都不易阅读。
选择对团队而言易于阅读的人类语言,然后使用正确的字符拼写。
PyCon 2014,“Character Encoding and Unicode in Python”演讲的第 12 张幻灯片。 ↩︎
my_bytes[0]
获取的是一个整数,而my_bytes[:1]
返回的是一个长度为 1 的bytes
对象——这一点应该不会让人意外。s[0] == s[:1]
只对str
这个序列类型成立。不过,str
类型的这个行为十分罕见。对其他各个序列类型来说,s[i]
返回一个元素,而s[i:i+1]
返回一个相同类型的序列,里面是s[i]
元素。 ↩︎Python 3.0~3.4 不能使用
%
运算符处理二进制序列,但是根据“PEP 461—Adding % formatting to bytes and bytearray”,Python 3.5 应该会支持。 ↩︎编解码器的错误处理方式是可扩展的。你可以为
errors
参数注册额外的字符串,方法是把一个名称和一个错误处理函数传给codecs.register_error
函数。参见codecs.register_error函数的文档。 ↩︎乱码字符称为鬼符(gremlin)或 mojibake(文字化け,“变形文本”的日文)。 ↩︎
现在,Python 3 的源码不再限于使用
ASCII
,而是默认使用优秀的UTF-8
编码,因此要修正源码的陈旧编码(如'cp1252'
)问题,最好将其转换成UTF-8
,别去麻烦coding
注释。如果你用的编辑器不支持UTF-8
,那么是时候换一个了。 ↩︎