java中的接口是类吗
277
2022-08-25
《流畅的Python》读书笔记——Python文本和字节序列
字符问题
从Python3的str对象获取的元素时Unicode字符,相当于从Python2的unicode对象中获取的元素,而不是从Python2的str对象中获取的原始字节序列。
把码位(字符的标识)转换成字节序列的过程是编码;把字节序列转换成码位的过程是解码:
>>> s = 'café' >>> len(s)#4个Unicode字符4>>> b = s.encode('utf8')#使用UTF-8把str对象编码成bytes对象>>> bb'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的方式>>> cafeb'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
基本的编码解码器
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'无法编码'ã',默认的错误处理方式抛出UnicodeEncodeErrorTraceback (most recent call last): File "
处理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 "
使用预期之外的编码加载模块时抛出的SyntaxError
SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line1, but no encoding declared; see details
为了修正这个问题,可以在文件顶部添加一个神奇的 coding 注释: (也可以通过utf8编码解码python2中的中文注释问题)
# coding: cp1252print('Olá, Mundo!')
Python 3 允许在源码中使用非 ASCII 标识符。
如何找出字节序列的编码
如何找出字节序列的编码?简单来说,不能。必须有人告诉你。
然而,就像人类语言也有规则和限制一样,只要假定字节流是人类可读 的纯文本,就可能通过试探和分析找出编码。例如,如果 b’\x00’ 字 节经常出现,那么可能是 16 位或 32 位编码,而不是 8 位编码方案,因 为纯文本中不能包含空字符;
二进制序列编码文本通常不会明确指明自己的编码,但是 UTF 格式可 以在文本内容的开头添加一个字节序标记
BOM:有用的鬼符
你可能注意到了,UTF-16 编码的序列开头有几个额外的字节,如下所示:
>>> u16 = 'El Niño'.encode('utf_16')>>> u16b'\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 和 0xa95>>> 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 == s2False
这个问题的解决方案是使用 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 异常。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~