感谢各种回复的点点滴滴,我想我们可以拼凑一个解释。
通过尝试打印 unicode 字符串 u'\xe9',Python 隐式尝试使用当前存储在 sys.stdout.encoding 中的编码方案对该字符串进行编码。 Python 实际上是从启动它的环境中获取这个设置的。如果它不能从环境中找到合适的编码,那么它才会恢复到它的默认,ASCII。
例如,我使用编码默认为 UTF-8 的 bash shell。如果我从它启动 Python,它会选择并使用该设置:
$ python
>>> import sys
>>> print sys.stdout.encoding
UTF-8
让我们暂时退出 Python shell 并使用一些虚假编码设置 bash 的环境:
$ export LC_CTYPE=klingon
# we should get some error message here, just ignore it.
然后再次启动 python shell 并验证它确实恢复为默认的 ascii 编码。
$ python
>>> import sys
>>> print sys.stdout.encoding
ANSI_X3.4-1968
宾果!
如果您现在尝试在 ascii 之外输出一些 unicode 字符,您应该会收到一条不错的错误消息
>>> print u'\xe9'
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9'
in position 0: ordinal not in range(128)
让我们退出 Python 并丢弃 bash shell。
我们现在将观察 Python 输出字符串后会发生什么。为此,我们将首先在图形终端(我使用 Gnome 终端)中启动一个 bash shell,然后我们将终端设置为使用 ISO-8859-1 aka latin-1 解码输出(图形终端通常有一个 在其下拉菜单之一中设置字符编码)。请注意,这不会改变实际 shell 环境的 编码,它只会改变 终端 本身对它给出的输出进行解码的方式,有点像网络浏览器。因此,您可以独立于 shell 环境更改终端的编码。然后让我们从 shell 启动 Python 并验证 sys.stdout.encoding 是否设置为 shell 环境的编码(对我来说是 UTF-8):
$ python
>>> import sys
>>> print sys.stdout.encoding
UTF-8
>>> print '\xe9' # (1)
é
>>> print u'\xe9' # (2)
é
>>> print u'\xe9'.encode('latin-1') # (3)
é
>>>
(1) python 按原样输出二进制字符串,终端接收它并尝试将其值与 latin-1 字符映射匹配。在 latin-1 中,0xe9 或 233 产生字符“é”,这就是终端显示的内容。
(2) python 尝试隐式使用 sys.stdout.encoding 中当前设置的任何方案对 Unicode 字符串进行编码,在本例中为“UTF-8”。经过 UTF-8 编码后,生成的二进制字符串是 '\xc3\xa9'(见后面的解释)。终端这样接收流并尝试使用 latin-1 解码 0xc3a9,但 latin-1 从 0 变为 255,因此一次只解码 1 个字节的流。 0xc3a9 有 2 个字节长,因此 latin-1 解码器将其解释为 0xc3 (195) 和 0xa9 (169) 并产生 2 个字符:Ã 和 ©。
(3) python 使用 latin-1 方案对 unicode 代码点 u'\xe9' (233) 进行编码。结果 latin-1 代码点的范围是 0-255,并且指向与该范围内的 Unicode 完全相同的字符。因此,该范围内的 Unicode 代码点在以 latin-1 编码时将产生相同的值。因此以 latin-1 编码的 u'\xe9' (233) 也会产生二进制字符串 '\xe9'。终端接收该值并尝试在 latin-1 字符映射上匹配它。就像案例(1)一样,它产生“é”,这就是显示的内容。
现在让我们从下拉菜单中将终端的编码设置更改为 UTF-8(就像您更改网络浏览器的编码设置一样)。无需停止 Python 或重新启动 shell。终端的编码现在匹配 Python 的。让我们再次尝试打印:
>>> print '\xe9' # (4)
>>> print u'\xe9' # (5)
é
>>> print u'\xe9'.encode('latin-1') # (6)
>>>
(4) python 按原样输出 binary 字符串。终端尝试使用 UTF-8 解码该流。但是 UTF-8 不理解值 0xe9(请参阅后面的解释),因此无法将其转换为 unicode 代码点。未找到代码点,未打印字符。
(5) python 尝试隐式使用 sys.stdout.encoding 中的任何内容对 Unicode 字符串进行编码。仍然是“UTF-8”。生成的二进制字符串是 '\xc3\xa9'。终端接收流并尝试也使用 UTF-8 解码 0xc3a9。它产生返回码值 0xe9 (233),它在 Unicode 字符映射上指向符号“é”。终端显示“é”。
(6) python 用 latin-1 编码 unicode 字符串,它产生一个具有相同值 '\xe9' 的二进制字符串。同样,对于终端,这与案例 (4) 几乎相同。
结论:
- Python 将非 unicode 字符串作为原始数据输出,而不考虑其默认编码。如果终端当前的编码与数据匹配,终端恰好会显示它们。
- Python 使用 sys.stdout.encoding 中指定的方案编码后输出 Unicode 字符串。
- Python 从 shell 环境中获取该设置。
- 终端根据自己的编码设置显示输出。
- 终端的编码独立于 shell 的。
有关 unicode、UTF-8 和 latin-1 的更多详细信息:
Unicode 基本上是一个字符表,其中一些键(代码点)通常被分配以指向一些符号。例如按照惯例,已决定键 0xe9 (233) 是指向符号“é”的值。 ASCII 和 Unicode 使用从 0 到 127 的相同代码点,latin-1 和 Unicode 从 0 到 255 也是如此。即 ASCII 中的 0x41 指向 'A',latin-1 和 Unicode 中的 0xc8 指向 'Ü' latin-1 和 Unicode,0xe9 指向 latin-1 和 Unicode 中的 'é'。
在使用电子设备时,Unicode 代码点需要一种有效的电子方式来表示。这就是编码方案的意义所在。存在各种 Unicode 编码方案(utf7、UTF-8、UTF-16、UTF-32)。最直观和直接的编码方法是简单地使用 Unicode 映射中的代码点值作为其电子形式的值,但 Unicode 目前有超过一百万个代码点,这意味着其中一些需要 3 个字节表达。为了有效地处理文本,1 对 1 映射将是相当不切实际的,因为它要求所有代码点存储在完全相同的空间中,每个字符至少 3 个字节,而不管它们的实际需要。
大多数编码方案在空间要求方面存在缺陷,最经济的方案并未涵盖所有 unicode 码位,例如 ascii 仅涵盖前 128 个,而 latin-1 涵盖前 256 个。其他尝试更全面的最终也很浪费,因为它们需要比必要更多的字节,即使对于常见的“便宜”字符也是如此。例如,UTF-16 每个字符至少使用 2 个字节,包括 ascii 范围内的那些('B' 是 65,在 UTF-16 中仍然需要 2 个字节的存储空间)。 UTF-32 更加浪费,因为它将所有字符存储在 4 个字节中。
UTF-8 恰好巧妙地解决了这一难题,其方案能够存储具有可变字节空间数量的代码点。作为其编码策略的一部分,UTF-8 将代码点与指示(可能对解码器)它们的空间要求和边界的标志位相结合。
ASCII 码点的 UTF-8 编码在 ascii 范围 (0-127) 中:
0xxx xxxx (in binary)
- x 表示在编码期间保留用于“存储”代码点的实际空间
- 前导 0 是一个标志,向 UTF-8 解码器指示此代码点只需要 1 个字节。
- 在编码时,UTF-8 不会更改该特定范围内代码点的值(即 UTF-8 编码的 65 也是 65)。考虑到 Unicode 和 ASCII 在同一范围内也兼容,顺便说一下,UTF-8 和 ASCII 在该范围内也兼容。
例如'B' 的 Unicode 代码点在二进制中是 '0x42' 或 0100 0010(正如我们所说,在 ASCII 中是相同的)。 UTF-8编码后变成:
0xxx xxxx <-- UTF-8 encoding for Unicode code points 0 to 127
*100 0010 <-- Unicode code point 0x42
0100 0010 <-- UTF-8 encoded (exactly the same)
127 以上的 Unicode 码位(非 ascii)的 UTF-8 编码:
110x xxxx 10xx xxxx <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx <-- (from 2048 to 65535)
- 前导位“110”向 UTF-8 解码器指示以 2 个字节编码的代码点的开头,而“1110”表示 3 个字节,11110 表示 4 个字节,依此类推。
- 内部“10”标志位用于表示内部字节的开始。
- 同样,x 标记编码后存储 Unicode 代码点值的空间。
例如'é' Unicode 代码点是 0xe9 (233)。
1110 1001 <-- 0xe9
UTF-8对该值进行编码时,判断该值大于127小于2048,因此应编码为2字节:
110x xxxx 10xx xxxx <-- UTF-8 encoding for Unicode 128-2047
***0 0011 **10 1001 <-- 0xe9
1100 0011 1010 1001 <-- 'é' after UTF-8 encoding
C 3 A 9
UTF-8 编码后的 0xe9 Unicode 码位变为 0xc3a9。这正是终端接收它的方式。如果您的终端设置为使用 latin-1(非 unicode 传统编码之一)解码字符串,您将看到 é,因为恰好 latin-1 中的 0xc3 指向 à 和 0xa9 指向 ©。