¿Capturar stdout de un script?

89

supongamos que hay un script que hace algo como esto:

# module writer.py
import sys

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

Ahora suponga que quiero capturar la salida de la writefunción y almacenarla en una variable para su posterior procesamiento. La solución ingenua fue:

# module mymodule.py
from writer import write

out = write()
print out.upper()

Pero esto no funciona. Se me ocurre otra solución y funciona, pero por favor, avíseme si hay una mejor manera de resolver el problema. Gracias

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
fuente

Respuestas:

49

La configuración stdoutes una forma razonable de hacerlo. Otro es ejecutarlo como otro proceso:

import subprocess

proc = subprocess.Popen(["python", "-c", "import writer; writer.write()"], stdout=subprocess.PIPE)
out = proc.communicate()[0]
print out.upper()
Matthew Flaschen
fuente
4
check_output captura directamente la salida de un comando que se ejecuta en un subproceso: <br> valor = subprocess.check_output (comando, shell = True)
Arthur
1
Versión formateada :value = subprocess.check_output(command, shell=True)
Nae
46

Aquí hay una versión de administrador de contexto de su código. Produce una lista de dos valores; el primero es stdout, el segundo es 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
fuente
Amo esta solución. Modifiqué, para no perder accidentalmente cosas de una secuencia en la que no esperaba resultados, por ejemplo, errores inesperados. En mi caso, capture () puede aceptar sys.stderr o sys.stdout como parámetro, lo que indica que solo se captura ese flujo.
Joshua Richardson
StringIO no es compatible con Unicode de ninguna manera, por lo que puede integrar la respuesta aquí para que los caracteres anteriores admitan caracteres no ASCII: stackoverflow.com/a/1819009/425050
mafrosis
2
La modificación de un valor dado en el fin es realmente bastante extraño - with capture() as out:se comportan de manera diferente awith capture() as out, err:
Eric
Se puede acceder a la compatibilidad con Unicode / stdout.buffer utilizando el módulo io. Mira mi respuesta .
JonnyJD
1
Esta solución se rompe si usa subprocessy redirige la salida a sys.stdout / stderr. Esto se debe a StringIOque no es un objeto de archivo real y pierde la fileno()función.
letmaik
44

Para futuros visitantes: Python 3.4 contextlib proporciona esto directamente (consulte la ayuda de Python contextlib ) a través del redirect_stdoutadministrador de contexto:

from contextlib import redirect_stdout
import io

f = io.StringIO()
with redirect_stdout(f):
    help(pow)
s = f.getvalue()
Nodosr
fuente
Esto no resuelve el problema al intentar escribir en sys.stdout.buffer (como debe hacer al escribir bytes). StringIO no tiene el atributo de búfer, mientras que TextIOWrapper sí. Vea la respuesta de @JonnyJD.
tejedor
9

Esta es la contraparte del decorador de mi código original.

writer.py sigue siendo el mismo:

import sys

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

mymodule.py ligeramente se modifica:

from writer import write as _write
from decorators import capture

@capture
def write():
    return _write()

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

Y aquí está el decorador:

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
fuente
9

O tal vez use una funcionalidad que ya está allí ...

from IPython.utils.capture import capture_output

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

c()

print c.stdout
dgrigonis
fuente
7

A partir de Python 3, también puede usar sys.stdout.buffer.write()para escribir (ya) cadenas de bytes codificadas en stdout (consulte stdout en Python 3 ). Cuando haces eso, el StringIOenfoque simple no funciona porque ni sys.stdout.encodingni sys.stdout.bufferestarían disponibles.

A partir de Python 2.6, puede usar la TextIOBaseAPI , que incluye los atributos que faltan:

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

Esta solución funciona para Python 2> = 2.6 y Python 3. Tenga en cuenta que nuestro sys.stdout.write()solo acepta cadenas Unicode y sys.stdout.buffer.write()solo acepta cadenas de bytes. Puede que este no sea el caso del código antiguo, pero a menudo es el caso del código creado para ejecutarse en Python 2 y 3 sin cambios.

Si necesita admitir código que envía cadenas de bytes a stdout directamente sin usar stdout.buffer, puede usar esta variación:

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)

No es necesario establecer la codificación del búfer sys.stdout.encoding, pero esto ayuda cuando se usa este método para probar / comparar la salida del script.

JonnyJD
fuente
3

La pregunta aquí (el ejemplo de cómo redirigir la salida, no la teeparte) se utiliza os.dup2para redirigir una transmisión a nivel de sistema operativo. Eso es bueno porque también se aplicará a los comandos que genere desde su programa.

Jeremías Willcock
fuente
3

Creo que deberías mirar estos cuatro objetos:

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

Ejemplo:

from writer import write

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

UPD: Como dijo Eric en un comentario, uno no debería usarlos directamente, así que lo copié y pegué.

# 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
fuente
3

Me gusta la solución contextmanager, sin embargo, si necesita el búfer almacenado con el archivo abierto y el soporte de fileno, puede hacer algo como esto.

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

utilizar

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
fuente
0

Aquí hay un administrador de contexto que se inspira en la respuesta de @ JonnyJD que admite la escritura de bytes en bufferatributos, pero que también aprovecha las referencias dunder-io de sys para una mayor simplificación.

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

La salida es:

stdout: foo

stderr: bar
Ryne Everett
fuente