ChatGPT解决这个技术问题 Extra ChatGPT

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

从 Python 2.6 外壳:

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

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

编辑

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

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

M
Michael Ekoka

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

通过尝试打印 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(就像您更改 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 字符串。 - Python 从 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 范围 (0-127) 中 unicode 代码点的 UTF-8 编码:

0xxx xxxx  (in binary)

显示为在编码期间“存储”代码点而保留的实际空间

前导 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 代码点的 UTF-8 编码(非 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 指向 ©。


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

当 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 警告,如果字体支持,则会显示正确的字符。即使字体不支持它,字符仍然可以从终端剪切并粘贴到具有支持字体的应用程序中,并且它是正确的。升级!


I
Ignacio Vazquez-Abrams

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

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

只是出于好奇,我将如何将 sys.stdout.encoding 更改为 ascii?
@TankorSmash 我在 2.7.2 上获得 TypeError: readonly attribute
M
Mark Rushakoff

通过输入明确的 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。
u
user3611630

这个对我有用:

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

廉价肮脏的黑客将不可避免地破坏其他东西。正确的方法并不难!
i
ivan_pozdeev

根据 Python default/implicit string encodings and conversions

打印 unicode 时,它使用 .encoding 进行编码。当未设置编码时,unicode 被隐式转换为 str(因为它的编解码器是 sys.getdefaultencoding(),即 ascii,任何国家字符都会导致 UnicodeEncodeError),对于标准流,编码是从环境中推断出来的。它通常设置 fot tty 流(来自终端的语言环境设置),但可能不会为管道设置,因此 print u'\xe9' 在输出到终端时可能会成功,如果它被重定向则失败。一种解决方案是在打印之前使用所需的编码对字符串进行编码()。

当未设置编码时,unicode 被隐式转换为 str(因为它的编解码器是 sys.getdefaultencoding(),即 ascii,任何国家字符都会导致 UnicodeEncodeError)

对于标准流,编码是从环境中推断出来的。它通常设置 fot tty 流(来自终端的语言环境设置),但可能不会为管道设置,因此 print u'\xe9' 在输出到终端时可能会成功,如果它被重定向则失败。一种解决方案是在打印之前使用所需的编码对字符串进行编码()。

因此 print u'\xe9' 在输出到终端时可能会成功,如果被重定向则可能会失败。一种解决方案是在打印之前使用所需的编码对字符串进行编码()。

打印 str 时,字节按原样发送到流中。终端显示的字形将取决于其区域设置。