在python中,如何捕获从c++共享库到变量的stdout

In python, how to capture the stdout from a c++ shared library to a variable

本文关键字:共享 变量 stdout c++ python 何捕获      更新时间:2023-10-16

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

示例:抑制调用库外的模块的输出

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((更改为打印"hello">,它就会工作!

我的问题是:

  1. 如何将c++共享库的stdout捕获到变量
  2. 为什么使用StringIO不能捕获共享库的输出

多亏了Adam的回答,我才得以做到这一点。他的解决方案不太适合我的情况,因为我需要多次捕获文本、恢复和再次捕获文本,所以我不得不做出一些相当大的更改。此外,我还想让它也适用于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)

在Windows 7和Ubuntu 12.04上测试了Python 2.7.6。

要在Python 3中工作,请更改char = os.read(self.pipe_out,1)
至CCD_ 3。

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

因此,为了使共享库输出到不同的位置,您需要通过打开一个新的文件描述符,然后使用os.dup2()替换stdout来更改底层文件描述符。您可以使用临时文件作为输出,但最好使用使用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

更简单地说,Py库有一个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,请参阅这些文档。

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

谢谢Devan!

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

无论出于何种原因,您想要强制捕获的行停止

self.origstream.write(self.escape_char)

不起作用。我注释掉了它,并确保我的stdout捕获字符串包含转义符,否则就是行

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

在while循环中永远等待。

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

仅此而已。再次感谢!

对于任何从谷歌来到这里了解如何抑制共享库(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')

使用管道,即os.pipe。在调用库

之前,您需要os.dup2它

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

所以这是你不能做的。你可以做的是将对这个库的任何打印调用封装在你可以在子流程中执行的东西中。然后,使用Python的subprocess.check_output,您可以将该子进程的stdout返回到程序中。缓慢、混乱、有点邋遢,但另一方面,您使用的库会将有用的信息打印到stdout,而不会返回,所以…