我是靠谱客的博主 雪白康乃馨,最近开发中收集的这篇文章主要介绍第四章 文本和字节序列,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

Humans use text. Computers speak bytes.

                ----Esther Nam and Travis Fischer, Character Encoding and Unicode in Python

Python3明确区分了人类可读的文本字符串和原始序列字符串。隐式地把字节序列转换成Unicode文本已成过去。本章将讨论Unicode字符串、二进制序列以及二者之间转换时使用的编码。

根据您的 Python 编程场景,深入理解Unicode对您来说可能重要也可能不重要。最后,本章涉及的大多数问题对只处理 ASCII 文本的程序员没有影响。但是即便如此,也不能避而不谈字符串和字节序列的区别。此外,你会发现专门的二进制序列类型所提供的功能,是Python2中全功能的str类型所不具有的。

本章将讨论以下话题:

  • 字符、码位和字节表述
  • 二进制序列的独特功能:bytesbytearray,和 memoryview
  • 完整 Unicode 和旧字符集的编解码器
  • 避免和处理编码错误
  • 处理文本文件的最佳实践
  • 默认编码的陷阱和标准I/O问题
  • 标准化的进行安全的 Unicode 文本比较
  • 用于标准化、大小写折叠和强力移除音调符号的实用函数
  • 使用locale模块和PyUCA库正确的排序Uncode文本
  • Unicode 数据库中的字符元数据
  • 能够处理str和bytes的双模式API
  • 从字符组合构建表情符号

本章新增的内容

Python 3 中对 Unicode 的支持已经全面稳定了一段时间,所以本章最大的变化是新的表情符号部分——不是因为 Python 的变化,而是因为表情符号和表情符号组合的日益流行。2020 年发布的 Unicode 13 支持 3000 多个表情符号,其中许多是通过组合 Unicode 字符构建的,详见“Multi-character emojis”

第二版中的另一个新内容是“按名称查找字符”,包括用于搜索 Unicode 数据库的源代码,这是从命令行查找带圆圈数字和微笑的猫的好方法。

值得一提的一个小变化是 Windows 上的 Unicode 支持,它比 Python 3.6 更好、更简单,我们将在“注意编码默认值”中看到。

字符问题

“字符串”的概念很简单:一个字符串就是一个字符序列。问题在于”字符“的定义。

2021 年,我们对“字符”的最佳定义是 Unicode 字符。因此,您从 Python 3 str 中获得的元素是 Unicode 字符,就像 Python 2 中的 unicode 对象的元素一样——而不是您从 Python 2 str 中获得的原始字节。

Unicode 标准明确地将字符的标识与特定的字节表述进行区分:


  • 字符的标识——即code point(码位)——是一个从 0 到 1,114,111(十进制)的数字,在 Unicode 标准中显示为 4 到 6 个带有“U+”前缀的十六进制数字,从 U+0000 到 U+10FFFF。例如,字母 A 的码位是 U+0041,欧元符号是 U+20AC,音乐符号 G 谱号码位为 U+1D11E。大约 13% 的有效码位在 Unicode 13.0.0(Python 3.9 中使用的标准)中分配了字符。
  • 代表字符的实际字节取决于所用的编码。编码是一种将码位转换为字节序列的算法,解码是将字节序列转换为码位的算法。字母 A (U+0041) 的码位在 UTF-8 编码中编码为单个字节 x41,而在 UTF-16LE 编码中编码为两个字节 x41x00。再举一个例子,UTF-8 需要三个字节——xe2x82xac——来编码欧元符号 (U+20AC),但在 UTF-16LE 中,相同的码位被编码为两个字节:xacx20。

从码位转换为字节是编码;从字节到码位的转换就是解码。请参见下面示例。

>>> s = 'café'
>>> len(s)  
4
>>> b = s.encode('utf8')  
>>> b
b'cafxc3xa9'  
>>> len(b)  
5
>>> b.decode('utf8')  
'café'

字节概要

新的二进制序列类型在很多方面和Python2的str类型不同。首先要知道的是,二进制序列有两种基本的内置类型:Python 3 中引入的不可变的bytes类型和 Python 2.6 中添加的可变的bytearray类型。

bytes 或 bytearray对象中的每一个元素都是 0 到 255 之间的整数,而不是像 Python 2 str对象那样是单个的字符。然而,二进制序列的切片总是产生相同类型的二进制序列——包括长度为 1 的切片,如下例所示:

>>> cafe = bytes('café', encoding='utf_8')  
>>> cafe
b'cafxc3xa9'
>>> cafe[0]  
99
>>> cafe[:1]  
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr  
bytearray(b'cafxc3xa9')
>>> cafe_arr[-1:]  
bytearray(b'xa9')

注意:my_bytes[0] 获取了一个 int 但 my_bytes[:1] 返回一个长度为 1 的 bytes 对象的事实可能令人惊讶,并使得处理二进制数据的程序难以同时支持 Python 2.7 和 Python 3。但它与许多其他语言以及其他 Python 序列类型一致——除了 str这个序列类型,它是唯一一个 s[0] == s[:1] 的序列类型。

虽然二进制序列实际上是整数序列,但它们的字面量表示表明其中有ASCII 文本。因此,每个字节的值有以下四种方式进行展示:

  • 对于可打印 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身。
  • 对于与制表符、换行符、回车符和 对应的字节,使用转义序列 t、n、r 和 \。
  • 如果字符串分隔符 ' 和 " 都出现在字节序列中,则整个序列由 ' 分隔,其中的任何 ' 都转义为 '。
  • 对于其他字节值,使用十六进制转义序列(例如,x00 是空字节)。

这就是为什么我们在上例看到的是b'cafxc3xa9',前三个字节b‘caf’在可打印的ASCII范围内,而后两个字节则不然。

除了格式化(format、format_map)和其他一些依赖 Unicode 数据之外的方法,包括 casefold、isdecimal、isidentifier、isnumeric、isprintable 和 encode,bytes和bytearray类型支持str类型的其他所有方法。这意味着您可以使用熟悉的字符串方法,如 endwith、replace、strip、translate、upper 和许多其他二进制序列——只使用bytes而不是 str 参数。此外,如果正则表达式是从二进制序列而不是 str 编译的,re 模块中的正则表达式函数也能处理于二进制序列。从 Python 3.5 开始,% 运算符可以再次处理二进制序列。

二进制序列有一个 str 没有的类方法,名为 fromhex,它通过解析可选用空格分隔的十六进制数字对来构建二进制序列: 

>>> bytes.fromhex('31 4B CE A9')
b'1Kxcexa9'

构建bytes或bytearray实例的其他方法是调用它们的构造函数:

  • 一个 str对象 和encoding关键字参数
  • 一个可迭代对象,对象中的每个元素为0-255之间的数值
  • 一个实现缓存协议的对象(例如., bytesbytearraymemoryviewarray.array),此时会将源对象的字节序列复制到新创建的二进制序列中

注意:在 Python 3.5 之前,还可以使用单个整数调用 bytes 或 bytearray 来创建一个用空字节初始化的长度为该整数的二进制序列。此签名在 Python 3.5 中已弃用,并在 Python 3.6 中删除。

从类似缓冲对象构建二进制序列是一种可能涉及类型转换的低级操作。请参阅下面示例的演示。

>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])  
>>> octets = bytes(numbers)  
>>> octets
b'xfexffxffxffx00x00x01x00x02x00' 

从类似缓冲区对象的源创建bytes或bytearray对象将始终复制字节。与之相反,memoryview 对象允许您在二进制数据结构之间共享内存。要读取二进制序列中的结构化信息,struct 模块非常有用。我们将在“Structs and Memory Views”中看到它与字节和内存视图一起工作。

基本的编码器/解码器

Python 发行版捆绑了 100 多个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 Nixf1o'
utf_8   b'El Nixc3xb1o'
utf_16  b'xffxfeEx00lx00 x00Nx00ix00xf1x00ox00'

下图演示了从字母“A”到 G 谱号音乐符号等字符生成字节的各种编解码器。请注意,最后三种编码是可变长度的多字节编码。

图 4-1 中的所有星号都清楚地表明,某些编码(例如 ASCII 甚至多字节 GB2312)无法表示每个 Unicode 字符。然而,UTF 编码旨在处理每个 Unicode 码位。

选择上图中所示的编码作为代表性示例:

latin1 又名 iso8859_1

由于它是其他编码的基础,所以非常重要。例如 cp1252 和 Unicode 本身(注意 latin1 字节值如何出现在 cp1252 字节甚至码位中)。

cp1252

微软的 latin1 超集,添加了有用的符号,如卷曲引号和 €(欧元);一些 Windows 应用程序称其为“ANSI”,但它从来都不是真正的 ANSI 标准

cp437

IBM PC 的原始字符集,带有框绘图字符。与latin1 不兼容,后者出现在后面。

gb2312

对中国大陆使用的简体中文表意文字进行编码的传统标准;亚洲语言的几种广泛部署的多字节编码之一。

utf-8

迄今为止,Web 上最常见的 8 位编码,截至 2021 年 4 月,[W3Techs: Usage of Character Encodings for Websites] 声称 96.7% 的网站使用 UTF-8,高于我在 2014 年 9 月在 Fluent Python 第一版中写这一段时的 81.4%。

utf-16le

UTF 16 位编码方案的一种形式;所有 UTF-16 编码都通过称为“代理对”的转义序列支持 U+FFFF 之外的码位。

注意:UTF-16 早在 1996 年就取代了最初的 16 位 Unicode 1.0 编码——UCS-2。尽管自上个世纪以来已被弃用,但 UCS-2 仍在许多系统中使用,因为它仅到 U+FFFF 的码位。从 Unicode 12.1 开始,超过 57% 的已分配码位均高于 U+FFFF,包括最重要的表情符号。

了解编码/解码问题

尽管存在通用的 UnicodeError 异常,但 Python 报告的错误通常更具体:UnicodeEncodeError(将 str 转换为二进制序列时)或 UnicodeDecodeError(将二进制序列读入 str 时)。当模块的源编码不是按照预期时,加载 Python 模块也可能引发 SyntaxError。我们将在下一节中展示如何处理所有这些错误。

tip:当出现 Unicode 错误时,首先要注意的是异常的具体类型。是 UnicodeEncodeError、UnicodeDecodeError 还是其他一些提到编码问题的错误(例如 SyntaxError)?解决问题前首先需要了解它。

处理 UnicodeEncodeError

大多数非 UTF 编解码器仅处理 Unicode 字符的小部分子集。将文本转换为字节时,如果目标编码中未定义某个字符,则将引发 UnicodeEncodeError异常,除非通过将errors参数传递给编码方法或函数来提供特殊处理。处理错误的方式如下例所示:

>>> city = 'São Paulo'
>>> city.encode('utf_8')  1
b'Sxc3xa3o Paulo'
>>> city.encode('utf_16')
b'xffxfeSx00xe3x00ox00 x00Px00ax00ux00lx00ox00'
>>> city.encode('iso8859_1')  2
b'Sxe3o Paulo'
>>> city.encode('cp437')  3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../lib/python3.4/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')  4
b'So Paulo'
>>> city.encode('cp437', errors='replace')  5
b'S?o Paulo'
>>> city.encode('cp437', errors='xmlcharrefreplace')  6
b'S&#227;o Paulo'

ASCII 是我所知道的所有编码的通用子集,因此如果文本完全由 ASCII 字符组成,编码应该始终有效。Python 3.7 添加了一个新的布尔方法 str.isascii() 来检查您的 Unicode 文本是否是 100% 纯 ASCII。如果是,您应该能够以任何编码将其编码为字节而不会引发 UnicodeEncodeError。

处理 UnicodeDecodeError

不是每个字节都包含有效的 ASCII 字符,也不是每个字节序列都是有效的 UTF-8 或 UTF-16;因此,当您在将二进制序列转换为文本时采用这些编码之一时,如果遇到无法转换的字节时会抛出 UnicodeDecodeError异常。

另一方面,很多陈旧的8位编码--如'ap1252'、'ios8859_1'和'koi8_r' ----能解码任何字节序列而不抛出任何错误,例如随机噪声。因此,如果程序使用错误的8位编码,解码过程悄无声息,则得到的是无用的输出。

tip:乱码被称为 gremlins 或 mojibake(文字化け——日语,意为“转换后的文本”)。

下例说明了使用错误的编解码器如何产生鬼符或 UnicodeDecodeError。

>>> octets = b'Montrxe9al'  
>>> octets.decode('cp1252')  
'Montréal'
>>> octets.decode('iso8859_7')  
'Montrιal'
>>> octets.decode('koi8_r')  
'MontrИal'
>>> octets.decode('utf_8')  
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')  
'Montr�al'

加载带有预期之外的编码的模块时抛出的SyntaxError

UTF-8 是 Python 3 的默认源编码,就像 ASCII 是 Python 2(从 2.5 开始)的默认编码一样。如果加载包含非 UTF-8 数据且没有编码声明的 .py 模块,则会收到如下消息:

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

由于 UTF-8 广泛部署在 GNU/Linux 和 OSX 系统中,可能的情况是打开在 Windows 上使用 cp1252 创建的 .py 文件。请注意,即使在适用于 Windows 的 Python 中也会发生此错误,因为 Python 3 源的默认编码在所有平台上都是 UTF-8。

要解决此问题,请在文件顶部添加魔术编码注释,如下面示例 所示。

# coding: cp1252

print('Olá, Mundo!')

注意:现在 Python 3 源代码不再局限于 ASCII ,而是默认使用优秀的 UTF-8 编码,对于像“cp1252”这样的遗留编码中的源代码,最好的“修复”是已经将它们转换为 UTF-8,而不必理会编码注释。如果您的编辑器不支持 UTF-8,则该换一个了。

如何找到字节序列的编码

你如何找到字节序列的编码?简短的回答:不能。必须有人告诉你。

一些通信协议和文件格式,如 HTTP 和 XML,包含明确指明内容编码的首部。可以确定某些字节流不是 ASCII,因为它们包含超过 127 的字节值,并且 UTF-8 和 UTF-16 的构建方式也限制了可能的字节序列。但即便如此,您也不能仅仅因为某些位模式不存在就 100% 肯定二进制文件是 ASCII 或 UTF-8。

但是,考虑到人类语言也有其规则和限制,一旦您假设字节流是人类纯文本,就有可能使用启发式和统计方法来找出其编码。例如,如果 b'x00' 字节很常见,则可能是 16 位或 32 位编码,而不是 8 位方案,因为纯文本中的空字符是 bug。当字节序列 b'x20x00' 经常出现时,它很可能是 UTF-16LE 编码中的空格字符 (U+0020),而不是晦涩难懂的 U+2000 EN QUAD 字符——谁知道它是什么。

这就是  Chardet — The Universal Character Encoding Detector如何猜测 30 多种支持的编码。Chardet 是一个 Python 库,您可以在您的程序中使用它,但还包括一个命令行实用程序 chardetect。这是它在本章的源文件中报告的内容:

$ chardetect 04-text-byte.asciidoc
04-text-byte.asciidoc: utf-8 with confidence 0.99

尽管编码文本的二进制序列通常不携带其编码的明确提示,但 UTF 格式可能会在文本内容之前添加字节序标记。这将在接下来解释。

BOM:有用的鬼符

在上面中,您可能已经注意到在 UTF-16 编码序列的开头有几个额外的字节。示例如下:

>>> u16 = 'El Niño'.encode('utf_16')
>>> u16
b'xffxfeEx00lx00 x00Nx00ix00xf1x00ox00'

字节为 b'xffxfe'。这是一个 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'xffxfe'(十进制 255、254)。因为,按照设计,Unicode 中没有 U+FFFE 字符,字节序列 b'xffxfe' 必须表示小字节序列编码上的ZERO WIDTH NO-BREAK SPACE,因此编解码器知道使用哪个字节序。

UTF-16 有一种变体——UTF-16LE——它明确使用小字节序,另一个是明确的大字节序,UTF-16BE。如果使用它们,则不会生成 BOM:

>>> u16le = 'El Niño'.encode('utf_16le')
>>> list(u16le)
[69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
>>> u16be = 'El Niño'.encode('utf_16be')
>>> list(u16be)
[0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]

如果存在,BOM 应该由 UTF-16 编解码器过滤,以便您只能获得文件的实际文本内容,而没有前导的ZERO WIDTH NO-BREAK SPACE。Unicode 标准规定,如果文件是 UTF-16 且没有 BOM,则应假定它是 UTF-16BE(大字节序)。然而,Intel x86 架构是小字节序的,所以有很多没有 BOM 的小字节序 UTF-16。

整个字节序问题仅影响使用超过一个字节的字(word)的编码,例如 UTF-16 和 UTF-32。UTF-8 的一大优势是无论机器字节序如何,它都会生成相同的字节序列,因此不需要 BOM。一些 Windows 应用程序(特别是记事本)无论如何都会将 BOM 添加到 UTF-8 文件中——Excel 依赖 BOM 来检测 UTF-8 文件,否则它假定内容是用 Windows 代码页编码的。UTF-8 编码的字符 U+FEFF 是三字节序列 b'xefxbbxbf'。因此,如果文件以这三个字节开头,则很可能是带有 BOM 的 UTF-8 文件。但是,Python 不会仅仅因为文件以 b'xefxbbxbf' 开头就自动假定它是 UTF-8。

处理文本文件

处理文本 I/O 的最佳实践是“Unicode 三明治”。这意味着应该在输入(例如,在打开文件进行读取时)时尽早将bytes解码为 str 。三明治的“肉片”是程序的业务逻辑,其中仅在 str 对象上完成文本处理。您永远不应该在其他处理过程中进行编码或解码。在输出时, str要尽可能晚地编码为bytes。大多数 Web 框架都是这样工作的,我们在使用它们时很少接触bytes。例如,在 Django 中,视图应该输出 Unicode str; Django 本身负责将响应编码为bytes,并且默认使用 UTF-8编码。

Python 3 使得遵循 Unicode 三明治的建议变得更容易,因为 open 内置在读取时进行必要的解码,在以文本模式写入文件时进行编码,所以你从 my_file.read() 获得并传递给 my_file.write(text) 的都是 str 对象。

因此,使用文本文件显然很简单。但是如果依赖默认编码,你会遇到麻烦。

看一下下例中的控制台对话,你能发现问题吗?

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

错误是我在写入文件时指定了 UTF-8 编码,但在读取文件时没有这样做,因此 Python 假定 Windows 默认文件编码 - code page 1252 - 并且文件中的字节被解码为字符 'é' 而不是'é'。

我在 Python 3.8.1、64 位的Windows 10(内部版本 18363)上运行了上例。在最近的 GNU/Linux 或 Mac OSX 上运行的相同语句运行良好,因为它们的默认编码是 UTF-8,给人一种一切都很好的错误印象。如果在打开要写入的文件时省略了 encoding 参数,则将使用语言环境默认编码,使用相同的编码也可以正确读取文件。但是这个脚本会根据平台甚至根据同一平台的区域设置生成具有不同字节内容的文件,从而产生兼容性问题。

tip:在多台机器上或在多个场合运行的代码不应该依赖于编码默认值。打开文本文件时始终传递显式 encoding= 参数,因为不同机器的默认编码不同,有时隔一天也会发生变化。

上例 中一个奇怪的细节是,第一条语句中的 write 函数报告写入了四个字符,但在下一行读取了五个字符。下例是扩展版本,解释了这一点和其他细节。

>>> fp = open('cafe.txt', 'w', encoding='utf_8')
>>> fp  
<_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'>
>>> fp.write('café')  
4
>>> fp.close()
>>> import os
>>> os.stat('cafe.txt').st_size  
5
>>> fp2 = open('cafe.txt')
>>> fp2  
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'>
>>> fp2.encoding  
'cp1252'
>>> fp2.read() 
'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')  
>>> fp4                           
<_io.BufferedReader name='cafe.txt'>
>>> fp4.read()  
b'cafxc3xa9'

tip:不要以二进制模式打开文本文件,除非您需要分析文件内容以确定编码——即便如此,您也应该使用 Chardet 而不是重新发明轮子.普通代码应该只使用二进制模式打开二进制文件,比如光栅图像。

示例 中的问题与打开文本文件时依赖默认设置有关。此类默认值有多种来源,如下一节所示。

小心默认编码值

一些设置会影响 Python 中 I/O 的编码默认值。请参阅示例中的 default_encodings.py 脚本。

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

示例在 GNU/Linux(Ubuntu 14.04 到 19.10)和 MacOS(10.9 到 10.14)上的输出是相同的,表明在这些系统中到处都使用了 UTF-8:

$ python3 default_encodings.py
 locale.getpreferredencoding() -> 'UTF-8'
                 type(my_file) -> <class '_io.TextIOWrapper'>
              my_file.encoding -> 'UTF-8'
           sys.stdout.isatty() -> True
           sys.stdout.encoding -> 'utf-8'
            sys.stdin.isatty() -> True
            sys.stdin.encoding -> 'utf-8'
           sys.stderr.isatty() -> True
           sys.stderr.encoding -> 'utf-8'
      sys.getdefaultencoding() -> 'utf-8'
   sys.getfilesystemencoding() -> 'utf-8'

然而,在Windows的输出有所不同:

> chcp  
Active code page: 437
> python default_encodings.py  
 locale.getpreferredencoding() -> 'cp1252'  
                 type(my_file) -> <class '_io.TextIOWrapper'>
              my_file.encoding -> 'cp1252'  
           sys.stdout.isatty() -> True      
           sys.stdout.encoding -> 'utf-8'   
            sys.stdin.isatty() -> True
            sys.stdin.encoding -> 'utf-8'
           sys.stderr.isatty() -> True
           sys.stderr.encoding -> 'utf-8'
      sys.getdefaultencoding() -> 'utf-8'
   sys.getfilesystemencoding() -> 'utf-8'

自从我在 Fluent Python 的第 1 版中写到这一点以来,Windows 本身和 Python for Windows 中的 Unicode 支持变得更好。上例用于在 Windows 7 上报告 Python 3.4 中的四种不同编码。stdout、stdin 和 stderr 的编码过去与 chcp 命令报告的活动代码页相同,但现在它们都是 utf-8 --多亏了在python3.6支持的  PEP 528: Change Windows console encoding to UTF-8 ,以及 cmd.exe 中 PowerShell 中的 Unicode 支持(自 2018 年 10 月起 Windows 1809)。奇怪的是 chcp 和 sys.stdout.encoding 在 stdout 写入控制台时编码不同,但现在我们可以在 Windows 上打印 Unicode 字符串而不会出现编码错误,这很棒——除非用户将输出重定向到一个文件,下面就会看到。这并不意味着您喜欢的所有表情符号都会出现在控制台中:这也取决于控制台使用的字体。

另一个变化是 PEP 529: Change Windows filesystem encoding to UTF-8,也在 Python 3.6 中实现,它将文件系统编码(用于表示目录和文件的名称)从 Microsoft 专有的 MBCS 更改为 UTF-8。

但是,如果上面示例的输出被重定向到一个文件,如下所示:

Z:>python default_encodings.py > encodings.log

然后,sys.stdout.isatty() 的值变为 False,并且 sys.stdout.encoding 由该机器中的 locale.getpreferredencoding(), 'cp1252' 设置——但 sys.stdin.encoding 和 sys.stderr.encoding 仍然为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 = [
    'u2026',  # HORIZONTAL ELLIPSIS (in cp1252)
    'u221E',  # INFINITY (in cp437)
    'u32B7',  # CIRCLED NUMBER FORTY TWO
]

for char in test_chars:
    print(f'Trying to output {name(char)}:')
    print(char)

上面示例显示了 sys.stdout.isatty() 的结果、sys.stdout.encoding 的值以及这三个字符:

  • '…' HORIZONTAL ELLIPSIS (U+2026)--exists in CP 1252 but not in CP 437

  • '∞' INFINITY (U+221E)--exists in CP 437 but not in CP 1252

  • '㊷' CIRCLED NUMBER FORTY TWO (U+2026)--doesn’t exist in CP 1252 or CP 437

当我在 PowerShell 或 cmd.exe 上运行 stdout_check.py 时,结果如下图所示:

尽管 chcp 报告活动代码为 437,但 sys.stdout.encoding 是 UTF-8,因此 HORIZONTAL ELLIPSIS 和 INFINITY 都正确输出。CIRCLED NUMBER FORTY被矩形替换,但没有报错。也许它被识别为有效字符,但控制台字体不能显示它的字形。

然而,当我将 stdout_check.py 的输出重定向到一个文件时,我得到了下图:

上图展示的第一个问题是 UnicodeEncodeError 将问题定位到字符“u221e”,因为 sys.stdout.encoding 是“cp1252”——一个没有 INFINITY 字符的CP(code page)。

然后,检查部分写入的 out.txt,我得到两个惊喜:

  1. 使用 type 命令或 Windows 编辑器(如 VS Code 或 Sublime Text)读取 out.txt 显示,我得到的是 'à'(带有坟墓的拉丁文小写字母 A)而不是水平省略号。事实证明,CP 1252 中的字节值 0x85 表示“...”,但在 CP 437 中,相同的字节值表示“à”。因此,似乎活动代码页确实很重要,不是以明智或有用的方式,而是作为对糟糕的 Unicode 体验的部分解释。事实证明,CP 1252 中的字节值 0x85 表示“...”,但在 CP 437 中,相同的字节值表示“à”。因此,似乎生效的code page确实很重要,不是通过合理或有用的方式,而是作为对糟糕的 Unicode 体验的部分解释。
  2. out.txt 是用 UTF-16 LE 编码编写的。这样效果不错,因为 UTF 编码支持所有 Unicode 字符——如果不是不幸地用 'à' 替换了 '...'。

注意:我使用为美国市场配置的笔记本电脑,运行 Windows 10 OEM 来运行这些实验。为其他国家/地区本地化的 Windows 版本可能具有不同的编码配置。例如,在巴西,Windows 控制台默认使用code page 850,而不是 437。

为了解决这个令人抓狂的默认编码问题,让我们最后看看示例中的不同编码:

  • 如果在打开文件时省略 encoding 参数,则默认值由 locale.getpreferredencoding()给出。
  • sys.stdout|stdin|stderr 的编码在 Python 3.6 之前由 PYTHONIOENCODING 环境变量设置——现在该变量被忽略,除非 PYTHONLEGACYWINDOWSSTDIO 设置为非空字符串。否则,对于交互式 I/O(CMD,Power shell),标准 I/O 的编码是 UTF-8,或者如果输出/输入重定向到/从文件重定向,则由 locale.getpreferredencoding() 定义。
  • sys.getdefaultencoding() 由 Python 在内部用于二进制数据到/从 str 的隐式转换;这种情况在 Python 3 中较少发生,但仍然会发生。目前不支持更改此设置。
  • sys.getfilesystemencoding() 用于编码/解码文件名(不是文件内容)。当 open() 获取文件名的 str 参数时使用它;如果文件名作为 bytes 参数给出,则它会原封不动地传递给 OS API。在 Python 3.6 之前,编码在 Windows 上是 MBCS,现在是 UTF-8。

注意:在 GNU/Linux 和 OSX 上,所有这些编码都默认设置为 UTF-8,并且已经有好几年了,因此 I/O 可以处理所有 Unicode 字符。在 Windows 上,不仅在同一系统中使用不同的编码,而且它们通常是像“cp850”或“cp1252”这样的code page,它们仅支持带有 127 个附加字符的 ASCII,这些字符在不同编码中也都不相同。因此,Windows 用户更容易遇到编码错误。

总而言之,最重要的编码设置是 locale.getpreferredencoding() 返回的设置:当它们被重定向到文件时,它是打开文本文件和 sys.stdout/stdin/stderr 的默认设置。但是,文档是这么写的:
locale.getpreferredencoding(do_setlocale=True):
根据用户偏好返回用于文本数据的编码。用户偏好在不同系统上的表达方式不同,并且在某些系统上可能无法以编程方式使用,因此此函数仅返回猜测值。 […]

因此,关于编码默认值的最佳建议是:不要依赖它们。

如果您遵循 Unicode 三明治的建议并始终明确说明程序中的编码,您将避免很多痛苦。不幸的是,即使您将字节正确转换为 str,Unicode 也是不能尽如人意的。接下来的两节涵盖了在 ASCII 领域很简单但在 Unicode 星球上变得相当复杂的主题:文本规范化(即,将文本转换为统一表示以进行比较)和排序。

最后

以上就是雪白康乃馨为你收集整理的第四章 文本和字节序列的全部内容,希望文章能够帮你解决第四章 文本和字节序列所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(36)

评论列表共有 0 条评论

立即
投稿
返回
顶部