繁体   English   中英

为什么Python在默认编码为ASCII时会打印unicode字符?

[英]Why does Python print unicode characters when the default encoding is ASCII?

从Python 2.6 shell:

>>> import sys
>>> print sys.getdefaultencoding()
ascii
>>> print u'\xe9'
é
>>> 

我希望在print语句之后有一些乱码或错误,因为“é”字符不是ASCII的一部分,我没有指定编码。 我想我不明白ASCII是默认编码的意思。

编辑

我将编辑移动到了答案部分并按照建议接受了它。

感谢各种回复的点点滴滴,我想我们可以解释一下。

通过尝试打印unicode字符串u'\\ xe9',Python隐式尝试使用当前存储在sys.stdout.encoding中的编码方案对该字符串进行编码。 Python实际上是从它所启动的环境中获取此设置。 如果它无法从环境中找到正确的编码,那么它才会恢复为默认的 ASCII。

例如,我使用bash shell,其默认编码为UTF-8。 如果我从它启动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环境的编码,它只会改变终端本身解码输出的方式,有点像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编码之后,生成的二进制字符串是'\\ 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完全相同的字符。 因此,在latin-1中编码时,该范围内的Unicode代码点将产生相同的值。 因此,在latin-1中编码的u'\\ xe9'(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尝试使用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字符串进行编码后输出Unicode字符串。 - Python从shell的环境中获取该设置。 - 终端根据自己的编码设置显示输出。 - 终端的编码与shell的编码无关。


关于unicode,UTF-8和latin-1的更多细节:

Unicode基本上是一个字符表,其中一些键(代码点)通常被指定为指向某些符号。 例如,按照惯例,已经确定密钥0xe9(233)是指向符号'é'的值。 ASCII和Unicode使用相同的代码点,从0到127,latin-1和Unicode从0到255.也就是说,0x41指向ASCII中的'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范围内的unicode代码点的UTF-8编码(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)

Unicode代码的UTF-8编码高于127(非ascii):

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指向©。

将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警告并显示正确的字符。 即使字体支持它,字符仍然可以从终端切割到具有支持字体的应用程序,并且它将是正确的。 升级!

Python REPL尝试从您的环境中获取要使用的编码。 如果它找到了理智的东西那么它就是Just Works。 它是什么时候它无法弄清楚它发生了什么,它的错误。

>>> print sys.stdout.encoding
UTF-8

通过输入显式Unicode字符串指定了编码。 比较不使用u前缀的结果。

>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> '\xe9'
'\xe9'
>>> u'\xe9'
u'\xe9'
>>> print u'\xe9'
é
>>> print '\xe9'

>>> 

\\xe9的情况下,Python假定您的默认编码(Ascii),因此打印......空白。

这个对我有用:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')

根据Python默认/隐式字符串编码和转换

  • print unicode ,用<file>.encoding encode d。
    • 当没有设置encoding时, unicode被隐式转换为str (因为它的编解码器是sys.getdefaultencoding() ,即ascii ,任何国家字符都会导致UnicodeEncodeError
    • 对于标准流, encoding是从环境推断的。 它通常设置为tty流(来自终端的语言环境设置),但可能不会为管道设置
      • 因此,当输出到终端时, print u'\\xe9'可能会成功,如果重定向则print u'\\xe9'失败。 一种解决方案是在print之前用所需的编码对字符串进行encode()
  • print str ,字节按原样发送到流。 终端显示的字形将取决于其区域设置。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM