我能把stdout重定向到某个字符串缓冲区吗?

184

我正在使用Python的ftplib编写一个小型FTP客户端,但该包中的某些函数不返回字符串输出,而是打印到stdout。我希望将stdout重定向到一个对象,以便能够从中读取输出。

我知道可以通过以下方式将stdout重定向到任何常规文件:

stdout = open("file", "a")

不过,我更喜欢一种不使用本地驱动器的方法。

我正在寻找像Java中的BufferedReader那样可以用来将缓冲区包装成流的方法。


1
我认为仅仅使用stdout = open("file", "a")是无法重定向任何内容的。 - Alexey
9个回答

255
from cStringIO import StringIO # Python3 use: from io import StringIO
import sys

old_stdout = sys.stdout
sys.stdout = mystdout = StringIO()

# blah blah lots of code ...

sys.stdout = old_stdout

# examine mystdout.getvalue()

71
+1,您不需要保留对原始stdout对象的引用,因为它始终可在sys.__stdout__中获得。请参见http://docs.python.org/library/sys.html#sys.__stdout__。 - Ayman Hourieh
104
这是一个有趣的讨论。原始的stdout是可用的,但像这样进行替换时,最好使用显式保存,就像我所做的那样,因为其他人可能已经替换了stdout,如果您使用__stdout__,就会覆盖他们的替换。 - Ned Batchelder
7
这个操作在一个线程中会改变其他线程的行为吗?我的意思是它是线程安全的吗? - Anuvrat Parashar
10
我强烈建议在finally:块中重新分配旧的stdout,这样即使在此期间出现异常也会重新分配。 try:bkp = sys.stdout ...... finally:sys.stdout = bkp - Matthias Kuhn
25
如果您想在Python 3中使用这个,请用io替换cStringIO。 - Anthony Labarre
显示剩余7条评论

149

8
最新版的Python也有redirect_stderr - CMCDragonkai

42

补充一下Ned上面的回答:你可以使用这个方法将输出重定向到任何实现write(str)方法的对象

这可以用于在GUI应用程序中"捕获"stdout输出。

以下是一个PyQt中的愚蠢示例:

import sys
from PyQt4 import QtGui

class OutputWindow(QtGui.QPlainTextEdit):
    def write(self, txt):
        self.appendPlainText(str(txt))

app = QtGui.QApplication(sys.argv)
out = OutputWindow()
sys.stdout=out
out.show()
print "hello world !"

5
Python 2.6和PyQT4版本对我有效。当你不知道为什么程序能运行却被投反对票时,这似乎很奇怪! - Nicolas Lefebvre
9
别忘记加上 flush()! - Will

11

一个Python3的上下文管理器:

import sys
from io import StringIO


class RedirectedStdout:
    def __init__(self):
        self._stdout = None
        self._string_io = None

    def __enter__(self):
        self._stdout = sys.stdout
        sys.stdout = self._string_io = StringIO()
        return self

    def __exit__(self, type, value, traceback):
        sys.stdout = self._stdout

    def __str__(self):
        return self._string_io.getvalue()

使用方法如下:

>>> with RedirectedStdout() as out:
>>>     print('asdf')
>>>     s = str(out)
>>>     print('bsdf')
>>> print(s, out)
'asdf\n' 'asdf\nbsdf\n'

9
从Python 2.6开始,您可以使用实现io.TextIOBase API的任何内容来替换。 这个解决方案还使您能够在Python 3中使用sys.stdout.buffer.write()将(已经)编码的字节字符串写入标准输出(请参见Python 3中的stdout)。 然后使用io.StringIO是行不通的,因为既没有sys.stdout.buffer也没有sys.stdout.encoding可用。
使用io.TextIOWrapper的解决方案:
import io
import sys

# Setup stdout.
old_stdout = sys.stdout
sys.stdout = io.TextIOWrapper(io.BytesIO(), sys.stdout.encoding)

# Write to stdout or stdout.buffer.
...

# Read from stdout.
sys.stdout.seek(0)
out = sys.stdout.read()

# Restore stdout.
sys.stdout.close()
sys.stdout = old_stdout

这个解决方案适用于Python 2 >= 2.6和Python 3。

请注意,新的sys.stdout.write()只接受Unicode字符串,而sys.stdout.buffer.write()只接受字节字符串。 在不需要更改的情况下构建运行于Python 2和3的代码通常使用sys.stdout.buffer

因此,为了让sys.stdout.write()同时接受Unicode和字节字符串,您可以使用这个io.TextIOWrapper子类:

class StdoutBuffer(io.TextIOWrapper):

    def write(self, string):
        try:
            return super(StdoutBuffer, self).write(string)
        except TypeError:
            # Redirect encoded byte strings directly to buffer.
            return super(StdoutBuffer, self).buffer.write(string)

您不必设置缓冲区的编码 sys.stdout.encoding,但在使用此方法进行测试/比较脚本输出时会有所帮助。


这个答案帮助我设置了一个环境对象的stdout参数,用于与Httpie的core.py一起使用。 - fragorl

8

该方法即使出现异常也会恢复sys.stdout。它还可获取异常之前的任何输出。

import io
import sys

real_stdout = sys.stdout
fake_stdout = io.BytesIO()   # or perhaps io.StringIO()
try:
    sys.stdout = fake_stdout
    # do what you have to do to create some output
finally:
    sys.stdout = real_stdout
    output_string = fake_stdout.getvalue()
    fake_stdout.close()
    # do what you want with the output_string

使用 io.BytesIO() 在Python 2.7.10中进行测试。

使用 io.StringIO() 在Python 3.6.4中进行测试。


Bob,如果你觉得修改/扩展代码实验中的任何内容都可能变得有趣,可以留下它。否则请随意删除

为了提供信息......在寻找一些可行的机制“抓取”输出时,在numexpr.print_versions()直接指向<stdout>(需要清理GUI并将详细信息收集到调试报告)期间进行扩展实验的一些评论

# THIS WORKS AS HELL: as Bob Stein proposed years ago:
#  py2 SURPRISEDaBIT:
#
import io
import sys
#
real_stdout = sys.stdout                        #           PUSH <stdout> ( store to REAL_ )
fake_stdout = io.BytesIO()                      #           .DEF FAKE_
try:                                            # FUSED .TRY:
    sys.stdout.flush()                          #           .flush() before
    sys.stdout = fake_stdout                    #           .SET <stdout> to use FAKE_
    # ----------------------------------------- #           +    do what you gotta do to create some output
    print 123456789                             #           + 
    import  numexpr                             #           + 
    QuantFX.numexpr.__version__                 #           + [3] via fake_stdout re-assignment, as was bufferred + "late" deferred .get_value()-read into print, to finally reach -> real_stdout
    QuantFX.numexpr.print_versions()            #           + [4] via fake_stdout re-assignment, as was bufferred + "late" deferred .get_value()-read into print, to finally reach -> real_stdout
    _ = os.system( 'echo os.system() redir-ed' )#           + [1] via real_stdout                                 + "late" deferred .get_value()-read into print, to finally reach -> real_stdout, if not ( _ = )-caught from RET-d "byteswritten" / avoided from being injected int fake_stdout
    _ = os.write(  sys.stderr.fileno(),         #           + [2] via      stderr                                 + "late" deferred .get_value()-read into print, to finally reach -> real_stdout, if not ( _ = )-caught from RET-d "byteswritten" / avoided from being injected int fake_stdout
                       b'os.write()  redir-ed' )#  *OTHERWISE, if via fake_stdout, EXC <_io.BytesIO object at 0x02C0BB10> Traceback (most recent call last):
    # ----------------------------------------- #           ?                              io.UnsupportedOperation: fileno
    #'''                                                    ? YET:        <_io.BytesIO object at 0x02C0BB10> has a .fileno() method listed
    #>>> 'fileno' in dir( sys.stdout )       -> True        ? HAS IT ADVERTISED,
    #>>> pass;            sys.stdout.fileno  -> <built-in method fileno of _io.BytesIO object at 0x02C0BB10>
    #>>> pass;            sys.stdout.fileno()-> Traceback (most recent call last):
    #                                             File "<stdin>", line 1, in <module>
    #                                           io.UnsupportedOperation: fileno
    #                                                       ? BUT REFUSES TO USE IT
    #'''
finally:                                        # == FINALLY:
    sys.stdout.flush()                          #           .flush() before ret'd back REAL_
    sys.stdout = real_stdout                    #           .SET <stdout> to use POP'd REAL_
    sys.stdout.flush()                          #           .flush() after  ret'd back REAL_
    out_string = fake_stdout.getvalue()         #           .GET string           from FAKE_
    fake_stdout.close()                         #                <FD>.close()
    # +++++++++++++++++++++++++++++++++++++     # do what you want with the out_string
    #
    print "\n{0:}\n{1:}{0:}".format( 60 * "/\\",# "LATE" deferred print the out_string at the very end reached -> real_stdout
                                     out_string #                   
                                     )
'''
PASS'd:::::
...
os.system() redir-ed
os.write()  redir-ed
/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
123456789
'2.5'
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Numexpr version:   2.5
NumPy version:     1.10.4
Python version:    2.7.13 |Anaconda 4.0.0 (32-bit)| (default, May 11 2017, 14:07:41) [MSC v.1500 32 bit (Intel)]
AMD/Intel CPU?     True
VML available?     True
VML/MKL version:   Intel(R) Math Kernel Library Version 11.3.1 Product Build 20151021 for 32-bit applications
Number of threads used by default: 4 (out of 4 detected cores)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
>>>

EXC'd :::::
...
os.system() redir-ed
/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
123456789
'2.5'
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Numexpr version:   2.5
NumPy version:     1.10.4
Python version:    2.7.13 |Anaconda 4.0.0 (32-bit)| (default, May 11 2017, 14:07:41) [MSC v.1500 32 bit (Intel)]
AMD/Intel CPU?     True
VML available?     True
VML/MKL version:   Intel(R) Math Kernel Library Version 11.3.1 Product Build 20151021 for 32-bit applications
Number of threads used by default: 4 (out of 4 detected cores)
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\

Traceback (most recent call last):
  File "<stdin>", line 9, in <module>
io.UnsupportedOperation: fileno
'''

5
在 Python3.6 版本中,StringIOcStringIO 模块已被删除,应该使用 io.StringIO 代替。因此,你应该像第一个答案那样做:
import sys
from io import StringIO

old_stdout = sys.stdout
old_stderr = sys.stderr
my_stdout = sys.stdout = StringIO()
my_stderr = sys.stderr = StringIO()

# blah blah lots of code ...

sys.stdout = self.old_stdout
sys.stderr = self.old_stderr

// if you want to see the value of redirect output, be sure the std output is turn back
print(my_stdout.getvalue())
print(my_stderr.getvalue())

my_stdout.close()
my_stderr.close()

1
你可以通过解释上述代码的工作原理以及如何改进提问者的情况来提高你的回答质量。 - toonice

3
这是另一种方法。使用io.StringIO()作为文档contextlib.redirect_stdout非常好,但对于日常使用来说仍然有点冗长。以下是通过子类化contextlib.redirect_stdout将其变成一行代码的方法:
import sys
import io
from contextlib import redirect_stdout

class capture(redirect_stdout):

    def __init__(self):
        self.f = io.StringIO()
        self._new_target = self.f
        self._old_targets = []  # verbatim from parent class

    def __enter__(self):
        self._old_targets.append(getattr(sys, self._stream))  # verbatim from parent class
        setattr(sys, self._stream, self._new_target)  # verbatim from parent class
        return self  # instead of self._new_target in the parent class

    def __repr__(self):
        return self.f.getvalue()  

由于__enter__返回self,因此您可以在with块退出后使用上下文管理器对象。此外,由于__repr__方法,上下文管理器对象的字符串表示实际上是标准输出(stdout)。因此现在您拥有了:

with capture() as message:
    print('Hello World!')
print(str(message)=='Hello World!\n')  # returns True

3

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