В python, как захватить stdout из общей библиотеки С++ в переменную
По другим причинам, используемая совместно используемая библиотека С++ выводит некоторые тексты на стандартный вывод. В 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(), чтобы напечатать "привет" , это сработает!
Моя проблема:
- как записать stdout общей библиотеки С++ в переменную?
- Почему использование StringIO не может захватывать выходы из общей библиотеки?
Ответы
Ответ 1
Python sys.stdout
объект - это просто оболочка Python поверх обычного дескриптора файла stdout - его изменение влияет только на процесс Python, а не на основной дескриптор файла. Любой не-Python-код, будь то другой исполняемый файл, который был exec
'ed или C shared library, который был загружен, не понимает этого и будет продолжать использовать обычные файловые дескрипторы для ввода-вывода.
Итак, для того, чтобы общая библиотека могла выводиться в другое место, вам необходимо изменить базовый дескриптор файла, открыв новый файловый дескриптор, а затем заменив stdout с помощью 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
Ответ 2
Благодаря хорошему ответу Адама я смог заставить это работать. Его решение не вполне сработало для моего случая, так как мне нужно было многократно захватывать текст, восстанавливать и захватывать текст, поэтому мне пришлось внести довольно большие изменения. Кроме того, я хотел, чтобы это работало и для 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 с Python 2.7.6 и Ubuntu 12.04 с Python 2.7.6.
Для работы в Python 3 измените char = os.read(self.pipe_out,1)
до char = os.read(self.pipe_out,1).decode(self.origstream.encoding)
.
Ответ 3
Спасибо, Деван!
Ваш код мне очень помог, но у меня были некоторые проблемы с его использованием. Я хочу поделиться здесь:
По какой-либо причине строка, в которой вы хотите остановить захват,
self.origstream.write(self.escape_char)
не работает. Я прокомментировал это и удостоверился, что моя строка, записанная stdout, содержит символ escape, иначе строка
data = os.read(self.pipe_out, 1) # Read One Byte Only
в цикле while ожидает навсегда.
Еще одна вещь - использование. Убедитесь, что объект класса OutputGrabber является локальной переменной. Если вы используете глобальный объект или атрибут класса (например, self.out = OutputGrabber()), вы столкнетесь с проблемой при его воссоздании.
Это все. Еще раз спасибо!
Ответ 4
Используйте канал, т.е. os.pipe. Вам нужно os.dup2 перед вызовом вашей библиотеки
Ответ 5
Для тех, кто пришел сюда из Google, чтобы найти, как подавить вывод stderr/stdout из общей библиотеки (dll), так же, как и я, я публикую следующий простой контекстный менеджер на основе ответа Адама:
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) as guard:
liba.hello() # Call into the shared library
print('End')
Ответ 6
В основном невозможно записать stdout из кода библиотеки, потому что это зависит от вашего кода, запущенного в среде, где.) вы находитесь в оболочке и b.) нет другого контента, идущего на ваш stdout. Хотя вы можете сделать что-то работающее под этими ограничениями, если вы намерены развернуть этот код в каком-либо смысле вообще, просто нет возможности разумно гарантировать последовательное хорошее поведение. На самом деле, довольно сомнительно, что этот код библиотеки печатает на stdout таким образом, который нельзя контролировать в любом случае.
Итак, что вы не можете сделать. Что вы можете сделать, это обернуть любые вызовы печати в эту библиотеку внутри того, что вы можете выполнить в подпроцессе. Используя Python subprocess.check_output
, вы можете получить stdout из этого подпроцесса в вашей программе. Медленный, беспорядочный, любопытный, все вокруг, но, с другой стороны, библиотека, которую вы используете, печатает полезную информацию в stdout и не возвращает ее так...