Przechwycić standardowe wyjście ze skryptu?

89

załóżmy, że skrypt robi coś takiego:

# module writer.py
import sys

def write():
    sys.stdout.write("foobar")

Załóżmy teraz, że chcę przechwycić dane wyjściowe writefunkcji i zapisać je w zmiennej do dalszego przetwarzania. Naiwnym rozwiązaniem było:

# module mymodule.py
from writer import write

out = write()
print out.upper()

Ale to nie działa. Wymyślam inne rozwiązanie i działa, ale proszę, daj mi znać, czy istnieje lepszy sposób rozwiązania problemu. Dzięki

import sys
from cStringIO import StringIO

# setup the environment
backup = sys.stdout

# ####
sys.stdout = StringIO()     # capture output
write()
out = sys.stdout.getvalue() # release output
# ####

sys.stdout.close()  # close the stream 
sys.stdout = backup # restore original stdout

print out.upper()   # post processing
Paolo
źródło

Odpowiedzi:

49

Ustawienie stdoutto rozsądny sposób na zrobienie tego. Innym jest uruchomienie go jako innego procesu:

import subprocess

proc = subprocess.Popen(["python", "-c", "import writer; writer.write()"], stdout=subprocess.PIPE)
out = proc.communicate()[0]
print out.upper()
Matthew Flaschen
źródło
4
check_output bezpośrednio przechwytuje dane wyjściowe polecenia wykonywanego w podprocesie: <br> wartość = podproces.check_output (polecenie, powłoka = True)
Arthur
1
Wersja sformatowana :value = subprocess.check_output(command, shell=True)
Nae
46

Oto wersja twojego kodu menedżera kontekstu. Daje listę dwóch wartości; pierwszy to stdout, drugi to stderr.

import contextlib
@contextlib.contextmanager
def capture():
    import sys
    from cStringIO import StringIO
    oldout,olderr = sys.stdout, sys.stderr
    try:
        out=[StringIO(), StringIO()]
        sys.stdout,sys.stderr = out
        yield out
    finally:
        sys.stdout,sys.stderr = oldout, olderr
        out[0] = out[0].getvalue()
        out[1] = out[1].getvalue()

with capture() as out:
    print 'hi'
Jason Grout
źródło
Uwielbiam to rozwiązanie. Zmodyfikowałem, żeby przypadkowo nie zgubić rzeczy ze strumienia, na którym nie spodziewam się wyjścia, np. Nieoczekiwane błędy. W moim przypadku funkcja capture () może zaakceptować sys.stderr lub sys.stdout jako parametr, wskazując na przechwytywanie tylko tego strumienia.
Joshua Richardson
StringIO nie obsługuje Unicode w żaden sposób, więc możesz zintegrować odpowiedź tutaj, aby powyższe obsługiwały znaki inne niż ASCII: stackoverflow.com/a/1819009/425050
mafrosis
2
Modyfikacja uzyskanej wartości w końcu jest naprawdę raczej dziwna - with capture() as out:będzie się zachowywać inaczej niżwith capture() as out, err:
Eric
Obsługę Unicode / stdout.buffer można uzyskać za pomocą modułu io. Zobacz moją odpowiedź .
JonnyJD
1
To rozwiązanie nie działa, jeśli używasz subprocessi przekierowujesz dane wyjściowe do sys.stdout / stderr. Dzieje się tak, ponieważ StringIOnie jest to prawdziwy obiekt pliku i brakuje mu fileno()funkcji.
letmaik
44

Dla przyszłych odwiedzających: Python 3.4 contextlib zapewnia to bezpośrednio (patrz pomoc Python contextlib ) za pośrednictwem redirect_stdoutmenedżera kontekstu:

from contextlib import redirect_stdout
import io

f = io.StringIO()
with redirect_stdout(f):
    help(pow)
s = f.getvalue()
nodesr
źródło
Nie rozwiązuje to problemu podczas próby zapisu do sys.stdout.buffer (tak jak trzeba to zrobić przy zapisywaniu bajtów). StringIO nie ma atrybutu bufora, podczas gdy TextIOWrapper ma. Zobacz odpowiedź od @JonnyJD.
tkacz
9

To jest odpowiednik dekoratora mojego oryginalnego kodu.

writer.py pozostaje takie samo:

import sys

def write():
    sys.stdout.write("foobar")

mymodule.py nieznacznie zostaje zmodyfikowany:

from writer import write as _write
from decorators import capture

@capture
def write():
    return _write()

out = write()
# out post processing...

A oto dekorator:

def capture(f):
    """
    Decorator to capture standard output
    """
    def captured(*args, **kwargs):
        import sys
        from cStringIO import StringIO

        # setup the environment
        backup = sys.stdout

        try:
            sys.stdout = StringIO()     # capture output
            f(*args, **kwargs)
            out = sys.stdout.getvalue() # release output
        finally:
            sys.stdout.close()  # close the stream 
            sys.stdout = backup # restore original stdout

        return out # captured output wrapped in a string

    return captured
Paolo
źródło
9

A może skorzystaj z funkcjonalności, która już tam jest ...

from IPython.utils.capture import capture_output

with capture_output() as c:
    print('some output')

c()

print c.stdout
dgrigonis
źródło
7

Począwszy od Pythona 3, możesz również użyć sys.stdout.buffer.write()do zapisania (już) zakodowanych ciągów bajtów na standardowe wyjście (zobacz stdout w Pythonie 3 ). Kiedy to robisz, proste StringIOpodejście nie działa, ponieważ ani jedno, sys.stdout.encodingani drugie nie sys.stdout.bufferbyłoby dostępne.

Począwszy od Pythona 2.6 możesz używać TextIOBaseAPI , które zawiera brakujące atrybuty:

import sys
from io import TextIOWrapper, BytesIO

# setup the environment
old_stdout = sys.stdout
sys.stdout = TextIOWrapper(BytesIO(), sys.stdout.encoding)

# do some writing (indirectly)
write("blub")

# get output
sys.stdout.seek(0)      # jump to the start
out = sys.stdout.read() # read output

# restore stdout
sys.stdout.close()
sys.stdout = old_stdout

# do stuff with the output
print(out.upper())

To rozwiązanie działa dla Pythona 2> = 2.6 i Pythona 3. Należy pamiętać, że nasz sys.stdout.write()akceptuje tylko ciągi znaków Unicode i sys.stdout.buffer.write()tylko ciągi bajtów. Może się tak nie zdarzyć w przypadku starego kodu, ale często ma to miejsce w przypadku kodu, który jest zbudowany tak, aby działał w Pythonie 2 i 3 bez zmian.

Jeśli potrzebujesz obsługiwać kod, który wysyła ciągi bajtów bezpośrednio na standardowe wyjście bez używania stdout.buffer, możesz użyć tej odmiany:

class StdoutBuffer(TextIOWrapper):
    def write(self, string):
        try:
            return super(StdoutBuffer, self).write(string)
        except TypeError:
            # redirect encoded byte strings directly to buffer
            return super(StdoutBuffer, self).buffer.write(string)

Nie musisz ustawiać kodowania bufora sys.stdout.encoding, ale jest to pomocne, gdy używasz tej metody do testowania / porównywania danych wyjściowych skryptu.

JonnyJD
źródło
3

Pytanie tutaj (przykład przekierowania wyjścia, a nie teeczęści) służy os.dup2do przekierowania strumienia na poziomie systemu operacyjnego. To miłe, ponieważ będzie miało zastosowanie również do poleceń, które tworzysz z programu.

Jeremiah Willcock
źródło
3

Myślę, że powinieneś spojrzeć na te cztery obiekty:

from test.test_support import captured_stdout, captured_output, \
    captured_stderr, captured_stdin

Przykład:

from writer import write

with captured_stdout() as stdout:
    write()
print stdout.getvalue().upper()

UPD: Jak powiedział Eric w komentarzu, nie powinno się ich używać bezpośrednio, więc skopiowałem i wkleiłem.

# Code from test.test_support:
import contextlib
import sys

@contextlib.contextmanager
def captured_output(stream_name):
    """Return a context manager used by captured_stdout and captured_stdin
    that temporarily replaces the sys stream *stream_name* with a StringIO."""
    import StringIO
    orig_stdout = getattr(sys, stream_name)
    setattr(sys, stream_name, StringIO.StringIO())
    try:
        yield getattr(sys, stream_name)
    finally:
        setattr(sys, stream_name, orig_stdout)

def captured_stdout():
    """Capture the output of sys.stdout:

       with captured_stdout() as s:
           print "hello"
       self.assertEqual(s.getvalue(), "hello")
    """
    return captured_output("stdout")

def captured_stderr():
    return captured_output("stderr")

def captured_stdin():
    return captured_output("stdin")
Oleksandr Fedorov
źródło
3

Podoba mi się rozwiązanie Contextmanager, ale jeśli potrzebujesz bufora przechowywanego z otwartym plikiem i obsługą plików, możesz zrobić coś takiego.

import six
from six.moves import StringIO


class FileWriteStore(object):
    def __init__(self, file_):
        self.__file__ = file_
        self.__buff__ = StringIO()

    def __getattribute__(self, name):
        if name in {
            "write", "writelines", "get_file_value", "__file__",
                "__buff__"}:
            return super(FileWriteStore, self).__getattribute__(name)
        return self.__file__.__getattribute__(name)

    def write(self, text):
        if isinstance(text, six.string_types):
            try:
                self.__buff__.write(text)
            except:
                pass
        self.__file__.write(text)

    def writelines(self, lines):
        try:
            self.__buff__.writelines(lines)
        except:
            pass
        self.__file__.writelines(lines)

    def get_file_value(self):
        return self.__buff__.getvalue()

posługiwać się

import sys
sys.stdout = FileWriteStore(sys.stdout)
print "test"
buffer = sys.stdout.get_file_value()
# you don't want to print the buffer while still storing
# else it will double in size every print
sys.stdout = sys.stdout.__file__
print buffer
Nathan Buckner
źródło
0

Oto kierownik kontekst wzorując @ JonnyJD za odpowiedź wspierający pisanie bajtów bufferatrybutów przylegają również wykorzystując Dunder-io referenes SYS koszulka dla dalszego uproszczenia.

import io
import sys
import contextlib


@contextlib.contextmanager
def capture_output():
    output = {}
    try:
        # Redirect
        sys.stdout = io.TextIOWrapper(io.BytesIO(), sys.stdout.encoding)
        sys.stderr = io.TextIOWrapper(io.BytesIO(), sys.stderr.encoding)
        yield output
    finally:
        # Read
        sys.stdout.seek(0)
        sys.stderr.seek(0)
        output['stdout'] = sys.stdout.read()
        output['stderr'] = sys.stderr.read()
        sys.stdout.close()
        sys.stderr.close()

        # Restore
        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__


with capture_output() as output:
    print('foo')
    sys.stderr.buffer.write(b'bar')

print('stdout: {stdout}'.format(stdout=output['stdout']))
print('stderr: {stderr}'.format(stderr=output['stderr']))

Wynik to:

stdout: foo

stderr: bar
Ryne Everett
źródło