引言
这是《流畅的Python第二版》抢先版的读书笔记。Python版本暂时用的是python3.8。为了使开发更简单、快捷,本文使用了JupyterLab。
Python3 明确区分了人类可读的字符串和原始的字节序列。
新内容简介
新增了对emoji表示字符的描述。
字符问题
字符串是个简单的概念:一个字符串是一个字符序列。问题在于字符的定义。
在 2021 年,“字符”的最佳定义是 Unicode 字符。因此,从 Python 3 的str 对象中获取的元素是 Unicode 字符,这相当于从 Python 2 的
unicode 对象中获取的元素,而不是从 Python 2 的 str 对象中获取的原始字节序列。
Unicode 标准把字符的标识和具体的字节表述进行了如下的明确区分。
- 字符的标识,即码位,是
0~1 114 111
的数字(十进制),在Unicode 标准中以 4~6 个十六进制数字表示,而且加前缀“U+”。 - 字符的具体表述取决于所用的编码。
把码位转换为字节序列的过程是编码;从字节序列转换为码位是解码。
s = 'café'
len(s) # 有4个Unicode字符
4
b = s.encode('utf8') # 使用utf-8编码字符串到字节序列
b # 字节序列以字面量b开头
b'caf\xc3\xa9'
len(b) # 字节序列b有5个字节
5
b.decode('utf8') # 把字节序列解码成str对象
'café'
字节概要
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,哪怕长度为1的切片。
b'c'
cafe_arr = bytearray(cafe)
cafe_arr # bytearray 对象没有字面量句法,而是以 bytearray() 和字节序列字面量参数的形式显示。
bytearray(b'caf\xc3\xa9')
cafe_arr[-1:] # bytearray的切片还是bytearray
bytearray(b'\xa9')
虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有ASCII 文本。因此,各个字节的值可能会使用下列三种不同的方式显示。
- 可打印的 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身。
- 制表符、换行符、回车符和
\
对应的字节,使用转义序列\t
、\n
、\r
和\
。 - 如果两个字符串分隔符
'
和"
都出现在字节序列,那么整个序列由’
分隔,内部的’
转义为\’
。 - ( 其他字节的值,使用十六进制转义序列(例如,
\x00
是空字节)。
我们看到的是 b’caf\xc3\xa9’
:前 3 个字节b’caf’
在可打印的 ASCII 范围内,后两个字节使用十六进制转义序列。
二进制序列有一个str
没有的类方法,叫作fromhex
,它的作用是解析十六进制数字对(数字对之间的空格是可选的),构建二进制序列:
bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'
其他构建bytes
或bytearray
实例的方法是通过以下参数调动它们的构造函数:
- 一个
str
和一个encoding
关键字参数 - 一个可迭代对象,提供0~255之间的数值
- 一个实现了缓冲协议的对象(如,
bytes
、bytearray
、memoryview
、array.array
),可从源对象拷贝字节到新建二进制序列
从一个类缓冲(buffer-like)对象构建一个二进制序列是低层操作,可能涉及类型转换,见示例:
import array
numbers = array.array('h', [-2, -1, 0, 1, 2]) # 类型代码 h 创建一个短整型(16位)数组
octets = bytes(numbers) # octets保存组成numbers字节序列的副本
octets # 通过10个字节表示5个短整型
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'
从任何类缓冲源创建一个bytes
或bytearray
始终会复制源中的字节序列。反之,memoryview
对象可以让你在二进制数据结构之间共享内存。
基本编码器/解码器
Python自动超过了100种编解码器(codecs),用于文本和字节之间相互转换。每个codec有一个名称,像utf_8
,通常还有别名,比如utf8
、utf-8
和U8
,你可以在像open()/str.encode()/bytes.decode()
中当成encoding
参数传入。下面的例子展示了一些文本以三种不同的字节序列编码:
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
---------------------------------------------------------------------------
UnicodeEncodeError Traceback (most recent call last)
Cell In [17], line 1
----> 1 city.encode('cp437')
File D:\program_tool\py39\lib\encodings\cp437.py:12, in Codec.encode(self, input, errors)
11 def encode(self,input,errors='strict'):
---> 12 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') # errors=ignore处理器跳过不能编码的字符,这通常会导致数据丢失。
b'So Paulo'
city.encode('cp437', errors='replace') # errors=replace用'?'替换不能处理的字符,也会数据丢失,但是用户知道发生什么
b'S?o Paulo'
city.encode('cp437', errors='xmlcharrefreplace') # xmlcharrefreplace用一个XML实体替换不能编码的字符。
b'São Paulo'
ASCII是我知道的所有编码的常见子集,因此,如果文本完全由 ASCII 字符构成,编码应该始终有效。Python3.7增加了一个新的布尔方法str.isacsii()
来检查你的Unicode文本是不是纯ACSII。如果是,那么你应该能将它编码成字节而不会抛出异常。
处理UnicodeDecodeError
并不是每个字节存储了一个有效的ASCII字符,并不是每个字节序列都是有效的UTF-8或UTF-16;因此,把字节序列转换成文本时,如果假设是这两个编码中的一个,遇到无法转换的字节序列时会抛出 UnicodeDecodeError。
另一方面,许多遗留的8-位编码,像cp1252
,iso8859_1
和koi8_r
能解码任意字节流,包括随机噪音,而不报告错误。如果程序使用错误的8位编码,解码过程悄无声息,而得到的是无用输出。
下面的例子展示了使用错误的编解码器可能出现乱码字符或抛出UnicodeDecodeError
。
octets = b'Montr\xe9al' # 这些字节序列是使用latin1编码的“Montréal”;'\xe9' 字节对应“é”。
octets.decode('cp1252') # 通过cp1252可以解码,因为它是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。
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
Cell In [25], line 1
----> 1 octets.decode('utf_8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte
octets.decode('utf_8', errors='replace') # 使用replace错误处理器来替换未知字符
'Montr�al'
使用预期之外的编码加载模块时抛出的SyntaxError
UTF-8是Python3默认的编码,Python2默认使用ACSII。如果你加载一个.py
模块包含非UTF-8且没有声明编码,你会得到以下信息:
SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line
1, but no encoding declared; see https://python.org/dev/peps/pep-0263/
for details
为了解决该问题,可以在文件头部增加一个魔法 coding
注释:
# coding: cp1252
print('Olá, Mundo!')
如何获取字节序列编码
如何获取字节序列的编码?简单来说,不能。必须被告知。
就像人类语言也有规则和限制一样,只要假定字节流是人类可读的纯文本,就可能通过试探和分析找出编码。例如,如果 b'\x00'
字 节经常出现,那么可能是 16 位或 32 位编码,而不是 8 位编码方案,因为纯文本中不能包含空字符;如果字节序列 b'\x20\x00'
经常出现, 那么可能是 UTF-16LE 编码中的空格字符(U+0020),而不是鲜为人知 的 U+2000 EN QUAD 字符——谁知道这是什么呢!
二进制序列编码文本通常不会明确指明自己的编码,但是 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。
list(u16)
[255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
在大字节序 CPU 中,编码顺序是相反的;‘E’ 编码为 0 和 69。
为了避免混淆,UTF-16 编码在要编码的文本前面加上特殊的不可见字 符 ZERO WIDTH NO-BREAK SPACE(U+FEFF)。在小字节序系统中, 这个字符编码为 b’\xff\xfe’(十进制数 255, 254)。因为按照设计, U+FFFE 字符不存在,在小字节序编码中,字节序列 b’\xff\xfe’ 必 定是 ZERO WIDTH NO-BREAK SPACE,所以编解码器知道该用哪个字节 序。
处理文本文件
处理文本的最佳实践是“Unicode 三明治”(如下图所示)。 意思是, 要尽早把输入(例如读取文件时)的字节序列解码成字符串。这种三明 治中的“肉片”是程序的业务逻辑,在这里只能处理字符串对象。在其他 处理过程中,一定不能编码或解码。对输出来说,则要尽量晚地把字符 串编码成字节序列。多数 Web 框架都是这样做的,使用框架时很少接 触字节序列。例如,在 Django 中,视图应该输出 Unicode 字符串; Django 会负责把响应编码成字节序列,而且默认使用 UTF-8 编码。
在 Python 3 中能轻松地采纳 Unicode 三明治的建议,因为内置的 open
函数会在读取文件时做必要的解码,以文本模式写入文件时还会做必要 的编码,所以调用 my_file.read()
方法得到的以及传给 my_file.write(text)
方法的都是字符串对象。
open('cafe.txt', 'w', encoding='utf8').write('café')
4
open('cafe.txt').read()
'caf茅'
上面的代码展示了一个bug,可能你的机器没有。
在写入文件时指定了UTF-8编码,但是读取文件时没有这么做。因此Python假定使用系统默认的编码,于是文件的最后一个字节解码成了字符’茅’而不是’é’。
所以打开文件时始终应该明确传入encoding=
参数。
fp = open('cafe.txt', 'w', encoding='utf_8')
fp # 默认情况下,open函数采用文本模式,返回一个TextIOWrapper对象
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
fp.write('café') # 在TextIOWrapper对象上调用write方法返回写入的Unicode字符数
4
fp.close()
import os
os.stat('cafe.txt').st_size # os.stat报告文件有5个字节;UTF-8编码的'é'占两个字节
5
fp2 = open('cafe.txt')
fp2 # 打开文本文件时没有显示指定编码,返回一个TextIOWrapper对象,编码是区域设置中的默认值
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'>
fp2.encoding # 通过encoding属性,可以发现编码为cp969
'cp936'
fp2.read() # 在cp939中,竟然成了'茅'
'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') # 'rb' 标志指明在二进制模式中读取文件
fp4 # 返回的是 BufferedReader 对象,而不是 TextIOWrapper 对象。
<_io.BufferedReader name='cafe.txt'>
fp4.read() # 读取返回的字节序列,结果与预期相符
b'caf\xc3\xa9'
当心编码默认值
有些设定可以影响Python中I/O的编码默认值,看下面的代码
import locale
import sys
expressions = """
locale.getpreferredencoding()
type(my_file)
my_file.encoding
sys.stdout.isatty()
sys.stdout.encoding
sys.stdin.isatty()
sys.stdin.encoding
sys.stderr.isatty()
sys.stderr.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()
"""
my_file = open('dummy', 'w')
for expression in expressions.split():
value = eval(expression)
print(f'{expression:>30} -> {value!r}')
locale.getpreferredencoding() -> 'cp936'
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'cp936'
sys.stdout.isatty() -> False
sys.stdout.encoding -> 'UTF-8'
sys.stdin.isatty() -> False
sys.stdin.encoding -> 'gbk'
sys.stderr.isatty() -> False
sys.stderr.encoding -> 'UTF-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'
在Windows上,输出如上。Unicode在Windows本身和Windows的Python中支持变得更好。PEP 529-更改Windows文件系统编码为UTF-8(也在Python 3.6中实现),该文件将文件系统编码(用于表示目录和文件的名称)从Microsoft的专有MBC更改为UTF-8。
import sys
from unicodedata import name
print(sys.version)
print()
print('sys.stdout.isatty():', sys.stdout.isatty())
print('sys.stdout.encoding:', sys.stdout.encoding)
print()
test_chars = [
'\N{HORIZONTAL ELLIPSIS}', # exists in cp1252, not in cp437
'\N{INFINITY}', # exists in cp437, not in cp1252
'\N{CIRCLED NUMBER FORTY TWO}', # not in cp437 or in cp1252
]
for char in test_chars:
print(f'Trying to output {name(char)}:')
print(char)
3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)]
sys.stdout.isatty(): False
sys.stdout.encoding: UTF-8
Trying to output HORIZONTAL ELLIPSIS:
…
Trying to output INFINITY:
∞
Trying to output CIRCLED NUMBER FORTY TWO:
㊷
locale.getpreferredencoding()
返回的编码是最重要的:这是打开文件的默认编码,也是重定向到文件的 sys.stdout/stdin/stderr
的默认编码。
因此,关于编码默认值的最佳建议是:别依赖默认值。
如果遵从 Unicode 三明治的建议,而且始终在程序中显式指定编码,那将避免很多问题。可惜,即使把字节序列正确地转换成字符串, Unicode 仍有不尽如人意的地方。
排序Unicode文本
Python比较任何类型的序列时,会一个一个地比较序列中的每项。对于字符串,这意味着比较码位(code point)。不幸地是,如果使用非ASCII字符会产生不可接受的结果。
考虑对下面这些生长在巴西的水果进行排序:
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted(fruits)
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
排序规则根据locales会产生变化,但在葡萄牙语和很多其他使用拉丁字母的语言中,变音符号(cedillas)很少起到作用。所以’cajá’排序时被当成是’caja’,且会出现在’caju’的前面。排序的水果列表应该是:
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
排序非ASCII文本的标准做法是使用locale.strxfrm
函数,根据locale模块文档,“将字符串转换为可用于区域设置感知比较的字符串”。
为了开启locale.strxfrm
,你首先必须为你的应用设置一个合适的locale,并期望操作系统能支持它:
import locale
my_locale = locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
print(my_locale)
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=locale.strxfrm)
print(sorted_fruits)
pt_BR.UTF-8
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
这样,支持该编码的系统中可以得到正确结果。
所以在使用locale.strxfrm
前调用setlocale(LC_COLLATE, <your_locale>)
是正确排序的关键。不过,有一些注意事项:
- 因为locale(区域)设置是全局的,所以不建议在库中调用
setlocale
。你的应用程序或框架应该在进程启动时设置区域设置,并且不应该在启动后更改它。 - 该语言环境(locale)必须安装在操作系统上,否则
setlocale
会抛出错误:unsupported locale setting exception.
- 你必须知道如何拼写语言环境名称
- 该语言环境必须由OS的制造商正确实现
幸运地是,有一个更简单的解决方法:pyuca
包。
使用Unicode排序算法进行排序
pyuca,一个纯Python实现的Unicode排序算法(Unicode Collation Algorithm):
!pip install pyuca
Collecting pyuca
Downloading pyuca-1.2-py2.py3-none-any.whl (1.5 MB)
---------------------------------------- 1.5/1.5 MB 5.8 MB/s eta 0:00:00
Installing collected packages: pyuca
Successfully installed pyuca-1.2
import pyuca
coll = pyuca.Collator()
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=coll.sort_key)
sorted_fruits
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
这种方法简单,并且在Linux,MacOS和Windows上都能用。
pyuca没有考虑语言环境。如果需要自定义排序,可以向Collator()
构造函数提供自定义排序规则表的路径。
Unicode数据库
Unicode标准提供了一个完整的数据库——以几个结构化的文本文件的形式——不仅包括映射码位指向字符名称的表,还包括有关单个字符及其它们如何关联的元数据。比如,Unicode数据库记录字符是可打印字符、字母、十进制数字还是其他数字符号。这就是str
方法isalpha
、isprintable
、isdecimal
和isnumeric
的实现原理。str.casefold
也使用了Unicode表中的信息。
通过名称找打字符
unicodedata
模块提供了遍历字符元数据的函数,包括unicodedata.name()
,它返回某个字符的官方名称:
from unicodedata import name
name('A')
'LATIN CAPITAL LETTER A'
name('ã')
'LATIN SMALL LETTER A WITH TILDE'
name('♛')
'BLACK CHESS QUEEN'
name('
标签:编码,字节,字符,Python,读书笔记,sys,Unicode,序列
From: https://blog.51cto.com/greyfoss/6944530