【问题标题】:In python, how to capture the stdout from a c++ shared library to a variable在 python 中,如何将标准输出从 c++ 共享库捕获到变量
【发布时间】:2014-08-08 06:33:17
【问题描述】:

由于某些其他原因,我使用的 c++ 共享库将一些文本输出到标准输出。在 python 中,我想捕获输出并保存到变量。关于重定向标准输出有很多类似的问题,但在我的代码中不起作用。

示例:Suppressing output of module calling outside library

1 import sys
2 import cStringIO
3 save_stdout = sys.stdout
4 sys.stdout = cStringIO.StringIO()
5 func()
6 sys.stdout = save_stdout

在第 5 行,func() 将调用共享库,共享库生成的文本仍然输出到控制台!如果把 func() 改成 print "hello" 就可以了!

我的问题是:

  1. 如何将 c++ 共享库的标准输出捕获到变量中
  2. 为什么使用 StringIO,无法捕获共享库的输出?

【问题讨论】:

  • C++ 代码必须用于调用 Python,因此它也使用sys.stdoutprint()。事实上,它可能使用std::coutprintf(),两者都写入进程的STDOUT 文件描述符。如果要捕获该输出,则必须用管道替换它。最简单的方法可能是更改 C++ 代码以返回相应的字符串,这也是传递数据最直接的方法。
  • 明确一点:更改sys.stdout 只会影响Python 解释器 的标准输出;它不会更改 实际 标准输出文件描述符 (1)。您提出的问题相关的,可以解决您的问题。

标签: python c++


【解决方案1】:

感谢Adam 提供的nice answer,我才得以完成这项工作。他的解决方案对我的情况不太适用,因为我需要多次捕获文本、恢复和再次捕获文本,所以我必须进行一些相当大的更改。另外,我想让它也适用于 sys.stderr(可能适用于其他流)。

所以,这是我最终使用的解决方案(带或不带线程):

代码

import os
import sys
import threading
import time


class OutputGrabber(object):
    """
    Class used to grab standard output or another stream.
    """
    escape_char = "\b"

    def __init__(self, stream=None, threaded=False):
        self.origstream = stream
        self.threaded = threaded
        if self.origstream is None:
            self.origstream = sys.stdout
        self.origstreamfd = self.origstream.fileno()
        self.capturedtext = ""
        # Create a pipe so the stream can be captured:
        self.pipe_out, self.pipe_in = os.pipe()

    def __enter__(self):
        self.start()
        return self

    def __exit__(self, type, value, traceback):
        self.stop()

    def start(self):
        """
        Start capturing the stream data.
        """
        self.capturedtext = ""
        # Save a copy of the stream:
        self.streamfd = os.dup(self.origstreamfd)
        # Replace the original stream with our write pipe:
        os.dup2(self.pipe_in, self.origstreamfd)
        if self.threaded:
            # Start thread that will read the stream:
            self.workerThread = threading.Thread(target=self.readOutput)
            self.workerThread.start()
            # Make sure that the thread is running and os.read() has executed:
            time.sleep(0.01)

    def stop(self):
        """
        Stop capturing the stream data and save the text in `capturedtext`.
        """
        # Print the escape character to make the readOutput method stop:
        self.origstream.write(self.escape_char)
        # Flush the stream to make sure all our data goes in before
        # the escape character:
        self.origstream.flush()
        if self.threaded:
            # wait until the thread finishes so we are sure that
            # we have until the last character:
            self.workerThread.join()
        else:
            self.readOutput()
        # Close the pipe:
        os.close(self.pipe_in)
        os.close(self.pipe_out)
        # Restore the original stream:
        os.dup2(self.streamfd, self.origstreamfd)
        # Close the duplicate stream:
        os.close(self.streamfd)

    def readOutput(self):
        """
        Read the stream data (one byte at a time)
        and save the text in `capturedtext`.
        """
        while True:
            char = os.read(self.pipe_out, 1)
            if not char or self.escape_char in char:
                break
            self.capturedtext += char

用法

使用 sys.stdout,默认:

out = OutputGrabber()
out.start()
library.method(*args) # Call your code here
out.stop()
# Compare the output to the expected value:
# comparisonMethod(out.capturedtext, expectedtext)

使用 sys.stderr:

out = OutputGrabber(sys.stderr)
out.start()
library.method(*args) # Call your code here
out.stop()
# Compare the output to the expected value:
# comparisonMethod(out.capturedtext, expectedtext)

with 块中:

out = OutputGrabber()
with out:
    library.method(*args) # Call your code here
# Compare the output to the expected value:
# comparisonMethod(out.capturedtext, expectedtext)

在带有 Python 2.7.6 的 Windows 7 和带有 Python 2.7.6 的 Ubuntu 12.04 上测试。

要在 Python 3 中工作,请更改 char = os.read(self.pipe_out,1)
char = os.read(self.pipe_out,1).decode(self.origstream.encoding)

【讨论】:

  • 你有self.capturedtext += data这一行,但数据从未定义?
  • Miguel,正确的代码应该是self.capturedtext += char,很好的捕捉
【解决方案2】:

Python 的 sys.stdout 对象只是普通标准输出文件描述符之上的 Python 包装器——更改它只会影响 Python 进程,而不影响底层文件描述符。任何非 Python 代码,无论是 exec'ed 的另一个可执行文件还是已加载的 C 共享库,都不会理解这一点,并将继续使用普通文件描述符进行 I/O。

因此,为了让共享库输出到不同的位置,您需要通过打开一个新的文件描述符然后使用os.dup2() 替换标准输出来更改底层文件描述符。您可以使用临时文件作为输出,但最好使用使用os.pipe() 创建的管道。但是,这有死锁的危险,如果没有任何东西正在读取管道,所以为了防止我们可以使用另一个线程来排空管道。

下面是一个完整的工作示例,它不使用临时文件并且不易发生死锁(在 Mac OS X 上测试)。

C 共享库代码:

// test.c
#include <stdio.h>

void hello(void)
{
  printf("Hello, world!\n");
}

编译为:

$ clang test.c -shared -fPIC -o libtest.dylib

Python 驱动程序:

import ctypes
import os
import sys
import threading

print 'Start'

liba = ctypes.cdll.LoadLibrary('libtest.dylib')

# Create pipe and dup2() the write end of it on top of stdout, saving a copy
# of the old stdout
stdout_fileno = sys.stdout.fileno()
stdout_save = os.dup(stdout_fileno)
stdout_pipe = os.pipe()
os.dup2(stdout_pipe[1], stdout_fileno)
os.close(stdout_pipe[1])

captured_stdout = ''
def drain_pipe():
    global captured_stdout
    while True:
        data = os.read(stdout_pipe[0], 1024)
        if not data:
            break
        captured_stdout += data

t = threading.Thread(target=drain_pipe)
t.start()

liba.hello()  # Call into the shared library

# Close the write end of the pipe to unblock the reader thread and trigger it
# to exit
os.close(stdout_fileno)
t.join()

# Clean up the pipe and restore the original stdout
os.close(stdout_pipe[0])
os.dup2(stdout_save, stdout_fileno)
os.close(stdout_save)

print 'Captured stdout:\n%s' % captured_stdout

【讨论】:

  • 如果 c 代码写入超过 1024 个字节,这是否真的有效?我希望当您调用 C 代码时会获取 GIL,并且在您回调 python 之前它不会被释放 - 所以我认为即使使用管道,您仍然可以填充管道 -更干净,因为当其他东西持有 GIL 时,它不会有机会执行。
  • @mgilson:这是一个值得关注的问题,但在我所做的测试中,这可以扩展到 MBs+ 的数据,而在 CPython 2.7.16 和 3.7.3 上都没有任何死锁问题(稍作修改)使其与 Py3k 兼容),因此看起来至少那些运行时版本在调用 C 代码时持有 GIL。
  • 是的,显然这是符合ctypes 文档的规范(例如docs.python.org/3.3/library/ctypes.html#ctypes.PyDLL 确实 获取了 GIL)。我猜魔鬼在细节中,它真的取决于如何 C/C++ 代码暴露给python。例如它可能适用于 boost::python 或 cython 包装的东西,除非你在这些情况下做额外的工作来释放 GIL。
【解决方案3】:

谢谢德文!

您的代码对我帮助很大,但我在使用它时遇到了一些问题,我想在这里分享:

出于任何原因您要强制捕获停止的行

self.origstream.write(self.escape_char)

不起作用。我将其注释掉并确保我的标准输出捕获的字符串包含转义字符,否则该行

data = os.read(self.pipe_out, 1)  # Read One Byte Only

在while循环中永远等待。

另一件事是用法。确保 OutputGrabber 类的对象是局部变量。如果使用全局对象或类属性(例如 self.out = OutputGrabber()),在重新创建它时会遇到麻烦。

就是这样。再次感谢您!

【讨论】:

    【解决方案4】:

    对于从谷歌来到这里以找到如何抑制共享库 (dll) 的 stderr/stdout 输出的任何人,就像我一样,我根据 Adam 的回答发布了下一个简单的上下文管理器:

    class SuppressStream(object): 
    
        def __init__(self, stream=sys.stderr):
            self.orig_stream_fileno = stream.fileno()
    
        def __enter__(self):
            self.orig_stream_dup = os.dup(self.orig_stream_fileno)
            self.devnull = open(os.devnull, 'w')
            os.dup2(self.devnull.fileno(), self.orig_stream_fileno)
    
        def __exit__(self, type, value, traceback):
            os.close(self.orig_stream_fileno)
            os.dup2(self.orig_stream_dup, self.orig_stream_fileno)
            os.close(self.orig_stream_dup)
            self.devnull.close()
    

    用法(改编亚当的例子):

    import ctypes
    import sys
    print('Start')
    
    liba = ctypes.cdll.LoadLibrary('libtest.so')
    
    with SuppressStream(sys.stdout):
        liba.hello()  # Call into the shared library
    
    print('End')
    

    【讨论】:

    • btw as guard 看起来没有必要,因为它只是 None
    • 效果很好,但与这个问题只是松散相关。恕我直言,您应该在此处删除并重新发布此答案:stackoverflow.com/q/5081657/837710 这将帮助我更早地找到它。如果你这样做,请发表评论,我会在那里投票。其他答案似乎适用于 Python 2,因此这将是一个有价值的贡献。
    【解决方案5】:

    更简单地说,Py library 有一个 StdCaptureFD 捕获流文件描述符,它允许捕获来自 C/C++ 扩展模块的输出(与其他答案类似的机制)。请注意,据说该库仅处于维护状态。

    >>> import py, sys
    >>> capture = py.io.StdCaptureFD(out=False, in_=False)
    >>> sys.stderr.write("world")
    >>> out,err = capture.reset()
    >>> err
    'world'
    

    另一个值得注意的解决方案是,如果你在一个pytest测试夹具中,你可以直接使用capfd,见these docs

    虽然其他答案也可能运行良好,但我在 PyCharm IDE (io.UnsupportedOperation: fileno) 中使用他们的代码时遇到了错误,而 StdCaptureFD 运行良好。

    【讨论】:

    • 这对于一个简单的 Clib 静音和笨重的终端登录来说非常有效。 py 包比我想要的要大一些 - 所以建议导入 py.io
    【解决方案6】:

    使用管道,即os.pipe。在调用你的图书馆之前,你需要os.dup2

    【讨论】:

      【解决方案7】:

      从库代码中捕获标准输出基本上是站不住脚的,因为这取决于您的代码在以下环境中运行:a.) 您在 shell 上,b.) 没有其他内容进入您的标准输出。虽然您可能使某些东西在这些限制下工作,但如果您打算在任何意义上部署此代码,则无法合理地保证一致的良好行为。事实上,这个库代码以一种无论如何都无法控制的方式打印到标准输出是非常值得怀疑的。

      所以这是你不能做的。您可以做的是将对该库的任何打印调用包装在您可以在子进程中执行的内容中。然后,使用 Python 的 subprocess.check_output,您可以从该子进程中将标准输出返回到您的程序中。到处都是缓慢,凌乱,有点恶心,但另一方面,您使用的库将有用信息打印到标准输出并且不会返回它所以......

      【讨论】:

        猜你喜欢
        • 2019-04-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-01-10
        • 2021-06-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多