Catturare lo stdout da uno script?


94

supponiamo che ci sia uno script che fa qualcosa del genere:

# module writer.py
import sys

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

Supponiamo ora di voler catturare l'output della writefunzione e memorizzarlo in una variabile per un'ulteriore elaborazione. La soluzione ingenua era:

# module mymodule.py
from writer import write

out = write()
print out.upper()

Ma questo non funziona. Ho trovato un'altra soluzione e funziona, ma per favore, fammi sapere se c'è un modo migliore per risolvere il problema. Grazie

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

Risposte:


51

L'impostazione stdoutè un modo ragionevole per farlo. Un altro è eseguirlo come un altro processo:

import subprocess

proc = subprocess.Popen(["python", "-c", "import writer; writer.write()"], stdout=subprocess.PIPE)
out = proc.communicate()[0]
print out.upper()

4
check_output acquisisce direttamente l'output di un comando eseguito in un sottoprocesso: <br> value = subprocess.check_output (command, shell = True)
Arthur

1
Versione formattata :value = subprocess.check_output(command, shell=True)
Nae

51

Ecco una versione del gestore di contesto del tuo codice. Restituisce un elenco di due valori; il primo è stdout, il secondo è 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'

Amo questa soluzione. Ho modificato, in modo da non perdere accidentalmente cose da uno stream su cui non mi aspetto output, ad esempio errori imprevisti. Nel mio caso, capture () può accettare sys.stderr o sys.stdout come parametro, indicando di catturare solo quel flusso.
Joshua Richardson

StringIO non supporta Unicode in alcun modo, in modo da poter integrare la risposta qui per fare le sopra supporto caratteri non ASCII: stackoverflow.com/a/1819009/425050
mafrosis

3
La modifica di un valore di resa in finalmente è davvero piuttosto strana - with capture() as out:si comporterà diversamente dawith capture() as out, err:
Eric

1
Il supporto Unicode / stdout.buffer può essere raggiunto utilizzando il modulo io. Vedi la mia risposta .
JonnyJD

3
Questa soluzione si interrompe se si utilizza subprocesse si reindirizza l'output a sys.stdout / stderr. Questo perché StringIOnon è un vero oggetto file e manca la fileno()funzione.
letmaik

47

Per i futuri visitatori: Python 3.4 contextlib fornisce questo direttamente (vedere la guida di Python contextlib ) tramite il redirect_stdoutgestore di contesto:

from contextlib import redirect_stdout
import io

f = io.StringIO()
with redirect_stdout(f):
    help(pow)
s = f.getvalue()

2
Questo non risolve il problema quando si tenta di scrivere su sys.stdout.buffer (come è necessario fare quando si scrivono i byte). StringIO non ha l'attributo buffer, mentre TextIOWrapper lo fa. Vedi la risposta di @JonnyJD.
tessitore

10

Questa è la controparte decoratore del mio codice originale.

writer.py rimane lo stesso:

import sys

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

mymodule.py leggermente viene modificato:

from writer import write as _write
from decorators import capture

@capture
def write():
    return _write()

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

Ed ecco il decoratore:

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

10

O forse usa funzionalità già presenti ...

from IPython.utils.capture import capture_output

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

c()

print c.stdout

9

A partire da Python 3 puoi anche usare sys.stdout.buffer.write()per scrivere stringhe di byte (già) codificate su stdout (vedi stdout in Python 3 ). Quando lo fai, l' StringIOapproccio semplice non funziona perché né sys.stdout.encodingsys.stdout.buffersarebbe disponibile.

A partire da Python 2.6 puoi utilizzare l' TextIOBaseAPI , che include gli attributi mancanti:

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())

Questa soluzione funziona per Python 2> = 2.6 e Python 3. Nota che il nostro sys.stdout.write()accetta solo stringhe Unicode e sys.stdout.buffer.write()accetta solo stringhe di byte. Questo potrebbe non essere il caso del vecchio codice, ma spesso è il caso del codice creato per essere eseguito su Python 2 e 3 senza modifiche.

Se è necessario supportare il codice che invia stringhe di byte direttamente a stdout senza utilizzare stdout.buffer, è possibile utilizzare questa variante:

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)

Non è necessario impostare la codifica del buffer sys.stdout.encoding, ma questo aiuta quando si utilizza questo metodo per testare / confrontare l'output dello script.


4

La domanda qui (l'esempio di come reindirizzare l'output, non la teeparte) utilizza os.dup2per reindirizzare un flusso a livello di sistema operativo. Questo è bello perché si applicherà anche ai comandi generati dal tuo programma.


4

Penso che dovresti guardare questi quattro oggetti:

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

Esempio:

from writer import write

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

UPD: Come ha detto Eric in un commento, non si dovrebbero usarli direttamente, quindi l'ho copiato e incollato.

# 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")

4

Mi piace la soluzione contextmanager, tuttavia se hai bisogno del buffer memorizzato con il file aperto e il supporto per file non puoi fare qualcosa del genere.

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()

uso

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

2

Ecco un gestore di contesto che si ispira alla risposta di @ JonnyJD che supporta la scrittura di byte negli bufferattributi e che sfrutta anche i riferimenti dunder-io di sys per un'ulteriore semplificazione.

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']))

L'output è:

stdout: foo

stderr: bar
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.