如何从Python函数调用中捕获标准输出?


112

我正在使用对对象执行某些操作的Python库

do_something(my_object)

并更改它。这样做时,它会向stdout打印一些统计信息,我希望掌握这些信息。正确的解决方案是更改do_something()以返回相关信息,

out = do_something(my_object)

但是开发人员需要一段时间才能do_something()解决此问题。作为一种解决方法,我考虑过解析do_something()对stdout的任何写入。

如何捕获代码两点之间的stdout输出,例如

start_capturing()
do_something(my_object)
out = end_capturing()



我在链接问题中的回答也适用于此。
马丁·彼得

我试过一次,发现的最佳答案是: stackoverflow.com/a/3113913/1330293
elyase 2013年

@elyase链接的答案是一个
很好的

看到这个答案
martineau

Answers:


183

试试这个上下文管理器:

from io import StringIO 
import sys

class Capturing(list):
    def __enter__(self):
        self._stdout = sys.stdout
        sys.stdout = self._stringio = StringIO()
        return self
    def __exit__(self, *args):
        self.extend(self._stringio.getvalue().splitlines())
        del self._stringio    # free up some memory
        sys.stdout = self._stdout

用法:

with Capturing() as output:
    do_something(my_object)

output 现在是一个包含函数调用打印的行的列表。

高级用法:

可能不明显的是,此操作可以执行一次以上,并将结果连接在一起:

with Capturing() as output:
    print('hello world')

print('displays on screen')

with Capturing(output) as output:  # note the constructor argument
    print('hello world2')

print('done')
print('output:', output)

输出:

displays on screen                     
done                                   
output: ['hello world', 'hello world2']

更新:它们已添加redirect_stdout()contextlibPython 3.4中(以及redirect_stderr())。因此,您可以使用io.StringIO它来达到类似的结果(尽管Capturing列表和上下文管理器可能更方便)。


谢谢!还要感谢您添加了高级部分...我本来是使用切片分配将捕获的文本粘贴到列表中的,然后我将自己放在头上,然后改为使用,.extend()以便可以将其串联使用,就像您注意到的那样。:-)
kindall

PS:如果要重复使用它,建议在该方法self._stringio.truncate(0)self.extend()调用后添加a ,__exit__()以释放该_stringio成员所拥有的一些内存。
martineau

25
好答案,谢谢。对于Python 3,from io import StringIO在上下文管理器中使用而不是第一行。
Wtower

1
这是线程安全的吗?如果其他线程/调用在do_something运行时使用print()会发生什么?
Derorrist

1
此答案不适用于C共享库的输出,请参阅此答案
craymichael

81

在python> = 3.4中,contextlib包含一个redirect_stdout装饰器。它可以像这样回答您的问题:

import io
from contextlib import redirect_stdout

f = io.StringIO()
with redirect_stdout(f):
    do_something(my_object)
out = f.getvalue()

文档

上下文管理器,用于临时将sys.stdout重定向到另一个文件或类似文件的对象。

该工具为现有功能或类的输出增加了灵活性,这些功能或类的输出被硬连线到stdout。

例如,通常将help()的输出发送到sys.stdout。您可以通过将输出重定向到io.StringIO对象来捕获该输出的字符串:

  f = io.StringIO() 
  with redirect_stdout(f):
      help(pow) 
  s = f.getvalue()

要将help()的输出发送到磁盘上的文件,请将输出重定向到常规文件:

 with open('help.txt', 'w') as f:
     with redirect_stdout(f):
         help(pow)

要将help()的输出发送到sys.stderr:

with redirect_stdout(sys.stderr):
    help(pow)

请注意,对sys.stdout的全局副作用意味着此上下文管理器不适合在库代码和大多数线程应用程序中使用。它还对子流程的输出没有影响。但是,对于许多实用程序脚本,它仍然是一种有用的方法。

该上下文管理器是可重入的。


当尝试f = io.StringIO() with redirect_stdout(f): logger = getLogger('test_logger') logger.debug('Test debug message') out = f.getvalue() self.assertEqual(out, 'DEBUG:test_logger:Test debug message')。它给了我一个错误:AssertionError: '' != 'Test debug message'
Eziz Durdyyev '19

这意味着我做错了什么,或者无法捕获标准输出日志。
Eziz Durdyyev '19

@EzizDurdyyev,logger.debug默认情况下不会写入标准输出。如果将日志呼叫替换为print(),则应该看到该消息。
ForeverWintr

是的,我知道,但是我确实将其写入stdout,如下所示:stream_handler = logging.StreamHandler(sys.stdout)。并将该处理程序添加到我的记录器中。所以它应该写到stdout并redirect_stdout捕获它,对吧?
Eziz Durdyyev

我怀疑问题出在您配置记录器的方式上。我将验证它是否在没有redirect_stdout的情况下打印到stdout。如果是这样,则在上下文管理器退出之前,可能不会刷新缓冲区。
ForeverWintr

0

这是使用文件管道的异步解决方案。

import threading
import sys
import os

class Capturing():
    def __init__(self):
        self._stdout = None
        self._stderr = None
        self._r = None
        self._w = None
        self._thread = None
        self._on_readline_cb = None

    def _handler(self):
        while not self._w.closed:
            try:
                while True:
                    line = self._r.readline()
                    if len(line) == 0: break
                    if self._on_readline_cb: self._on_readline_cb(line)
            except:
                break

    def print(self, s, end=""):
        print(s, file=self._stdout, end=end)

    def on_readline(self, callback):
        self._on_readline_cb = callback

    def start(self):
        self._stdout = sys.stdout
        self._stderr = sys.stderr
        r, w = os.pipe()
        r, w = os.fdopen(r, 'r'), os.fdopen(w, 'w', 1)
        self._r = r
        self._w = w
        sys.stdout = self._w
        sys.stderr = self._w
        self._thread = threading.Thread(target=self._handler)
        self._thread.start()

    def stop(self):
        self._w.close()
        if self._thread: self._thread.join()
        self._r.close()
        sys.stdout = self._stdout
        sys.stderr = self._stderr

用法示例:

from Capturing import *
import time

capturing = Capturing()

def on_read(line):
    # do something with the line
    capturing.print("got line: "+line)

capturing.on_readline(on_read)
capturing.start()
print("hello 1")
time.sleep(1)
print("hello 2")
time.sleep(1)
print("hello 3")
capturing.stop()
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.