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

2025-02-27 09:05:00
admin
原创
52
摘要:问题描述:从 Python 2.6 shell 中:>>> import sys >>> print sys.getdefaultencoding() ascii >>> print u'xe9' é >>> 我预计打印语句后会出现一些...

问题描述:

从 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'

>>> 

在这种情况下,xe9Python 会假定您的默认编码(Ascii),因此打印...一些空白内容。

解决方案 5:

它对我有用:

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

解决方案 6:

按照Python 默认/隐式字符串编码和转换:

  • printing时unicode,它是encoded 的<file>.encoding

+ 当`encoding`未设置时,`unicode`会隐式转换为`str`(因为 的编解码器是`sys.getdefaultencoding()`,即`ascii`,任何国家字符都会导致`UnicodeEncodeError`)
+ 对于标准流,`encoding`是从环境中推断出来的。它通常针对`tty`流进行设置(来自终端的区域设置),但可能不会针对管道进行设置


    - `print u'xe9'`因此,当输出到终端时,a很可能会成功,如果重定向,则可能会失败。一种解决方案是在 ing`encode()`之前使用所需的编码对字符串进行编码`print`。
  • printing时str,字节将按原样发送到流。终端显示的字形将取决于其语言环境设置。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   3407  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   2228  
  本文介绍了以下10款项目管理软件工具:禅道项目管理软件、Airtable、Paymo、Backlog、Basecamp、LiquidPlanner、MeisterTask、ProofHub、Forecast、Planview。在当今竞争激烈的商业环境中,企业面临着越来越多的项目管理挑战。无论是初创公司还是大型企业,如何...
项目管理软件   1  
  本文介绍了以下10款项目管理软件工具:禅道项目管理软件、ClickUp、Bonsai、Planview、Nifty、Filestage、Backlog、Plutio、Chanty、Smartsheet。在当今快节奏的商业环境中,企业面临着日益复杂的项目管理挑战。无论是跨部门协作、资源分配,还是进度跟踪,传统的管理方式已...
测试管理软件   0  
  本文介绍了以下10款项目管理软件工具:禅道项目管理软件、BubblePPM、Xebrio、Monday、Nifty、Freshdesk、Hubstaff、ProofHub、ClickUp、Wekan。在当今快节奏的商业环境中,项目管理已成为企业成功的关键因素之一。然而,许多企业在选择项目管理工具时常常面临困惑:如何找到...
测试管理工具   1  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用