在Python中通过管道输出stdout时设置正确的编码

373

当将Python程序的输出导入管道时,Python解释器会对编码感到困惑并将其设置为None。这意味着像这样的程序:

# -*- coding: utf-8 -*-
print u"åäö"

在正常运行时,这段代码可以正常工作,但在管道序列中使用时会失败并报错:

UnicodeEncodeError: 'ascii' codec can't encode character u'\xa0' in position 0: ordinal not in range(128)

在管道传输时,最好的方法是什么?我可以告诉它使用shell/文件系统/其他程序正在使用的任何编码吗?

到目前为止,我看到的建议是直接修改site.py文件或使用以下hack方法来硬编码defaultencoding:

# -*- coding: utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
print u"åäö"

有没有更好的方法使管道工作?

1
请参见 https://dev59.com/3W855IYBdhLWcg3wFAHy - ShreevatsaR
3
如果你在Windows上遇到这个问题,你也可以在执行脚本之前运行 chcp 65001。这可能会有一些问题,但通常有帮助作用,并且不需要输入太多文字(少于 set PYTHONIOENCODING=utf_8)。 - Tomasz Gandor
chcp命令与设置PYTHONIOENCODING不同。我认为chcp只是终端本身的配置,与写入文件无关(当您将stdout管道传输时)。如果您想要保存输入,请尝试使用“setx PYTHONENCODING utf-8”使其永久化。 - ejm
https://dev59.com/5qnka4cB1Zd3GeqPHwpm - bkrishna2006
我遇到了一个有点相关的问题,并在这里找到了解决方案 --> https://dev59.com/5qnka4cB1Zd3GeqPHwpm - bkrishna2006
@Tomasz,太棒了!你的环境变量是最简单、最好的解决方法,可以克服这个烦人的问题! - Apostolos
12个回答

170

当你在脚本中运行代码时,它能够正常工作是因为Python将输出编码成与您的终端应用程序使用的编码相同的格式。如果您正在进行管道传输,则必须自行进行编码。

一个经验法则是:始终在内部使用Unicode。解码接收到的内容,编码发送的内容。

# -*- coding: utf-8 -*-
print u"åäö".encode('utf-8')

另一个示范性的例子是一个Python程序,用于在ISO-8859-1和UTF-8之间进行转换,并将中间的所有内容变成大写。

import sys
for line in sys.stdin:
    # Decode what you receive:
    line = line.decode('iso8859-1')

    # Work with Unicode internally:
    line = line.upper()

    # Encode what you send:
    line = line.encode('utf-8')
    sys.stdout.write(line)

将系统默认编码设置为非 ASCII 编码是不好的做法,因为您使用的某些模块和库可能依赖于其为 ASCII 编码的事实。请勿这样做。


11
问题在于用户不想明确指定编码方式,他只想在IO操作中使用Unicode。而且他使用的编码方式应该是在语言环境设置中指定的编码方式,而不是终端应用程序设置中指定的编码方式。据我所知,在这种情况下Python 3会使用“locale”编码方式。更改sys.stdout似乎是一种更为愉悦的方法。 - Andrey Vlasovskikh
69
这个答案是错误的。你不应该在程序的每个输入和输出上手动转换;那很容易出问题,而且难以维护。 - Glenn Maynard
35
@Glenn Maynard:那么您认为正确答案是什么?告诉我们会更有帮助,而不仅仅是说“这个答案是错误的”。 - smci
15
如果在Python 2中重定向脚本的标准输出,不要修改脚本,请设置 PYTHONIOENCODING - jfs
6
实际上,解码和编码是一个好的做法,来自于Python文档:"软件应该只在内部使用Unicode字符串,在最早的时候将输入数据解码,只在最后编码输出。" - HenriTel
显示剩余4条评论

168

首先,关于这个解决方案:

# -*- coding: utf-8 -*-
print u"åäö".encode('utf-8')

每次都显式地使用给定编码打印输出并不切实际,这样做会重复而容易出错。

更好的解决方案是在程序开始时更改sys.stdout ,以使用所选编码进行编码。下面是我在Python: How is sys.stdout.encoding chosen?上发现的一个解决方案,特别是由“toka”发表的评论:

import sys
import codecs
sys.stdout = codecs.getwriter('utf8')(sys.stdout)

8
很不幸,将sys.stdout改为仅接受Unicode会破坏许多期望它接受编码字节串的库。 - nosklo
6
当输出为终端时,它如何能够可靠地自动工作? - Rasmus Kaj
3
@Rasmus Kaj:只需定义您自己的Unicode打印函数,并在每次想要打印Unicode时使用它:def myprint(unicodeobj): print unicodeobj.encode('utf-8')-- 您可以通过检查 sys.stdout.encoding 来自动检测终端编码,但您应该考虑当其为 None 时的情况(即将输出重定向到文件时),因此您仍需要一个单独的函数。 - nosklo
3
这并不意味着sys.stdout只接受Unicode。您可以将str和unicode都传递给StreamWriter。 - Glenn Maynard
它会破坏pdb或者像@JohnChain所说的IPython的任何读取行能力。 - vaab
11
我假设这个答案是针对Python2的。在既要支持Python2又要支持Python3的代码中,要小心使用此方法。对我来说,在Python3下运行时会出现问题。 - wim

141

您可能想尝试将环境变量"PYTHONIOENCODING"更改为"utf_8"。我写了一篇关于这个问题的博客

博客摘要:

import sys, locale, os
print(sys.stdout.encoding)
print(sys.stdout.isatty())
print(locale.getpreferredencoding())
print(sys.getfilesystemencoding())
print(os.environ["PYTHONIOENCODING"])
print(chr(246), chr(9786), chr(9787))

提供给你

utf_8
False
ANSI_X3.4-1968
ascii
utf_8
ö ☺ ☻

3
更改sys.stdout.encoding可能不起作用,但更改sys.stdout会起作用:sys.stdout = codecs.getwriter(encoding)(sys.stdout)。可以在Python程序内部完成此操作,因此用户不需要设置环境变量。 - blueFast
7
"PYTHONIOENCODING" 是有效的。字节如何被解释为文本是由用户环境定义的。你的脚本不应该假定和指导用户环境使用什么字符编码。如果Python没有自动识别设置,那么可以为你的脚本设置 "PYTHONIOENCODING"。除非输出被重定向到文件/管道,否则你不需要它。 - jfs
10
老实说,我认为这是Python的一个缺陷。当我重定向输出时,我希望将那些本应出现在终端上的字节写入文件中。也许这不适用于每个人,但它应该是一个不错的默认设置。做一些通常应该“只是工作”的微不足道的操作时,突然崩溃并没有任何解释是一个糟糕的默认设置。 - SnakE
@SnakE:我能想到的唯一合理解释是,Python实现在启动时故意强制选择stdout编码,可能是为了防止以后出现任何糟糕编码的东西。或者更改它只是一个未实现的功能,如果允许用户稍后更改它,那么这将是一个合理的Python功能请求。 - daveagp
3
我的观点是:我的程序的行为不应该取决于它是否被重定向——除非我真的想要这样做,在这种情况下,我会自己实现。Python 的行为与我使用其他控制台工具的经验相反。这违反了最小惊讶原则。除非有非常强有力的理由,否则我认为这是一个设计缺陷。 - SnakE
显示剩余2条评论

64
export PYTHONIOENCODING=utf-8

可以执行任务,但无法在Python本身上设置...

我们能做的是验证它是否没有设置并告诉用户在调用脚本之前进行设置:

if __name__ == '__main__':
    if (sys.stdout.encoding is None):
        print >> sys.stderr, "please set python env PYTHONIOENCODING=UTF-8, example: export PYTHONIOENCODING=UTF-8, when write to stdout."
        exit(1)

更新以回复评论:问题仅存在于将输出重定向到stdout时。我在Fedora 25 Python 2.7.13中进行了测试。

python --version
Python 2.7.13

执行 cat b.py 命令

#!/usr/bin/env python
#-*- coding: utf-8 -*-
import sys

print sys.stdout.encoding

运行 ./b.py

UTF-8

运行 ./b.py | less

None

2
那个检查在Python 2.7.13中不起作用。sys.stdout.encoding是根据LC_CTYPE区域设置值自动设置的。 - amphetamachine
1
https://mail.python.org/pipermail/python-list/2011-June/605938.html 上的例子仍然有效,即当您使用 ./a.py > out.txt 时,sys.stdout.encoding为None。 - Sérgio
我曾经遇到过与Backblaze B2同步脚本相关的问题,而设置PYTHONIOENCODING=utf-8解决了我的问题。我的系统是Debian Stretch上的Python 2.7。 - 0x3333

7

我很惊讶这个答案还没有被发布在这里

Since Python 3.7 you can change the encoding of standard streams with reconfigure():

sys.stdout.reconfigure(encoding='utf-8')

You can also modify how encoding errors are handled by adding an errors parameter.

https://dev59.com/hW855IYBdhLWcg3wYjOF#52372390


6
自 Python 3.7 开始,我们可以使用 Python UTF-8 模式,通过使用命令行选项 -X utf8:
 python -X utf8 testzh.py

脚本testzh.py包含

print("Content-type: text/html; charset=UTF-8\n") 
print("地球你好!")

要将Windows 10 Internet Service IIS设置为CGI脚本处理程序,

我们将可执行文件设置为:

"C:\Program Files\Python39\python.exe" -X utf8 %s

enter image description here

这在 Microsoft Edge 浏览器上对于中文汉字的表现符合预期,如屏幕截图所示。否则,会出现错误。

enter image description here

请查看https://docs.python.org/3/library/os.html#utf8-mode

5

上周我遇到了类似的问题。在我的IDE(PyCharm)中很容易解决。

以下是我的解决方法:

从PyCharm菜单栏开始:文件->设置...->编辑器->文件编码,然后将"IDE编码"、"项目编码"和"属性文件的默认编码"全部设为UTF-8,现在她就像魔法般工作了。

希望这能帮到你!


4
一个可能经过修正的 Craig McQueen 回答的版本。
import sys, codecs
class EncodedOut:
    def __init__(self, enc):
        self.enc = enc
        self.stdout = sys.stdout
    def __enter__(self):
        if sys.stdout.encoding is None:
            w = codecs.getwriter(self.enc)
            sys.stdout = w(sys.stdout)
    def __exit__(self, exc_ty, exc_val, tb):
        sys.stdout = self.stdout

使用方法:

with EncodedOut('utf-8'):
    print u'ÅÄÖåäö'

3

我在一个遗留应用程序中遇到了这个问题,很难确定打印的内容在哪里。我使用了以下方法来解决:

# encoding_utf8.py
import codecs
import builtins


def print_utf8(text, **kwargs):
    print(str(text).encode('utf-8'), **kwargs)


def print_utf8(fn):
    def print_fn(*args, **kwargs):
        return fn(str(*args).encode('utf-8'), **kwargs)
    return print_fn


builtins.print = print_utf8(print)

在我的脚本test.py的顶部:
import encoding_utf8
string = 'Axwell Λ Ingrosso'
print(string)

请注意,这将更改所有对print的调用以使用编码,因此您的控制台将打印此内容:
$ python test.py
b'Axwell \xce\x9b Ingrosso'

3
我想在这里提一下一些东西,之前我花了很长时间进行实验,最后才意识到发生了什么。这对于这里的每个人来说可能是如此明显,以至于他们没有提及它。但如果他们这样做了,那会帮助我,所以根据这个原则...!
注意:我专门使用 Jython,版本为 2.7,因此这可能不适用于 CPython...
注意2:我这里的 .py 文件的前两行是:
# -*- coding: utf-8 -*-
from __future__ import print_function

"

“%”(也称为“插值运算符”)字符串构造机制也会导致其他问题...如果“环境”的默认编码是ASCII,并且您尝试执行以下操作

"
print( "bonjour, %s" % "fréd" )  # Call this "print A"

您在 Eclipse 中运行时不会遇到任何困难... 在 Windows 命令行界面(DOS 窗口)中,您会发现编码方式为 代码页 850(我的 Windows 7 操作系统)或类似的编码方式,它至少可以处理欧洲重音字符,因此它能够正常工作。
print( u"bonjour, %s" % "fréd" ) # Call this "print B"

也可以工作。

然而,如果您从CLI直接引用文件,则stdout编码将为None,这将默认为ASCII(在我的操作系统上),无法处理上述任何一种打印方式...(可怕的编码错误)。

因此,您可以考虑使用重定向您的stdout:

sys.stdout = codecs.getwriter('utf8')(sys.stdout)

尝试在CLI中运行并将其导出到文件中...非常奇怪的是,上面的print A可以正常工作...但是上面的print B会抛出编码错误!然而,以下内容将可以正常工作:

print( u"bonjour, " + "fréd" ) # Call this "print C"

我得出的(暂定)结论是,如果一个字符串被指定为使用“u”前缀的Unicode字符串,并提交给%处理机制,它似乎涉及使用默认环境编码,无论是否设置了stdout重定向! 人们如何处理这个问题是个选择问题。我希望一个Unicode专家能说一下为什么会发生这种情况,我是否以某种方式搞错了,这个问题的首选解决方案是什么,它是否也适用于CPython,它是否在Python 3中发生等等。

这并不奇怪,因为“fréd”是字节序列而不是Unicode字符串,所以codecs.getwriter包装器会放过它。你需要在前面加上 u 或者使用 from __future__ import unicode_literals - Matthias Urlichs
@MatthiasUrlichs 好的...谢谢...但我认为编码是IT中最令人恼火的方面之一。你是从哪里获得你的理解的?例如,我刚刚在这里发布了另一个关于编码的问题:https://stackoverflow.com/questions/44483067/passing-an-encoding-switch-to-the-jvm-for-a-gradle-javaexec-task 这是关于Java、Eclipse、Cygwin和Gradle的。如果您的专业知识到达了这个程度,请帮忙...最重要的是我想知道在哪里学习更多! - mike rodent

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接