重定向到文件时出现UnicodeDecodeError


100

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

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

如果没有重定向,它将打印垃圾。通过重定向,我得到了UnicodeDecodeError。有人可以解释为什么仅在第二种情况下才得到错误,或者更好地给出两种情况下幕后情况的详细解释吗?


这个答案也可能有帮助。
tzot 2011年

当我尝试复制您的发现时,出现UnicodeEncodeError而不是UnicodeDecodeError。gist.github.com/jaraco/12abfc05872c65a4f3f6cd58b6f9be4d
Jason R. Coombs

Answers:


252

整个键到这样的编码的问题是要明白,有在原理上的“串”两个截然不同的概念:(1)的字符串的字符,和(2)串/数组字节。由于不超过256个字符(ASCII,Latin-1,Windows-1252,Mac OS Roman等)的不超过256个字符的历史悠久的编码普遍存在,这种区分已被长期忽视。 0到255之间的数字(即字节);在网络问世之前,相对有限的文件交换使得这种不兼容的编码的情况是可以容忍的,因为大多数程序可以忽略存在多种编码的事实,只要它们产生的文本仍保留在同一操作系统上即可:将文本视为字节(通过操作系统使用的编码)。正确的现代视图基于以下两点正确地将这两个字符串概念分开:

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

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

总而言之,计算机需要在内部用byte表示字符,它们通过两个操作来做到这一点:

编码:字符→字节

解码:字节→字符

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

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

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

尽管Python可以打印(用户可感知)字符的字符串,但Python非字节字符串本质上是Unicode代码点的序列,而不是用户可感知字符的序列。代码点值是在Python \u和中使用的值\U Unicode字符串语法中。不应将它们与字符的编码混淆(也不必与它有任何关系:Unicode代码点可以通过各种方式进行编码)。

这有一个重要的结果:Python(Unicode)字符串的长度是其代码点的数量,并不总是其用户可感知的字符的数量:因此s = "\u1100\u1161\u11a8"; print(s, "len", len(s))각 len 3尽管s只有一个用户可感知的(韩语),(Python 3)却给出了字符(因为它用3个代码点表示-即使不是必须的,例如print("\uac01")所示)。但是,在许多实际情况下,字符串的长度就是用户可感知的字符数,因为Python通常将许多字符存储为单个Unicode代码点。

Python 2中,Unicode字符串称为…“ Unicode字符串”(unicode类型,文字形式u"…"),而字节数组是“ strings”(str类型,其中字节数组可以例如由字符串文字构造"…")。在Python 3中,Unicode字符串简称为“字符串”(str类型,文字形式"…"),而字节数组则是“字节”(bytes类型,文字形式b"…")。结果,类似的东西"🐍"[0]在Python 2('\xf0'一个字节)和Python 3("🐍"第一个也是唯一的字符)中给出了不同的结果。

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


通常,在终端上打印 时,不会出现垃圾:Python知道终端的编码。实际上,您可以检查终端期望的编码方式:u"…"

% 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会抱怨(在Python中带有,UnicodeEncodeError因为无法以适合终端的方式对字符串进行编码)。唯一可能的解决方案是使用可以显示字符的终端(通过配置终端以使其接受可以代表您的字符的编码,或者使用其他终端程序)。当您分发可以在不同环境中使用的程序时,这一点很重要:您打印的消息应该可以在用户终端中表示。因此,有时最好坚持只包含ASCII字符的字符串。

但是,当您重定向或传递程序的输出时,通常无法知道接收程序的输入编码是什么,并且上面的代码返回一些默认编码:None(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

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

% 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的解决方案:

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上提出的问题之一


2
@singularity:谢谢!我加了一些信息为Python 3
埃里克ØLebigot

2
谢谢你,兄弟!我需要这么长时间的解释...很遗憾,我只能给你一个投票。
mik01aj 2012年

3
我很高兴获得帮助,@ m01!编写此答案的动机之一是,网络上有许多关于Unicode和Python的页面,但我发现尽管很有趣,但它们从未完全允许我解决具体的编码问题……我真正相信通过牢记该答案中找到的原理以及在解决具体编码问题时花点时间使用它们很有帮助。
Eric O Lebigot 2012年

3
这是有史以来对unicode和python最好的解释。Python Unicode HOWTO应该替换为此。
stantonk

1
在这里,让我在此黑板上绘制“从右到左的替代”字符…
icktoofay

20

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

在您的情况下,您已打印了终端不支持的4个不常见字符的字体。以下是一些示例,这些示例可以帮助解释该行为,以及我的终端实际使用的字符(使用cp437,而不是UTF-8)。

例子1

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

#coding: utf8
import sys
uni = u'αßΓπΣσµτΦΘΩδ∞φ'
print >>sys.stderr,sys.stdout.encoding
print uni

输出(直接从终端运行)

cp437
αßΓπΣσµτΦΘΩδ∞φ

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)

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

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

cp437

并且我的输出文件是正确的:

C:\>type out.txt
αßΓπΣσµτΦΘΩδ∞φ

例子2

现在,我将在终端不支持的源代码中添加一个字符:

#coding: utf8
import sys
uni = u'αßΓπΣσµτΦΘΩδ∞φ马' # added Chinese character at end.
print >>sys.stderr,sys.stdout.encoding
print uni

输出(直接从终端运行)

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:替换)

cp437
αßΓπΣσµτΦΘΩδ∞φ?

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


并非完全正确的说法是“当写入文件或管道时,Python默认使用'ascii'编码,除非另有明确说明。”。实际上,Python 3在Mac OS X / Fink上使用UTF-8。
Eric O Lebigot 2011年

2
是的,Python 3默认为'utf8',但是根据OP的示例,他使用的是Python 2.X,默认为'ascii'。
Mark Tolonen 2011年

我无法通过操作获得正确的输出PYTHONIOENCODING。这样print string.encode("UTF-8")的建议通过@Ismail为我工作。
2012年

即使chcp代码页不支持中文,如果您的字体支持中文,您也可以看到中文。为避免这种情况UnicodeEncodeError: 'charmap',您可以安装win-unicode-console软件包。
jfs 2015年

我的问题是python-gitlab CLI在cmd中很好地打印了中文字符,但是这些字符在被重定向到文件后是垃圾。PYTHONIOENCODING=utf-8解决了问题。
ElpieKay

12

打印时进行编码

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

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


4
它仍然无法回答WTH正在此处发生的问题。为什么突然决定只在重定向时才编码,而这应该对过程完全透明。
马克西姆Sloyko

为什么在执行重定向时python不对它进行编码?python是否会明确检查并确定它将以不同的方式处理事情,只是很难?
Arafangion

1
python甚至有办法区分这两种情况?我想(直到现在...)它无法知道。
zedoo 2010年

4
Python可以检查输出是否为终端,如果将其输出到管道,则终端类型将为“哑”。我想“哑巴”应该告诉您为什么Python在这种情况下不尝试自动执行任何操作,否则可能会失败。
ismail 2010年

1
如果环境使用与utf-8不兼容的字符编码(例如,在Windows上很常见),它将产生mojibake。不要在脚本中对环境的字符编码进行硬编码。配置您的语言环境,或PYTHONIOENCODING,或安装win-unicode-console(Windows),或接受命令行参数(如果需要)。
jfs 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.