4.1 字符问题
“字符串”是个相当简单的概念:一个字符串是一个字符序列。问题出在“字符”的定义上。
在 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('utf-8')
print(b)
print(len(b))
print(b.decode('utf-8'))

Unicode(又称统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。
汉字“字”对应的数字是23383(十进制),十六进制表示为5B57。在Unicode中,我们有很多方式将数字23383表示成程序中的数据,包括:UTF-8、UTF-16、UTF-32。UTF是“Unicode Transformation Format”的缩写,可以翻译成Unicode字符集转换格式,即怎样将Unicode定义的数字转换成程序数据。
4.2字节概要
新的二进制序列类型在很多方面与 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 的切片
cafe = bytes( 'café' , encoding = 'utf-8')
print(cafe)
print(cafe[0])
print(cafe[:1])
cafe_arr = bytearray(cafe)
print(cafe_arr)
print(cafe_arr[-1 :])

my_bytes[0] 获取的是一个整数,而my_bytes[:1] 返回的是一个长度为1 的 bytes 对象——这一点应该不会让人意外。s[0] == s[:1] 只对 str 这个序列类 型成立。不过,str 类型的这个行为十分罕见。对其他各个序列类型来说,s[i] 返回一个元素,而 s[i:i+1] 返回一个相同类型的序列,里面是 s[i] 元素。
虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有 ASCII 文本。因 此,各个字节的值可能会使用下列三种不同的方式显示。
- 可打印的 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身
- 制表符、换行符、回车符和 \ 对应的字节,使用转义序列 \t、\n、\r 和 \
- 其他字节的值,使用十六进制转义序列(例如,\x00 是空字节)。
构建 bytes 或 bytearray 实例还可以调用各自的构造方法,传入下述参数。
- 一个str对象和一个encoding关键字参数
- 一个可迭代对象
- 一个整数
使用缓冲类对象构建二进制序列是一种低层操作,可能涉及类型转换。
import array
numbers = array.array('h', [-2, -1, 0, 1, 2])
octets = bytes(numbers)
octets

- 结构体和内存视图
struct 模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有 一些函数用于执行反向转换,把元组转换成打包的字节序列。struct 模块能处理 bytes、 bytearray 和 memoryview 对象。
4.3基本的编码器
Python 自带了超过 100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互 转换。每个编解码器都有一个名称,如 'utf_8',而且经常有几个别名,如 'utf8'、'utf8' 和 'U8'。这些名称可以传给 open()、str.encode()、bytes.decode() 等函数的 encoding 参数。示例 4-5 使用 3 个编解码器把相同的文本编码成不同的字节序列。

4.4了解编码问题
虽然有个一般性的UnicodeError 异常,但是报告错误时几乎都会指明具体的异常: UnicodeEncodeError(把字符串转换成二进制序列时)或UnicodeDecodeError(把二进 制序列转换成字符串时)。如果源码的编码与预期不符,加载 Python 模块时还可能抛出 SyntaxError。接下来的几节说明如何处理这些错误。
出现与Unicode 有关的错误时,首先要明确异常的类型。导致编码问题的 是 UnicodeEncodeError、UnicodeDecodeError,还是如 SyntaxError 的其他错 误?解决问题之前必须清楚这一点。
4.4.1 解决UnicodeEncodeError
多数非 UTF 编解码器只能处理 Unicode 字符的一小部分子集。把文本转换成字节序列时, 如果目标编码中没有定义某个字符,那就会抛出 UnicodeEncodeError 异常,除非把 errors 参数传给编码方法或函数,对错误进行特殊处理。
city = 'São Paulo'
print(city.encode('utf-8'))
print(city.encode('utf-16'))
print(city.encode('iso8859_1'))
print(city.encode('cp437'))
print(city.encode('cp437',errors = 'ignore'))
print(city.encode('cp437',errors = 'replace'))
print(city.encode('cp437',errors = 'xmlcharrefreplace'))

city = 'São Paulo'
print(city.encode('utf-8'))
print(city.encode('utf-16'))
print(city.encode('iso8859_1'))
#print(city.encode('cp437'))
print(city.encode('cp437',errors = 'ignore'))
print(city.encode('cp437',errors = 'replace'))
print(city.encode('cp437',errors = 'xmlcharrefreplace'))

4.4.2 处理UnicodeDecodeError
octets = b'Montr\xe9al'
print(octets.decode('cp1252'))
print(octets.decode('iso8859_7'))
print( octets.decode('koi8_r') )
print( octets.decode('utf_8') )
print(octets.decode('utf_8', errors='replace') )

octets = b'Montr\xe9al'
print(octets.decode('cp1252'))
print(octets.decode('iso8859_7'))
print( octets.decode('koi8_r') )
#print( octets.decode('utf_8') )
print(octets.decode('utf_8', errors='replace') )

4.4.3 使用预期之外的编码加载模块时抛出的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;
GNU/Linux 和 OS X 系统大都使用 UTF-8,因此打开在 Windows 系统中使用 cp1252 编码 的 .py 文件时可能发生这种情况。注意,这个错误在 Windows 版 Python 中也可能会发生, 因为 Python 3 为所有平台设置的默认编码都是 UTF-8
4.4.4 如何找出字节序列的编码
如何找出字节序列的编码?简单来说,不能。必须有人告诉你。
有些通信协议和文件格式,如HTTP 和 XML,包含明确指明内容编码的首部。可以肯 定的是,某些字节流不是ASCII,因为其中包含大于127 的字节值,而且制定UTF-8 和 UTF-16 的方式也限制了可用的字节序列。不过即便如此,我们也不能根据特定的位模式 来 100% 确定二进制文件的编码是 ASCII 或 UTF-8。
然而,就像人类语言也有规则和限制一样,只要假定字节流是人类可读的纯文本,就可 能通过试探和分析找出编码。例如,如果 b'\x00' 字节经常出现,那么可能是 16 位或 32 位编码,而不是8 位编码方案,因为纯文本中不能包含空字符;如果字节序列 b'\x20\ x00' 经常出现,那么可能是 UTF-16LE 编码中的空格字符(U+0020),而不是鲜为人知的 U+2000 EN QUAD 字符——谁知道这是什么呢!
统一字符编码侦测包 Chardet(https://pypi.python.org/pypi/chardet)就是这样工作的,它能 识别所支持的 30 种编码。Chardet 是一个 Python 库,可以在程序中使用,不过它也提供了 命令行工具 chardetect。下面是它对本章书稿文件的检测报告:
$ chardetect 04-text-byte.asciidoc 04-text-byte.asciidoc: utf-8 with confidence 0.99
二进制序列编码文本通常不会明确指明自己的编码,但是 UTF 格式可以在文本内容的开头 添加一个字节序标记。
4.5 处理文本文件
处理文本的最佳实践是“Unicode 三明治”(如图 4-2 所示)。4 意思是,要尽早把输入(例 如读取文件时)的字节序列解码成字符串。这种三明治中的“肉片”是程序的业务逻辑, 在这里只能处理字符串对象。在其他处理过程中,一定不能编码或解码。对输出来说,则 要尽量晚地把字符串编码成字节序列。多数 Web 框架都是这样做的,使用框架时很少接触 字节序列。例如,在 Django 中,视图应该输出 Unicode 字符串;Django 会负责把响应编 码成字节序列,而且默认使用 UTF-8 编码。

在 Python 3 中能轻松地采纳 Unicode 三明治的建议,因为内置的 open 函数会在读取文件时 做必要的解码,以文本模式写入文件时还会做必要的编码,所以调用 my_file.read() 方法得到的以及传给 my_file.write(text) 方法的都是字符串对象,可以看出,处理文本文件很简单。但是,如果依赖默认编码,你会遇到麻烦。

4.6 为了正确比较而规范化Unicode字符串
因为Unicode 有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整 体),所以字符串比较起来很复杂
例如,“café”这个词可以使用两种方式构成,分别有 4 个和 5 个码位,但是结果完全一样
s1 = 'café'
s2 = 'cafe\u0301'
print(s1,s2)
print( len(s1), len(s2) )
print( s1 == s2 )

网友评论