关于python:重定向到文件时的UnicodeDecodeError

UnicodeDecodeError when redirecting to file

我在Ubuntu终端(编码设置为utf-8)中运行了这段代码两次,一次使用./test.py,然后使用./test.py >out.txt

1
2
uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni

如果没有重定向,它将打印垃圾。通过重定向,我得到一个unicodedecode错误。有人能解释为什么只有在第二种情况下我才会出错,或者更好地详细解释在这两种情况下帘子后面发生了什么吗?


这种编码问题的关键是要理解原则上有两个不同的"字符串"概念:(1)字符串和(2)字符串/字节数组。这种区别在很长一段时间内都被忽略了,因为不超过256个字符(ascii,拉丁语-1,windows-1252,mac-os-roman,…)的编码在历史上无处不在:这些编码将一组通用字符映射到0到255之间的数字(即字节);在web mad出现之前,文件交换相对有限。e这种不兼容编码的情况是可以容忍的,因为大多数程序可以忽略这样一个事实,即只要它们生成的文本保持在同一操作系统上,就有多个编码:这样的程序只将文本视为字节(通过操作系统使用的编码)。基于以下两点,正确的现代观点正确地将这两个字符串概念分开:好的。

  • 字符大多与计算机无关:例如,人们可以在黑板上绘制它们,等等????????"机器的"字符"还包括"绘图指令",例如空格、回车、设置书写方向的指令(阿拉伯语等)、重音符号等。Unicode标准中包括一个非常大的字符列表;它涵盖了大多数已知字符。好的。

  • 另一方面,计算机确实需要以某种方式表示抽象字符:为此,它们使用字节数组(包括0到255之间的数字),因为它们的内存是字节块。将字符转换为字节的必要过程称为编码。因此,计算机需要编码来表示字符。您计算机上的任何文本都会被编码(直到显示出来),无论是发送到终端(需要以特定方式编码字符),还是保存到文件中。为了显示或正确地"理解"(例如,通过python解释器),字节流被解码为字符。一些编码(utf-8,utf-16,…)是由Unicode为其字符列表定义的(Unicode因此定义了字符列表和这些字符的编码在某些地方仍然可以看到表达式"Unicode编码"是指无处不在的utf-8,但这是不正确的术语,因为Unicode提供了多个编码)。好的。

  • 总之,计算机需要在内部用字节表示字符,它们通过两个操作来实现这一点:好的。

    Encoding: characters → bytes

    Ok.

    Decoding: bytes → characters

    Ok.

    某些编码不能对所有字符(例如,ASCII)进行编码,而(某些)Unicode编码允许您对所有Unicode字符进行编码。编码也不一定是唯一的,因为有些字符可以直接表示,也可以组合表示(例如,基字符和重音)。好的。

    注意,换行的概念增加了一层复杂性,因为它可以由依赖于操作系统的不同(控制)字符表示(这是Python通用换行文件读取模式的原因)。好的。

    现在,我上面所说的"字符"就是Unicode所说的"用户感知字符"。单用户感知的字符有时可以用Unicode表示,通过组合Unicode列表中不同索引处的字符部分(基本字符、重音符号等),这些代码点被称为"代码点"——这些代码点可以组合在一起形成"图形簇"。因此,Unicode导致了字符串的第三个概念,由一系列Unicode代码点组成,这些代码点位于字节和字符串之间,并且更接近后者。我将称它们为"unicode字符串"(就像在python中一样)。好的。

    虽然python可以打印(用户感知的)字符字符串,但python非字节字符串本质上是Unicode代码点的序列,而不是用户感知的字符。代码点值是python的\u\uunicode字符串语法中使用的值。它们不应该与字符的编码混淆(也不必与字符有任何关系:Unicode代码点可以以各种方式编码)。好的。

    这有一个重要的后果:python(unicode)字符串的长度是它的代码点数量,而不是它的用户感知字符数量:因此,尽管s有一个单用户感知(韩文)字符,但EDOCX1(python 3)给出? len 3,因为它用3个代码点表示,即使正如print("\uac01")所示,它不必如此。但是,在许多实际情况下,字符串的长度是用户感知的字符数,因为许多字符通常由Python存储为单个Unicode代码点。好的。

    在python 2中,unicode字符串称为"unicode字符串"(unicode类型,文字形式为u"…"),而字节数组称为"字符串"(str类型,其中字节数组可以用字符串文字"…"构造)。在python 3中,unicode字符串简单地称为"字符串"(str类型,文字形式"…"),而字节数组则称为"字节"(bytes类型,文字形式b"…")。好的。

    有了这几个关键点,您应该能够理解大多数与编码相关的问题!好的。

    通常,当您将u"…"打印到终端时,不应该得到垃圾:python知道终端的编码。实际上,您可以检查终端需要什么编码:好的。

    1
    2
    3
    4
    5
    6
    7
    % python
    Python 2.7.6 (default, Nov 15 2013, 15:20:37)
    [GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.2.79)] on darwin
    Type"help","copyright","credits" or"license" for more information.
    >>> import sys
    >>> print sys.stdout.encoding
    UTF-8

    如果您的输入字符可以使用终端的编码进行编码,那么Python将这样做,并将相应的字节发送到您的终端,而不会抱怨。然后,终端将尽最大努力在解码输入字节后显示字符(最坏情况下,终端字体没有某些字符,而是打印某种空白)。好的。

    如果您的输入字符不能使用终端的编码进行编码,则意味着终端没有配置为显示这些字符。python会抱怨(在使用UnicodeEncodeError的python中,因为字符串的编码方式不能适合您的终端)。唯一可能的解决方案是使用可以显示字符的终端(通过配置终端使其接受可以表示字符的编码,或者使用其他终端程序)。当您分发可在不同环境中使用的程序时,这一点很重要:您打印的消息应该可以在用户终端中表示。因此,有时最好坚持使用只包含ASCII字符的字符串。好的。

    但是,当您重定向或传输程序的输出时,通常不可能知道接收程序的输入编码是什么,上面的代码返回一些默认编码:none(python 2.7)或utf-8(python 3):好的。

    1
    2
    3
    4
    % python2.7 -c"import sys; print sys.stdout.encoding" | cat
    None
    % python3.4 -c"import sys; print(sys.stdout.encoding)" | cat
    UTF-8

    但是,如果需要,可以通过PYTHONIOENCODING环境变量设置stdin、stdout和stderr的编码:好的。

    1
    2
    % PYTHONIOENCODING=UTF-8 python2.7 -c"import sys; print sys.stdout.encoding" | cat
    UTF-8

    如果打印到终端不能产生您期望的结果,您可以检查手动输入的UTF-8编码是否正确;例如,如果我没有弄错,您的第一个字符(\u001A是不可打印的。好的。

    在http://wiki.python.org/moin/printfails,您可以找到一个类似于下面的解决方案,用于python 2.x:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import codecs
    import locale
    import sys

    # Wrap sys.stdout into a StreamWriter to allow writing unicode.
    sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)

    uni = u"\u001A\u0BC3\u1451\U0001D10C"
    print uni

    对于python 3,您可以检查前面在stackoverflow中提出的问题之一。好的。好啊。


    当写入终端、文件、管道等时,python总是对unicode字符串进行编码。当写入终端时,python通常可以确定终端的编码并正确使用它。当写入文件或管道时,除非另有明确说明,否则python默认为"ascii"编码。当输出通过PYTHONIOENCODING环境变量进行管道传输时,可以告诉python应该做什么。shell可以在将python输出重定向到文件或管道之前设置此变量,以便知道正确的编码。

    在您的情况下,您已经打印了4个不常见的字符,而您的终端在其字体中不支持这些字符。下面是一些例子来帮助解释这种行为,其中包含我的终端实际支持的字符(它使用CP437,而不是UTF-8)。

    例1

    注意,#coding注释表示保存源文件的编码。我选择了utf8,这样我就可以支持源代码中终端不能支持的字符。编码重定向到stderr,以便在重定向到文件时可以看到。

    1
    2
    3
    4
    5
    #coding: utf8
    import sys
    uni = u'α?ΓπΣσμτΦΘΩδ∞φ'
    print >>sys.stderr,sys.stdout.encoding
    print uni

    输出(直接从终端运行)

    1
    2
    cp437
    α?ΓπΣσμτΦΘΩδ∞φ

    python正确地确定了终端的编码。

    输出(重定向到文件)

    1
    2
    3
    4
    5
    None
    Traceback (most recent call last):
      File"C:\ex.py", line 5, in <module>
        print uni
    UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-13: ordinal not in range(128)

    python无法确定编码(无),因此使用了"ascii"默认值。ASCII只支持转换Unicode的前128个字符。

    输出(重定向到文件,pythonioencoding=cp437)

    1
    cp437

    我的输出文件是正确的:

    1
    2
    C:\>type out.txt
    α?ΓπΣσμτΦΘΩδ∞φ

    例2

    现在,我将在源中插入一个终端不支持的字符:

    1
    2
    3
    4
    5
    #coding: utf8
    import sys
    uni = u'α?ΓπΣσμτΦΘΩδ∞φ马' # added Chinese character at end.
    print >>sys.stderr,sys.stdout.encoding
    print uni

    输出(直接从终端运行)

    1
    2
    3
    4
    5
    6
    7
    cp437
    Traceback (most recent call last):
      File"C:\ex.py", line 5, in <module>
        print uni
      File"C:\Python26\lib\encodings\cp437.py", line 12, in encode
        return codecs.charmap_encode(input,errors,encoding_map)
    UnicodeEncodeError: 'charmap' codec can't encode character u'\u9a6c' in position 14: character maps to <undefined>

    我的终端不懂最后一个汉字。

    输出(直接运行,pythonioencoding=437:replace)

    1
    2
    cp437
    α?ΓπΣσμτΦΘΩδ∞φ?

    可以使用编码指定错误处理程序。在这种情况下,未知字符被?替换。ignorexmlcharrefreplace是其他一些选项。使用支持所有Unicode字符编码的utf8时,将永远不会进行替换,但用于显示字符的字体必须仍然支持这些字符。


    打印时对其进行编码

    1
    2
    uni = u"\u001A\u0BC3\u1451\U0001D10C"
    print uni.encode("utf-8")

    这是因为当您手动运行脚本时,python会在将其输出到终端之前对其进行编码,当您通过管道传输时,python不会对其本身进行编码,因此在执行I/O时必须手动进行编码。