当默认编码为 ASCII 时,为啥 Python 会打印 unicode 字符?

Posted

技术标签:

【中文标题】当默认编码为 ASCII 时,为啥 Python 会打印 unicode 字符?【英文标题】:Why does Python print unicode characters when the default encoding is ASCII?当默认编码为 ASCII 时,为什么 Python 会打印 unicode 字符? 【发布时间】:2011-02-05 12:07:29 【问题描述】:

从 Python 2.6 外壳:

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

我预计在打印语句之后会有一些乱码或错误,因为“é”字符不是 ASCII 的一部分,而且我没有指定编码。我想我不明白 ASCII 作为默认编码是什么意思。

编辑

I moved the edit to the Answers section and accepted it as suggested.

【问题讨论】:

如果你能把那个 edit 变成一个答案并接受它,那就太好了。 在为 UTF-8 配置的终端中打印 '\xe9'打印 é。它将打印一个替换字符(通常是一个问号),因为 \xe9 不是一个有效的 UTF-8 序列(它缺少两个应该跟在前导字节后面的字节)。它肯定会不会被解释为 Latin-1。 @MartijnPieters 我怀疑当我输出\xe9 以打印é 时,我指定终端设置为在ISO-8859-1 (latin1) 中解码的部分可能已经略过了。 啊,是的,我确实错过了那部分;终端的配置与外壳不同。检查。 我浏览了答案,但实际上,我有没有 u 前缀的字符串用于 python 2.7。为什么那个仍然作为unicode处理? (我的 sys.getdefaultencoding() 是 ascii) 【参考方案1】:

根据Python default/implicit string encodings and conversions:

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

【讨论】:

【参考方案2】:

当 Unicode 字符被打印到标准输出时,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】:

它对我有用:

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

【讨论】:

廉价肮脏的黑客,将不可避免地破坏其他东西。正确的方法并不难!【参考方案4】:

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

通过尝试打印 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 指向 ©。

【讨论】:

很好的解释。现在我理解了 UTF-8! 好的,我在大约 10 秒内阅读了您的整个帖子。它说,“Python 在编码方面很糟糕。” 很好的解释。你能解决this的问题吗?【参考方案5】:

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

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

>>> 

\xe9 的情况下,Python 会采用您的默认编码 (Ascii),因此打印...一些空白。

【讨论】:

所以如果我理解得很好,当我打印出 unicode 字符串(代码点)时,python 假设我想要一个以 utf-8 编码的输出,而不是仅仅试图给我它的内容 可以使用ASCII吗? @mike: AFAIK 你说的是对的。如果它确实打印出 Unicode 字符但编码为 ASCII,那么一切都会出现乱码,可能所有初学者都会问:“我怎么不能打印出 Unicode 文本?” 谢谢。我实际上是那些初学者之一,但来自对 unicode 有一定了解的人,这就是为什么这种行为让我有点失望。 R.,不正确,因为 '\xe9' 不在 ascii 字符集中。非 Unicode 字符串使用 sys.stdout.encoding 打印,Unicode 字符串在打印前编码为 sys.stdout.encoding。【参考方案6】:

Python REPL 尝试从您的环境中获取要使用的编码。如果它发现一些理智的东西,那么这一切都只是工作。当它无法弄清楚发生了什么时,它就会出错。

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

【讨论】:

只是出于好奇,如何将 sys.stdout.encoding 更改为 ascii? @TankorSmash 我在 2.7.2 收到 TypeError: readonly attribute

以上是关于当默认编码为 ASCII 时,为啥 Python 会打印 unicode 字符?的主要内容,如果未能解决你的问题,请参考以下文章

ascii 转换为 utf-8

设置utf-8为默认编码

python中reload(sys)作用

docx模塊的使用

Python 设置系统默认编码 转

python:'ascii' codec can't encode character