字节问题
“字符串”是个相当简单的概念:一个字符串是一个字符序列。问题出 在“字符”的定义上。
1.Unicode 标准把字符的标识和具体的字节表述进行了如下的明确区分 字符的标识,即码位,是 0~1 114 111 的数字(十进制),在Unicode 标准中以 4~6 个十六进制数字表示,而且加前缀“U+”。例 如,字母 A 的码位是 U+0041
**2.字符的具体表述取决于所用的编码。编码是在码位和字节序列之间转换时使用的算法。**在 UTF-8 编码中,A(U+0041)的码位编码成 单个字节 \x41,而在 UTF-16LE 编码中编码成两个字节\x41\x00。再举个例子,欧元符号(U+20AC)在 UTF-8 编码中是三个字节——\xe2\x82\xac, 而在 UTF-16LE 中编码成两个字 节:\xac\x20。
把码位转换成字节序列的过程是编码;把字节序列转换成码位的过程是解码。
#编码和解码
>>> s = 'café'
>>> len(s) # ➊
4
>>> b = s.encode('utf8') # ➋
>>> b
b'caf\xc3\xa9' # ➌
>>> len(b) # ➍
5
>>> b.decode('utf8') # ➎
'café'
➊ 'café’字符串有4个Unicode字符。
➋ 使用UTF-8把str对象编码成bytes对象。
➌ bytes字面量以b开头。
➍ 字节序列b有5个字节(在UTF-8中,“é”的码位编码成两个字节)。
➎ 使用UTF-8把bytes对象解码成str对象。
总结:.decode() 和 .encode() 的区别,可以把字节序列想成晦涩难懂的机器磁芯转储,把 Unicode 字符串想 成“人类可读”的文本。那么,把字节序列变成人类可读的文本字符串就是解码,而把字符串变成用于存储或传输的字节序列就是编码。
字节概要
#包含5个字节的bytes和bytearray对象
>>> cafe = bytes('café', encoding='utf_8') #➊
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0] #➋
99
>>> cafe[:1] #➌
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr #➍
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:] #➎
bytearray(b'\xa9')
➊bytes对象可以从str对象使用给定的编码创建。
➋ 各个元素是range(256)内的整数。
➌ bytes对象的切片还是bytes对象,即使是只有一个字节的切片。
➍ bytearray对象没有字面量句法,而是以bytearray()和字节序列字面量参数的形式显示。
➎ bytearray对象的切片还是bytearray对象。
基本的编解码器
#使用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”和高音谱号等字符编码后得到的字节序列
latin1(即iso8859_1)
一种重要的编码,是其他编码的基础,例如cp1252和Unicode(注意,latin1与cp1252的字节值是一样的,甚至连码位也相同)。
cp1252
Microsoft制定的latin1超集,添加了有用的符号,例如弯引号和€(欧元);有些Windows应用把它称为“ANSI”,但它并不是ANSI标准。
cp437
IBM PC最初的字符集,包含框图符号。与后来出现的latin1不兼容。
gb2312
用于编码简体中文的陈旧标准;这是亚洲语言中使用较广泛的多字节编码之一。
utf-8
目前Web中最常见的8位编码;与ASCII兼容(纯ASCII文本是有效的UTF-8文本)。
utf-16le
UTF-16的16位编码方案的一种形式;所有UTF-16支持通过转义序列(称为“代理对”,surrogate pair)表示超过U+FFFF的码位。
编码问题
虽然有个一般性的UnicodeError异常,但是报告错误时几乎都会指明具体的异常:
UnicodeEncodeError(把字符串转换成二进制序列时)或UnicodeDecodeError(把二进制序列转换成字符串时)。
处理UnicodeEncodeError
把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出UnicodeEncodeError异常。
#编码成字节序列:成功和错误处理
>>> city = 'São Paulo'
>>> city.encode('utf_8') #➊
b'S\xc3\xa3o Paulo'
>>> city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
>>> city.encode('iso8859_1') #➋
b'S\xe3o Paulo'
>>> city.encode('cp437') #➌
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>
>>> city.encode('cp437', errors='ignore') #➍
b'So Paulo'
>>> city.encode('cp437', errors='replace') #➎
b'S?o Paulo'
>>> city.encode('cp437', errors='xmlcharrefreplace') #➏
b'São Paulo'
➊ 'utf_?‘编码能处理任何字符串。
➋ ‘iso8859_1’编码也能处理字符串’São Paulo’。
➌ ‘cp437’无法编码’ã’(带波形符的“a”)。默认的错误处理方式’strict’抛出Unicode-
EncodeError。
➍ error=‘ignore’处理方式悄无声息地跳过无法编码的字符;这样做通常很是不妥。
➎ 编码时指定error=‘replace’,把无法编码的字符替换成’?’;数据损坏了,但是用户知
道出了问题。
➏ 'xmlcharrefreplace’把无法编码的字符替换成XML实体。
处理UnicodeDecodeError
把二进制序列转换成文本时,如果假设是这两个编码中的一个,遇到无法转换的字节序列时会抛出UnicodeDecodeError。
#把字节序列解码成字符串:成功和错误处理
>>> octets = b'Montr\xe9al' #➊
>>> octets.decode('cp1252') #➋
'Montréal'
>>> octets.decode('iso8859_7') #➌
'Montrιal'
>>> octets.decode('koi8_r') #➍
'MontrИal'
>>> octets.decode('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
>>> octets.decode('utf_8', errors='replace') #➏
'Montr�al'
➊ 这些字节序列是使用latin1编码的“Montréal”;’\xe9’字节对应“é”。
➋ 可以使用’cp1252’(Windows 1252)解码,因为它是latin1的有效超集。
➌ ISO-8859-7用于编码希腊文,因此无法正确解释’\xe9’字节,而且没有抛出错误。
➍ KOI8-R用于编码俄文;这里,’\xe9’表示西里尔字母“И”。
➎ 'utf_8’编解码器检测到octets不是有效的UTF-8字符串,抛出UnicodeDecodeError。
➏ 使用’replace’错误处理方式,\xe9替换成了“�”(码位是U+FFFD),这是官方指定
的REPLACEMENT CHARACTER(替换字符),表示未知字符。
处理文本文件
处理文本的最佳实践是“Unicode三明治”
>>> fp = open('cafe.txt', 'w', encoding='utf_8')
>>> fp #➊
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>> fp.write('café')
4 #➋
>>> fp.close()
>>> import os
>>> os.stat('cafe.txt').st_size
5 #➌
>>> fp2 = open('cafe.txt')
>>> fp2 #➍
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>> fp2.encoding #➎
'cp1252'
>>> fp2.read()
>'café' #➏
>>> fp3 = open('cafe.txt', encoding='utf_8') #➐
>>> fp3
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'>
>>> fp3.read()
'café' #➑
>>> fp4 = open('cafe.txt', 'rb') #➒
>>> fp4
<_io.BufferedReader name='cafe.txt'> #➓
>>> fp4.read() #⑪
b'caf\xc3\xa9'
➊ 默认情况下,open函数采用文本模式,返回一个TextIOWrapper对象。
➋ 在TextIOWrapper对象上调用write方法返回写入的Unicode字符数。
➌ os.stat报告文件中有5个字节;UTF-8编码的’é’占两个字节,0xc3和0xa9。
➍ 打开文本文件时没有显式指定编码,返回一个TextIOWrapper对象,编码是区域设置中
的默认值。
➎ TextIOWrapper对象有个encoding属性;查看它,发现这里的编码是cp1252。
➏ 在Windows cp1252编码中,0xc3字节是“Ô(带波形符的A),0xa9字节是版权符号。
➐ 使用正确的编码打开那个文件。
➑ 结果符合预期:得到的是四个Unicode字符’café’。
➒ 'rb’标志指明在二进制模式中读取文件。
➓ 返回的是BufferedReader对象,而不是TextIOWrapper对象。
⑪读取返回的字节序列,结果与预期相符。