一、字符问题
“字符串”是个相当简单的概念:一个字符串是一个字符序列。问题出在“字符”的定义上。
在2015年,“字符”的最佳定义是Unicode字符。因此,从Python 3的str对象中获取的元素是Unicode字符,这相当于从Python 2的unicode对象中获取的元素,而不是从Python 2的str对象中获取的原始字节序列。
字符的标识,即码位,在Unicode标准中以4~6个十六进制数字表示;字符的具体表述取决于所用的编码:编码是在码位和字节序列之间转换时使用的算法。在UTF-8编码中,A(U+0041)的码位编码成单个字节\x41,而在UTF-16LE编码中编码成两个字节\x41\x00。把码位转换成字节序列的过程是编码;把字节序列转换成码位的过程是解码。
编码和解码: 把字节序列变成人类可读的文本字符串就是解码,而把字符串变成用于存储或传输的字节序列就是编码。
s = '苹果'
s.encode('utf-8')
Out[57]: b'\xe8\x8b\xb9\xe6\x9e\x9c'
en_s = s.encode('utf-8')
en_s.decode()
Out[59]: '苹果'
二、字节概要
Python内置了两种基本的二进制序列类型:Python 3引入的不可变bytes类型和Python 2.6添加的可变bytearray类型。bytes或bytearray对象的各个元素是介于0~255(含)之间的整数。然而,二进制序列的切片始终是同一类型的二进制序列,包括长度为1的切片。
三、基本的编解码器
Python自带了超过100种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换。每个编解码器都有一个名称,如’utf_8’,而且经常有几个别名,如’utf8’、‘utf-8’和’U8’。这些名称可以传给open( )、str.encode( )、bytes.decode( )等函数的encoding参数。下面介绍了几种编码
1、latin1(即iso8859_1)
一种重要的编码,是其他编码的基础,例如cp1252和Unicode(注意,latin1与cp1252的字节值是一样的,甚至连码位也相同)。
2、cp1252
Microsoft制定的latin1超集,添加了有用的符号,例如弯引号和€(欧元);有些Windows应用把它称为“ANSI”,但它并不是ANSI标准。
3、cp437
IBM PC最初的字符集,包含框图符号。与后来出现的latin1不兼容。
4、gb2312
用于编码简体中文的陈旧标准;这是亚洲语言中使用较广泛的多字节编码之一。
5、utf-8
目前Web中最常见的8位编码;[插图]与ASCII兼容(纯ASCII文本是有效的UTF-8文本)。
6、utf-16
UTF-16的16位编码方案的一种形式;所有UTF-16支持通过转义序列(称为“代理对”,surrogate pair)表示超过U+FFFF的码位。
四、了解编解码问题
虽然有个一般性的UnicodeError异常,但是报告错误时几乎都会指明具体的异常:UnicodeEncodeError(把字符串转换成二进制序列时)或UnicodeDecodeError(把二进制序列转换成字符串时)。如果源码的编码与预期不符,加载Python模块时还可能抛出SyntaxError。出现与Unicode有关的错误时,首先要明确异常的类型。导致编码问题的是UnicodeEncodeError、UnicodeDecodeError,还是如SyntaxError的其他错误?
1、处理UnicodeEncodeError
多数非UTF编解码器只能处理Unicode字符的一小部分子集。把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出UnicodeEncodeError异常,除非把errors参数传给编码方法或函数,对错误进行特殊处理。
city = 'Sã Paulo'
city.encode('utf-8')
Out[63]: b'S\xc3\xa3 Paulo'
city.encode('utf-16')
Out[64]: b'\xff\xfeS\x00\xe3\x00 \x00P\x00a\x00u\x00l\x00o\x00'
city.encode('cp437')
Traceback (most recent call last):
UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined>
# error='ignore'处理方式悄无声息地跳过无法编码的字符;这样做通常很是不妥。
city.encode('cp437', errors='ignore')
Out[66]: b'S Paulo'
#编码时指定error='replace',把无法编码的字符替换成'?';数据损坏了,但是用户知道出了问题。
city.encode('cp437', errors='replace')
Out[67]: b'S? Paulo'
# 'xmlcharrefreplace'把无法编码的字符替换成XML实体。
city.encode('cp437', errors='xmlcharrefreplace')
Out[68]: b'Sã Paulo'
2、 处理UnicodeDecodeError
不是每一个字节都包含有效的ASCII字符,也不是每一个字符序列都是有效的UTF-8或UTF-16。因此,把二进制序列转换成文本时,如果假设是这两个编码中的一个,遇到无法转换的字节序列时会抛出UnicodeDecodeError。
另一方面,很多陈旧的8位编码——如’cp1252’、‘iso8859_1’和’koi8_r’——能解码任何字节序列流而不抛出错误,例如随机噪声。因此,如果程序使用错误的8位编码,解码过程悄无声息,而得到的是无用输出。
octets = b'Montr\xe9al'
octets.decode('cp1252')
Out[70]: 'Montréal'
octets.decode('iso8859_7')
Out[71]: 'Montrιal'
octets.decode('koi8_r')
Out[72]: 'MontrИal'
octets.decode('utf_8')
Traceback (most recent call last):
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
octets.decode('utf_8', errors='replace')
Out[74]: '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(替换字符),表示未知字符。
3、使用预期之外的编码加载模块时抛出的SyntaxError
Python 3默认使用UTF-8编码源码,Python 2(从2.5开始)则默认使用ASCII。如果加载的.py模块中包含UTF-8之外的数据,而且没有声明编码,会出现SyntaxError。
4、如何找出字节序列的编码
一节使用一个推荐的库回答这个问题。4.4.4 如何找出字节序列的编码如何找出字节序列的编码?简单来说,不能。必须有人告诉你。
有些通信协议和文件格式,如HTTP和XML,包含明确指明内容编码的首部。可以肯定的是,某些字节流不是ASCII,因为其中包含大于127的字节值,而且制定UTF-8和UTF-16的方式也限制了可用的字节序列。不过即便如此,我们也不能根据特定的位模式来100%确定二进制文件的编码是ASCII或UTF-8。
统一字符编码侦测包Chardet就是这样工作的,它能识别所支持的30种编码。Chardet是一个Python库,可以在程序中使用,不过它也提供了命令行工具chardetect。
import chardet
import urllib
TestData = urllib.request.urlopen('http://www.baidu.com/').read()
print(chardet.detect(TestData))
{'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}
5、处理文本文件
处理文本的最佳实践是“Unicode三明治”(如图4-2所示)。[插图]意思是,要尽早把输入(例如读取文件时)的字节序列解码成字符串。这种三明治中的“肉片”是程序的业务逻辑,在这里只能处理字符串对象。在其他处理过程中,一定不能编码或解码。对输出来说,则要尽量晚地把字符串编码成字节序列。多数Web框架都是这样做的,使用框架时很少接触字节序列。例如,在Django中,视图应该输出Unicode字符串;Django会负责把响应编码成字节序列,而且默认使用UTF-8编码。
待续。。。