重定向到文件时的UnicodeDecodeError

zed*_*doo 99 python unicode

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

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni
Run Code Online (Sandbox Code Playgroud)

没有重定向它会打印垃圾.通过重定向,我得到了一个UnicodeDecodeError.有人可以解释为什么我只在第二种情况下得到错误,或者甚至更好地详细解释两种情况下幕后发生的情况?

Eri*_*got 249

整个键到这样的编码的问题是要明白,有在原理"串"的两个截然不同的概念:(1)的字符串的字符,和(2)的串/数组字节.这种区别在很长一段时间内都被忽略了,因为编码的历史无处不在,不超过256个字符(ASCII,Latin-1,Windows-1252,Mac OS Roman,......):这些编码将一组常见字符映射到0到255之间的数字(即字节); 在网络出现之前相对有限的文件交换使得这种不兼容的编码情况可以容忍,因为大多数程序可以忽略这样一个事实,即只要它们产生了保留在同一操作系统上的文本,就会有多种编码:这样的程序只会简单将文本视为字节(通过操作系统使用的编码).正确的现代视图根据以下两点正确地分离了这两个字符串概念:

  1. 角色大多与计算机无关:人们可以在粉笔板等上绘制它们,例如بايثون,中蟒和.机器的"字符"还包括"绘图指令",例如空格,回车,设置书写方向的指令(用于阿拉伯语等),重音符号等.Unicode标准中包含非常大的字符列表.它涵盖了大多数已知角色.

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

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

编码:字符→字节

解码:字节→字符

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

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

现在,我上面称之为"字符"的是Unicode所谓的" 用户感知角色 ".单个用户感知的字符有时可以通过组合Unicode列表中不同索引处的字符部分(基本字符,重音符号......)来表示,这些字符部分称为" 代码点 " - 这些代码点可以组合在一起形成一个"字形集群".因此,Unicode导致字符串的第三个概念,由一系列Unicode代码点组成,位于字节和字符串之间,并且更接近后者.我将它们称为" Unicode字符串 "(如Python 2中所示).

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

这有一个重要的结果:Python(Unicode)字符串的长度是它的代码点数,它并不总是用户感知字符的数量:因此s = "\u1100\u1161\u11a8"; print(s, "len", len(s))(Python 3)? len 3尽管s只有一个用户感知(韩语)字符(因为它用3个代码点表示 - 即使它没有,如图print("\uac01")所示).但是,在许多实际情况中,字符串的长度是用户感知字符的数量,因为许多字符通常由Python存储为单个Unicode代码点.

Python 2中,Unicode字符串被称为"Unicode字符串"(unicode类型,文字形式u"…"),而字节数组是"字符串"(str类型,其中字节数组可以例如用字符串文字构造"…").在Python 3中,Unicode字符串简称为"字符串"(str类型,文字形式"…"),而字节数组则是"字节"(bytes类型,文字形式b"…").

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


通常,当您打印 u"…" 到终端时,您不应该得到垃圾:Python知道终端的编码.实际上,您可以检查终端期望的编码:

% 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
Run Code Online (Sandbox Code Playgroud)

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

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

但是,当您重定向或管道程序的输出时,通常无法知道接收程序的输入编码是什么,并且上面的代码返回一些默认编码:无(Python 2.7)或UTF-8( Python 3):

% python2.7 -c "import sys; print sys.stdout.encoding" | cat
None
% python3.4 -c "import sys; print(sys.stdout.encoding)" | cat
UTF-8
Run Code Online (Sandbox Code Playgroud)

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

% PYTHONIOENCODING=UTF-8 python2.7 -c "import sys; print sys.stdout.encoding" | cat
UTF-8
Run Code Online (Sandbox Code Playgroud)

如果打印到终端没有达到预期效果,可以检查手动输入的UTF-8编码是否正确; 例如,如果我没有弄错的话,你的第一个字符(\u001A)是不可打印的.

http://wiki.python.org/moin/PrintFails上,您可以找到适合Python 2.x的解决方案,如下所示:

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
Run Code Online (Sandbox Code Playgroud)

对于Python 3,您可以检查之前在StackOverflow上提出的问题之一.

  • 我很高兴得到了帮助,@ m01!写这个答案的动机之一就是网上有很多关于Unicode和Python的页面,但我发现虽然很有趣,但他们从未完全允许我解决具体的编码问题...我真的相信通过记住在这个答案中找到的原则*并且在解决具体的编码问题时花时间*使用它们有很大帮助. (3认同)
  • 这是unicode和python的最佳解释.应该用这个替换Python Unicode HOWTO. (3认同)
  • @singularity:谢谢!我为Python 3添加了一些信息. (2认同)
  • 谢谢你,兄弟!我需要这么长时间的解释...很可惜我只给你一个upvote. (2认同)

Mar*_*nen 20

Python在写入终端,文件,管道等时总是编码Unicode字符串.当写入终端时,Python通常可以确定终端的编码并正确使用它.在写入文件或管道时,Python默认为'ascii'编码,除非另有明确说明.可以告诉Python在通过PYTHONIOENCODING环境变量管道输出时该怎么做.shell可以在将Python输出重定向到文件或管道之前设置此变量,因此已知正确的编码.

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

例1

请注意,#coding注释指示保存源文件的编码.我选择了utf8所以我可以支持我的终端无法支持的源代码.编码重定向到stderr,以便在重定向到文件时可以看到.

#coding: utf8
import sys
uni = u'?ß????µ???????'
print >>sys.stderr,sys.stdout.encoding
print uni
Run Code Online (Sandbox Code Playgroud)

输出(直接从终端运行)

cp437
?ß????µ???????
Run Code Online (Sandbox Code Playgroud)

Python正确地确定了终端的编码.

输出(重定向到文件)

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)
Run Code Online (Sandbox Code Playgroud)

Python无法确定编码(无),因此使用'ascii'默认值.ASCII仅支持转换Unicode的前128个字符.

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

cp437
Run Code Online (Sandbox Code Playgroud)

我的输出文件是正确的:

C:\>type out.txt
?ß????µ???????
Run Code Online (Sandbox Code Playgroud)

例2

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

#coding: utf8
import sys
uni = u'?ß????µ????????' # added Chinese character at end.
print >>sys.stderr,sys.stdout.encoding
print uni
Run Code Online (Sandbox Code Playgroud)

输出(直接从终端运行)

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>
Run Code Online (Sandbox Code Playgroud)

我的终端不明白最后一个汉字.

输出(直接运行,PYTHONIOENCODING = 437:替换)

cp437
?ß????µ????????
Run Code Online (Sandbox Code Playgroud)

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

  • 是的,Python 3默认为'utf8',但根据OP的样本,他使用的是Python 2.X,默认为'ascii'. (2认同)

ism*_*ail 12

打印时对其进行编码

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni.encode("utf-8")
Run Code Online (Sandbox Code Playgroud)

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

  • 它仍然没有回答WTH正在进行的问题.为什么,它只是在重定向时决定编码,当它应该对进程完全透明时. (4认同)
  • Python可以检查输出是否是终端,如果输出到管道,那么终端类型将是"哑".我想"哑"应该告诉你为什么Python在这种情况下不会尝试自动做任何事情,它可能会失败. (4认同)