为什么Python在默认编码为ASCII时仍然打印unicode字符?
来自Python 2.6的命令行:
>>> import sys
>>> print sys.getdefaultencoding()
ascii
>>> print u'\xe9'
é
>>>
我本以为在打印语句后会出现一些乱码或者错误,因为“é”这个字符不在ASCII字符集里,而且我也没有指定编码。我想我还不太明白默认编码是ASCII是什么意思。
编辑
6 个回答
Python的交互式命令行(REPL)会根据你的环境来选择使用什么编码。如果它找到一个合适的编码,那么一切都会正常运行。但如果它搞不清楚情况,就会出现问题。
>>> print sys.stdout.encoding
UTF-8
当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字符写入终端。如果字体支持这些字符,就不会出现Unicode编码错误的警告,并且字符会正确显示。即使字体不支持这些字符,你仍然可以从终端复制粘贴到支持的应用程序中,显示也会是正确的。建议你升级一下!
感谢大家的回复,我们可以把这些信息整理成一个解释。
当你尝试打印一个unicode字符串,比如u'\xe9'时,Python会自动尝试使用当前在sys.stdout.encoding中存储的编码方式来编码这个字符串。Python会从它启动的环境中获取这个设置。如果找不到合适的编码,Python才会退回到它的默认编码,即ASCII。
举个例子,我使用的bash shell默认编码是UTF-8。如果我从这个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 Terminal)中启动一个bash shell,并将终端设置为使用ISO-8859-1,也就是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。0xc3a9是2个字节长,latin-1解码器因此将其解释为0xc3 (195) 和 0xa9 (169),所以得到两个字符:Ã和©。
(3) Python使用latin-1编码unicode代码点u'\xe9' (233)。结果是,latin-1的代码点范围是0-255,并且在这个范围内,它指向的字符与Unicode是相同的。因此,在这个范围内的Unicode代码点在latin-1编码时会得到相同的值。所以u'\xe9' (233)在latin-1编码后也会得到二进制字符串'\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直接输出一个二进制字符串。终端尝试用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在输出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在ASCII、latin-1和Unicode中都指向'A',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在代码点中嵌入标志位,以指示(大概是给解码器)它们的空间需求和边界。
UTF-8对ASCII范围(0-127)内的Unicode代码点的编码:
0xxx xxxx (in binary)
- x表示在编码过程中“存储”代码点所保留的实际空间
- 前导0是一个标志,指示UTF-8解码器这个代码点只需要1个字节。
- 在编码时,UTF-8不会改变这个特定范围内代码点的值(即65在UTF-8中编码后仍然是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)
UTF-8对127以上(非ASCII)Unicode代码点的编码:
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
0xe9的Unicode代码点在UTF-8编码后变成0xc3a9。这正是终端接收到的值。如果你的终端设置为使用latin-1解码字符串(一种非Unicode的遗留编码),你会看到é,因为恰好0xc3在latin-1中指向Ã,0xa9指向©。