Подтвердить что ты не робот

В 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 не может захватывать выходы из общей библиотеки?
4b9b3361

Ответ 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 и не возвращает ее так...