您的位置:首页 > 编程语言 > Python开发

《流畅的Python》读书笔记——Python文本和字节序列

2020-04-01 18:29 1011 查看

字符问题

从Python3的str对象获取的元素时Unicode字符,相当于从Python2的unicode对象中获取的元素,而不是从Python2的str对象中获取的原始字节序列。

把码位(字符的标识)转换成字节序列的过程是编码;把字节序列转换成码位的过程是解码:

>>> s = 'café'
>>> len(s)#4个Unicode字符
4
>>> b = s.encode('utf8')#使用UTF-8把str对象编码成bytes对象
>>> b
b'caf\xc3\xa9' #bytes对象以b开头
>>> len(b)# 字节序列b有5个字节(UTF-8中,"é"的码位编码成两个字节)
5
>>> b.decode('utf8')#解码成str对象
'café'

虽然 Python 3 的 str 类型基本相当于 Python 2 的 unicode 类型,只不
过是换了个新名称,但是 Python 3 的 bytes 类型却不是把 str 类型换
个名称那么简单,而且还有关系紧密的 bytearray 类型。

字节概要

Python 内置了两种基本的二进制序列类型:Python 3 引入的不可变
bytes 类型和 Python 2.6 添加的可变 bytearray 类型。

bytes 或 bytearray 对象的各个元素是介于 0~255(含)之间的整
数,而不像 Python 2 的 str 对象那样是单个的字符。然而,二进制序列
的切片始终是同一类型的二进制序列,包括长度为 1 的切片

>>> cafe = bytes('café',encoding='utf_8')#另一种构造bytes的方式
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0]#每个元素时range(256)内的整数
99
>>> cafe[:1]#bytes对象的切片还是bytes对象,注意和上面通过索引访问的区别
b'c'
>>> cafe_arr = bytearray(cafe)#构造bytearray
>>> cafe_arr[-1:]#bytearray对象的切片还是bytearray对象
bytearray(b'\xa9')

虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有ASCII 文本。因此,各个字节的值可能会使用下列三种不同的方式显示。

  • 可打印的 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身。
  • 制表符、换行符、回车符和 \ 对应的字节,使用转义序列\t、\n、\r 和 \。
  • 其他字节的值,使用十六进制转义序列(例如,\x00 是空字节)。

我们看到的是 b’caf\xc3\xa9’:前 3 个字节b’caf’ 在可打印的 ASCII 范围内,后两个字节使用十六进制转义序列。

使用数组中的原始数据初始化 bytes 对象

>>> import array
>>> numbers = array.array('h',[-2,-1,0,1,2])#指定类型代码h,创建一个短整数(16位)数组
>>> octets = bytes(numbers)
>>> octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'#表示那5个短整数的10个字节

结构体和内存视图

struct 模块提供了一些函数,把打包的字节序列转换成不同类型字段
组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的字
节序列。struct 模块能处理 bytes、bytearray 和 memoryview对象。

memoryview 类不是用于创建或存储字节序列的,而是共享内存,让你访问其他二进制序列、打包的数组和缓冲中的数据切片,而无需复制字节序列。

使用 memoryview 和 struct 查看一个 GIF 图像的首部:

>>> import struct
>>> fmt = '<3s3sHH' #结构体的格式:<是小字节序,3s3s是两个3字节序列,HH是两个16位二进制整数 其实就是gif图形首部的格式
>>> with open('test.gif','rb') as fp:
...      img = memoryview(fp.read())# 使用内存中的文件内容创建一个memoryview对象
...
>>> header = img[:10] #memoryview 对象的切片是一个新 memoryview 对象,而且不会复制字节序列
>>> bytes(header)#转换成字节序列
b'GIF89aJ\x01\x87\x00'
>>> struct.unpack(fmt,header)#拆包memoryview对象,得到一个元组,包含类型、版本、宽度和 高度
(b'GIF', b'89a', 330, 135)
>>> del header# 释放资源
>>> del img

基本的编码解码器

Python 自带了超过 100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换。每个编解码器都有一个名称,如’utf_8’,而且经常有几个别名,如 ‘utf8’、‘utf-8’ 和 ‘U8’。这
些名称可以传给 open()、str.encode()、bytes.decode() 等函数的 encoding 参数。

使用 3 个编解码器把相同的文本编码成不同的字节序列:

>>> 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'

编码解码问题

虽然有个一般性的

UnicodeError
异常,但是报告错误时几乎都会指
明具体的异常:
UnicodeEncodeError
(把字符串转换成二进制序列
时)或
UnicodeDecodeError
(把二进制序列转换成字符串时)。如
果源码的编码与预期不符,加载 Python 模块时还可能抛出
SyntaxError

处理
UnicodeEncodeError

多数非 UTF 编解码器只能处理 Unicode 字符的一小部分子集。把文本转
换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出
UnicodeEncodeError 异常,除非把 errors 参数传给编码方法或函
数,对错误进行特殊处理:

>>> city = 'São Paulo'
>>> city.encode('utf_8') #'utf_?'编码能处理任何字符串
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') #'iso8859_1'也能处理该字符串
b'S\xe3o Paulo'
>>> city.encode('cp437')#'cp437'无法编码'ã',默认的错误处理方式抛出UnicodeEncodeError
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.6/lib/python3.6/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') #把无法编码的字符替换成 XML 实体。
b'S&#227;o Paulo'

处理UnicodeDecodeError

不是每一个字节都包含有效的 ASCII 字符,也不是每一个字符序列都是
有效的 UTF-8 或 UTF-16。因此,把二进制序列转换成文本时,如果假
设是这两个编码中的一个,遇到无法转换的字节序列时会抛出

UnicodeDecodeError

乱码字符称为鬼符(gremlin)或 mojibake(文字化け,“变形文本”的日文)

下面演示了使用错误的编解码器可能出现鬼符或抛出

UnicodeDecodeError
:

>>> octets = b'Montr\xe9al'  #使用 latin1 编码的“Montréal”;'\xe9' 字节对应“é”。
>>> octets.decode('cp1252') #可以使用 'cp1252'(Windows 1252)解码,因为它是 latin1 的有效超集
'Montréal'
>>> octets.decode('iso8859_7')#ISO-8859-7 用于编码希腊文,因此无法正确解释 '\xe9' 字节,而且没有抛出错误
'Montrιal'
>>> octets.decode('koi8_r')#KOI8-R 用于编码俄文;这里,'\xe9' 表示西里尔字母“И”
'MontrИal'
>>> octets.decode('utf_8') #'utf_8' 编解码器检测到 octets 不是有效的 UTF-8 字符串,抛出UnicodeDecodeError。
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') #使用 'replace' 错误处理方式,\xe9 替换成了�,表示未知字符
'Montr�al'

使用预期之外的编码加载模块时抛出的
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

为了修正这个问题,可以在文件顶部添加一个神奇的 coding 注释:
(也可以通过utf8编码解码python2中的中文注释问题)

# coding: cp1252
print('Olá, Mundo!')

Python 3 允许在源码中使用非 ASCII 标识符。

如何找出字节序列的编码

如何找出字节序列的编码?简单来说,不能。必须有人告诉你。

然而,就像人类语言也有规则和限制一样,只要假定字节流是人类可读
的纯文本,就可能通过试探和分析找出编码。例如,如果 b’\x00’ 字
节经常出现,那么可能是 16 位或 32 位编码,而不是 8 位编码方案,因
为纯文本中不能包含空字符;

二进制序列编码文本通常不会明确指明自己的编码,但是 UTF 格式可
以在文本内容的开头添加一个字节序标记

BOM:有用的鬼符

你可能注意到了,UTF-16 编码的序列开头有几个额外的字节,如下所示:

>>> u16 = 'El Niño'.encode('utf_16')
>>> u16
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

指的是

b'\xff\xfe'
。这是 BOM,即字节序标记(byte-order mark),指明编码时使用 Intel CPU 的小字节序。

在小字节序设备中,各个码位的最低有效字节在前面:字母 ‘E’ 的码
位是 U+0045(十进制数 69),在字节偏移的第 2 位和第 3 位编码为 69和 0。

在大字节序 CPU 中,编码顺序是相反的;‘E’ 编码为 0 和 69。

UTF-8 的一大优势是,不管设备使用哪种字节序,生成的字节序列始终一致,因此不需要 BOM。
尽管如此,某些Windows 应用(尤其是 Notepad)依然会在 UTF-8 编码的文件中添加
BOM;而且,Excel 会根据有没有 BOM 确定文件是不是 UTF-8 编码,
否则,它假设内容使用 Windows 代码页(codepage)编码。


难怪NodePad++会有无BOM格式编码这个选项。

处理文本文件

处理文本的最佳实践是“Unicode 三明治”。 意思是,
要尽早把输入(例如读取文件时)的字节序列解码成字符串。这种三明
治中的“肉片”是程序的业务逻辑,在这里只能处理字符串对象。在其他
处理过程中,一定不能编码或解码。对输出来说,则要尽量晚地把字符
串编码成字节序列。

在 Python 3 中能轻松地采纳 Unicode 三明治的建议,因为内置的 open
函数会在读取文件时做必要的解码,以文本模式写入文件时还会做必要
的编码,所以调用 my_file.read() 方法得到的以及传给
my_file.write(text) 方法的都是字符串对象。

可以看出,处理文本文件很简单。但是,如果依赖默认编码,你会遇到麻烦。

>>> open('cafe.txt', 'w', encoding='utf_8').write('café')
4
>>> open('cafe.txt').read()
'café'

一个平台上的编码问题(如果在你的机器上运行,它可能会发生,也可能不会)

写入文件时指定了 UTF-8 编码,但是读取文件时没有这么做,
因此 Python 假定要使用系统默认的编码(Windows 1252),于是文件的
最后一个字节解码成了字符 ‘é’,而不是 ‘é’。

需要在多台设备中或多种场合下运行的代码,一定不能依赖
默认编码
。打开文件时始终应该明确传入

encoding=
参数,因为
不同的设备使用的默认编码可能不同,有时隔一天也会发生变化。

下面展示一个奇怪的细节:第一个语句中的 write 函数报告写入了 4
个字符,但是下一行读取时却得到了 5 个字符。

>>> fp = open('cafe.txt','w',encoding='utf_8')
>>> fp
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>> fp.write('café') #返回写入的Unicode字符数
4
>>> fp.close()
>>> import os
>>> os.stat('cafe.txt').st_size # os.stat 报告文件中有 5 个字节;UTF-8 编码的 'é' 占两个字节,0xc3 和 0xa9
5
>>> fp2 = open('cafe.txt') #打开文本文件时没有显式指定编码
>>> fp2
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='UTF-8'>
>>> fp2.encoding
'UTF-8'
>>> fp2.read()
'café'

为了正确比较而规范化Unicode字符串

因为 Unicode 有组合字符,所以字符串比较起来很复杂。

例如,“café”这个词可以使用两种方式构成,分别有 4 个和 5 个码位,
但是结果完全一样:

>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> s1,s2
('café', 'café')
>>> len(s1),len(s2)
(4, 5)
>>> s1 == s2
False

这个问题的解决方案是使用 unicodedata.normalize 函数提供的
Unicode 规范化。这个函数的第一个参数是这 4 个字符串中的一
个:‘NFC’、‘NFD’、‘NFKC’ 和 ‘NFKD’。

NFC(Normalization Form C)使用最少的码位构成等价的字符串,而
NFD 把组合字符分解成基字符和单独的组合字符。这两种规范化方式都
能让比较行为符合预期:

>>> from unicodedata import normalize
>>> s1 = 'café'
>>> s2 = 'cafe\u0301'
>>> len(s1),len(s2)
(4, 5)
>>> len(normalize('NFC',s1)),len(normalize('NFC',s2))
(4, 4)
>>> len(normalize('NFD',s1)),len(normalize('NFD',s2))
(5, 5)
>>> normalize('NFC',s1) == normalize('NFC',s2)
True
>>> normalize('NFD',s1) == normalize('NFD',s2)
True

在另外两个规范化形式(NFKC 和 NFKD)的首字母缩略词中,字母 K
表示“compatibility”(兼容性)。

下面是 NFKC 的具体应用:

>>> from unicodedata import normalize, name
>>> half = '½'
>>> normalize('NFKC',half)
'1⁄2'
>>> four_squared = '4²'
>>> normalize('NFKC',four_squared)
'42'
>>> micro = 'μ'
>>> micro_kc = normalize('NFKC',micro)
>>> micro,micro_kc
('μ', 'μ')
>>> ord(micro), ord(micro_kc)
(956, 956)
>>> name(micro), name(micro_kc)
('GREEK SMALL LETTER MU', 'GREEK SMALL LETTER MU')

使用 ‘1/2’ 替代 ‘½’ 可以接受,微符号也确实是小写的希腊字母
‘μ’,但是把 ‘4²’ 转换成 ‘42’ 就改变原意了。

Unicode文本排序

Python 比较任何类型的序列时,会一一比较序列里的各个元素。对字符
串来说,比较的是码位。可是在比较非 ASCII 字符时,得到的结果不尽
如人意。

下面对一个生长在巴西(作者是巴西人)的水果的列表进行排序:

>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted(fruits)
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']

排序时“cajá”视作“caja”,必定排在“caju”前面。

排序后的 fruits 列表应该是:

['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

非 ASCII 文本的标准排序方式是使用 locale.strxfrm函数。
使用 locale.strxfrm 函数之前,必须先为应用设定合适的区域设
置,还要祈祷操作系统支持这项设置。

>>> import locale
>>> locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
'pt_BR.UTF-8'
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
>>> sorted_fruits = sorted(fruits, key=locale.strxfrm)
>>> sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

不过,有几点要注意。

  • 区域设置是全局的,因此不推荐在库中调用 setlocale 函数。应用或框架应该在进程启动时设定区域设置,而且此后不要再修改。
  • 操作系统必须支持区域设置,否则 setlocale 函数会抛出locale.Error: unsupported locale setting 异常。
  • 点赞
  • 收藏
  • 分享
  • 文章举报
愤怒的可乐 博客专家 发布了151 篇原创文章 · 获赞 186 · 访问量 14万+ 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: