为什么默认编码是 ASCII 时 Python 会打印 unicode 字符?
- 2025-02-27 09:05:00
- admin 原创
- 52
问题描述:
从 Python 2.6 shell 中:
>>> import sys
>>> print sys.getdefaultencoding()
ascii
>>> print u'xe9'
é
>>>
我预计打印语句后会出现一些乱码或错误,因为“é”字符不是 ASCII 的一部分,而且我还没有指定编码。我想我不明白 ASCII 作为默认编码意味着什么。
编辑
我将编辑移至答案部分并按照建议接受了它。
解决方案 1:
根据各种回复的点点滴滴,我想我们可以得出一个解释。
当尝试打印 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 输出字符串后会发生什么。为此,我们将首先在图形终端中启动 bash shell(我将使用 Gnome 终端)。我们将设置终端以使用 ISO-8859-1 又名 Latin-1 解码输出(图形终端通常在其下拉菜单之一中有一个“设置字符编码”选项)。请注意,这不会更改实际shell 环境的编码,它只会更改终端本身解码给定输出的方式,有点像 Web 浏览器。因此,您可以独立于 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 编码后,生成的二进制字符串为'xc3xa9'
(请参阅后面的解释)。终端接收流并尝试使用 Latin-1 解码 0xc3a9,但 Latin-1 从 0 到 255,因此每次只能解码 1 个字节的流。0xc3a9 长 2 个字节,因此 Latin-1 解码器将其解释为两个不同的字节,0xc3(195)和 0xa9(169),分别产生字符'Ã'
和'©'
。
(3) pythonu'xe9'
使用 Latin-1 方案对 Unicode 代码点 (233) 进行编码。事实证明,Latin-1 代码点范围是 0-255,它指向的字符与 Unicode 在该范围内指向的字符完全相同。因此,0-255 之间的 Unicode 代码点在用 Latin-1 编码时将产生相同的值。因此,u'xe9'
用 Latin-1 编码的 (233) 也会产生二进制字符串'xe9'
。终端接收该值并尝试将其与 Latin-1 字符映射进行匹配。就像情况 (1) 一样,它会产生“é”,这就是显示的内容。
现在让我们从下拉菜单中将终端的编码设置更改为 UTF-8(就像更改 Web 浏览器的编码设置一样)。无需停止 Python 或重新启动 shell。终端的编码现在与 Python 的编码匹配。让我们再次尝试打印:
>>> print 'xe9' # (4)
>>> print u'xe9' # (5)
é
>>> print u'xe9'.encode('latin-1') # (6)
>>>
(4) python 按原样输出二进制字符串。终端尝试使用 UTF-8 解码该流。但 UTF-8 无法理解值 0xe9(请参阅后面的解释),因此无法将其转换为 Unicode 代码点。未找到代码点,未打印任何字符。
(5) python 尝试使用当前设置的任何内容(仍为 UTF-8)隐式编码 Unicode 字符串sys.stdout.encoding
。生成的二进制字符串为 'é'。终端接收流并尝试使用 UTF-8 解码 0xc3a9。它产生返回代码值 0xe9 (233),它在 Unicode 字符映射上指向符号“é”。终端显示“é”。
(6) python 使用 Latin-1 对 Unicode 字符串进行编码,生成一个具有相同值 'é' 的二进制字符串。同样,对于终端来说,这与情况 (4) 几乎相同。
结论:
Python 将非 Unicode 字符串输出为原始数据,而不考虑其默认编码。如果终端的当前编码与数据匹配,则终端会恰好显示它们。
Python 使用 中指定的方案对 Unicode 字符串进行编码后输出
sys.stdout.encoding
。Python 从 shell 的环境中获取该设置。
终端根据自己的编码设置显示输出。
终端的编码与 shell 的编码无关。
有关 Unicode、UTF-8 和 Latin-1 的更多详细信息
Unicode 本质上是一个字符表,其中某些键(代码点)已按惯例分配给特定符号。例如,按照惯例,已确定十六进制键0xe9
(十进制 233)指向符号“é”。ASCII 和 Unicode 使用相同的代码点(从 0 到 127),Latin-1 和 Unicode 也使用相同的代码点(从 0 到 255)。也就是说,0x41(十进制 65)在 ASCII、Latin-1 和 Unicode 中指向“A”,0xc8 在 Latin-1 和 Unicode 中指向“Ü”,0xe9 在 Latin-1 和 Unicode 中指向“é”。
在处理电子产品时,Unicode 代码点需要一种有效的表示方案。这就是编码的意义所在。存在各种 Unicode 编码方案(UTF-7、UTF-8、UTF-16、UTF-32)。最直观、最直接的编码方法是简单地使用 Unicode 映射中的代码点值作为其电子形式的值,但 Unicode 目前有超过一百万个代码点,这意味着其中一些代码点需要 3 个字节来表示。为了有效地处理文本,1 对 1 映射是不切实际的,因为它要求所有代码点都存储在完全相同的空间中,每个字符至少需要 3 个字节,而不管它们的实际需要如何。
大多数编码方案在空间要求方面存在缺陷,最经济的编码方案会遗漏许多 Unicode 代码点。例如,ASCII 仅涵盖前 128 个 Unicode 代码点,而 Latin-1 仅涵盖前 256 个。其他试图更全面的编码最终也是一种浪费,因为即使是“便宜”的代码点,它们也需要比必要更多的字节空间。例如,UTF-16 每个代码点至少使用 2 个字节,包括那些通常只需要一个字节的 ASCII 范围内的代码点(例如,'B' 为 66,在 UTF-16 中仍需要 2 个字节的存储空间)。UTF-32 更加浪费,因为它将所有代码点存储在 4 个字节中。
UTF-8 方案(比 UTF-16 和 UTF-32 更新颖)恰好巧妙地缓解了这一困境。它能够存储具有可变字节空间的代码点。作为其编码策略的一部分,UTF-8 将代码点与标志位连接起来,以指示(大概是向解码器)其空间要求及其边界。
ASCII 范围 (0-127) 内的 Unicode 代码点的 UTF-8 编码
0xxx xxxx (in binary)
x 显示在编码过程中为“存储”代码点而保留的实际空间。
前导 0 是一个标志,它向 UTF-8 解码器指示该代码点只需要 1 个字节。
编码时,UTF-8 不会改变该特定范围内的 Unicode 代码点的值(即,以 UTF-8 编码的 Unicode 65 也是 65)。考虑到 ASCII 也与该范围内的 Unicode 兼容,它顺便使 ASCII 与 UTF-8 兼容(对于该范围)。
例如,Unicode 中“B”的代码点为“0x42”(十进制为 66),0100 0010
二进制为 0x42。如前所述,ASCII 中也是一样。下面是其 UTF-8 编码的描述:
0xxx xxxx <-- UTF-8 wrapper for Unicode code points in the range 0 - 127
*100 0010 <-- Unicode code point 0x42
0100 0010 <-- UTF-8 encoded (exactly the same)
用于 Unicode 代码点超过 127(超出 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 字节 UTF-8 包装器对其进行编码:
110x xxxx 10xx xxxx <-- 2-byte UTF-8 wrapper for Unicode 128-2047
***0 0011 **10 1001 <-- 0xe9
1100 0011 1010 1001 <-- 'é' after UTF-8 encoding
C 3 A 9
UTF-8 编码后的 Unicode 代码点 0xe9 变为 0xc3a9。这正是终端接收它的方式。如果您的终端设置为使用 Latin-1 解码字符串,您将看到“é”,因为 Latin-1 中的 0xc3 恰好指向 Ã,而 0xa9 指向 ©。
解决方案 2:
当 Unicode 字符打印到 stdout 时,sys.stdout.encoding
使用。非 Unicode 字符被假定为sys.stdout.encoding
,并直接发送到终端。在我的系统 (Python 2) 上:
>>> import unicodedata as ud
>>> import sys
>>> sys.stdout.encoding
'cp437'
>>> ud.name(u'xe9') # U+00E9 Unicode codepoint
'LATIN SMALL LETTER E WITH ACUTE'
>>> ud.name('xe9'.decode('cp437'))
'GREEK CAPITAL LETTER THETA'
>>> 'xe9'.decode('cp437') # byte E9 decoded using code page 437 is U+0398.
u'/u0398'
>>> ud.name(u'/u0398')
'GREEK CAPITAL LETTER THETA'
>>> print u'xe9' # Unicode is encoded to CP437 correctly
é
>>> print 'xe9' # Byte is just sent to terminal and assumed to be CP437.
Θ
sys.getdefaultencoding()
仅当 Python 没有其他选项时才使用。
请注意,Python 3.6 或更高版本会忽略 Windows 上的编码并使用 Unicode API 将 Unicode 写入终端。如果字体支持,则不会出现 UnicodeEncodeError 警告,并且会显示正确的字符。即使字体不支持,仍然可以将字符从终端剪切粘贴到具有支持字体的应用程序中,并且显示正确。升级!
解决方案 3:
Python REPL 会尝试从您的环境中选择要使用的编码。如果它找到了合理的编码,那么一切都会正常进行。当它无法弄清楚发生了什么时,就会出错。
>>> print sys.stdout.encoding
UTF-8
解决方案 4:
您已通过输入显式 Unicode 字符串指定了编码。比较不使用前缀的结果u
。
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> 'xe9'
'xe9'
>>> u'xe9'
u'xe9'
>>> print u'xe9'
é
>>> print 'xe9'
>>>
在这种情况下,xe9
Python 会假定您的默认编码(Ascii),因此打印...一些空白内容。
解决方案 5:
它对我有用:
import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')
解决方案 6:
按照Python 默认/隐式字符串编码和转换:
当
print
ing时unicode
,它是encode
d 的<file>.encoding
。
+ 当`encoding`未设置时,`unicode`会隐式转换为`str`(因为 的编解码器是`sys.getdefaultencoding()`,即`ascii`,任何国家字符都会导致`UnicodeEncodeError`)
+ 对于标准流,`encoding`是从环境中推断出来的。它通常针对`tty`流进行设置(来自终端的区域设置),但可能不会针对管道进行设置
- `print u'xe9'`因此,当输出到终端时,a很可能会成功,如果重定向,则可能会失败。一种解决方案是在 ing`encode()`之前使用所需的编码对字符串进行编码`print`。
当
print
ing时str
,字节将按原样发送到流。终端显示的字形将取决于其语言环境设置。
扫码咨询,免费领取项目管理大礼包!