代码之家  ›  专栏  ›  技术社区  ›  Michael Ekoka

当默认编码为ASCII时,为什么python要打印Unicode字符?

  •  133
  • Michael Ekoka  · 技术社区  · 15 年前

    从python 2.6 shell:

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

    由于“_”)字符不是ASCII的一部分,而且我没有指定编码,所以我希望在print语句后出现一些乱码或错误。我想我不明白默认的ASCII编码意味着什么。

    编辑

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

    6 回复  |  直到 6 年前
        1
  •  98
  •   Michael Ekoka    11 年前

    多亏了各种各样的回复,我想我们可以编一个解释。

    通过尝试打印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输出字符串之后会发生什么。为此,我们首先在图形终端内启动bash shell(我使用gnome终端),然后将终端设置为使用iso-8859-1(即拉丁语-1)解码输出(图形终端通常可以选择 设置字符编码 在其中一个下拉菜单中)。注意,这不会改变 壳牌环境 编码,它只改变了 终端 它本身将解码给定的输出,有点像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按原样输出二进制字符串,终端接收该字符串,并尝试将其值与拉丁-1字符映射匹配。在拉丁语-1中,0xe9或233生成字符“_”),因此终端显示的就是这个字符。

    (2)python试图 含蓄地 使用sys.stdout.encoding中当前设置的任何方案对unicode字符串进行编码,在本例中是“utf-8”。在UTF-8编码之后,生成的二进制字符串是'\xc3\xa9'(见后面的解释)。终端接收流,并尝试使用拉丁语-1解码0xc3a9,但拉丁语-1从0变为255,因此一次只能解码流1字节。0xc3a9是2个字节长,因此拉丁语1解码器将其解释为0xc3(195)和0xa9(169),并生成2个字符:和。

    (3)python使用拉丁-1方案对unicode码位u'\xe9'(233)进行编码。结果发现,拉丁-1代码点的范围是0-255,并且在该范围内指向与Unicode完全相同的字符。因此,当用拉丁文-1编码时,该范围内的Unicode代码点将产生相同的值。因此,用拉丁语1编码的u''xe9'(233)也将生成二进制字符串''xe9'。终端接收该值并尝试在拉丁-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用拉丁文-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,与拉丁语1和Unicode从0到255相同。也就是说,在ASCII、拉丁文-1和Unicode中,0x41指向“A”,在拉丁文-1和Unicode中,0xC8指向“”,在拉丁文-1和Unicode中,0xE9指向“”。

    使用电子设备时,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,仍然需要2个字节的UTF-16存储)。UTF-32更加浪费,因为它将所有字符存储在4个字节中。

    UTF-8恰好巧妙地解决了这一难题,它的方案能够用可变字节空间存储代码点。作为其编码策略的一部分,UTF-8使用标志位来标识(可能是解码器)它们的空间需求和边界。

    ASCII范围(0-127)中Unicode码位的UTF-8编码:

    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(非ASCII)以上的Unicode码位的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指向。

        2
  •  25
  •   Mark Tolonen    7 年前

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

        3
  •  8
  •   Ignacio Vazquez-Abrams    15 年前

    python repl尝试从您的环境中获取要使用的编码。如果它发现什么东西是正常的,那么一切都是正常的。当它不知道发生了什么事情的时候,它就会崩溃。

    >>> print sys.stdout.encoding
    UTF-8
    
        4
  •  4
  •   Mark Rushakoff    15 年前

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

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

    如果是 \xe9 然后,python采用默认编码(ascii),从而打印…空白的东西。

        5
  •  0
  •   ivan_pozdeev RenanSS    7 年前

    Python default/implicit string encodings and conversions :

    • 什么时候? print 惯性导航与制导 unicode 它是 encode 与D <file>.encoding
      • encoding 未设置,则 Unicode码 被隐式转换为 str (因为它的编解码器是 sys.getdefaultencoding() ,即 ascii ,任何民族特征都会导致 UnicodeEncodeError )
      • 对于标准流, 编码 是从环境推断出来的。它是典型的设置FOT tty 流(来自终端的区域设置),但可能不会为管道设置
        • 所以A print u'\xe9' 当输出到终端时很可能成功,如果重新定向则失败。解决办法是 encode() 之前具有所需编码的字符串 打印 惯性导航与制导。
    • 什么时候? 打印 惯性导航与制导 STR ,字节按原样发送到流。终端显示的标志符号将取决于其区域设置。
        6
  •  -1
  •   user3611630    10 年前

    它对我有用:

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