¿Cómo capturar la salida stdout de una llamada a una función de Python?

112

Estoy usando una biblioteca de Python que hace algo con un objeto.

do_something(my_object)

y lo cambia. Mientras lo hace, imprime algunas estadísticas en stdout, y me gustaría controlar esta información. La solución adecuada sería cambiar do_something()para devolver la información relevante,

out = do_something(my_object)

pero pasará un tiempo antes de que los desarrolladores do_something()lleguen a este tema. Como solución alternativa, pensé en analizar todo lo que se do_something()escribe en stdout.

¿Cómo puedo capturar la salida estándar entre dos puntos en el código, por ejemplo,

start_capturing()
do_something(my_object)
out = end_capturing()

?

Nico Schlömer
fuente
2
posible duplicado de la captura de resultados de dis.dis
Martijn Pieters
Mi respuesta en la pregunta vinculada también se aplica aquí.
Martijn Pieters
Traté de hacer eso una vez y la mejor respuesta que encontré fue: stackoverflow.com/a/3113913/1330293
elyase
La respuesta vinculada @elyase es una solución elegante
Pykler
Vea esta respuesta .
martineau

Respuestas:

183

Prueba este administrador de contexto:

from io import StringIO 
import sys

class Capturing(list):
    def __enter__(self):
        self._stdout = sys.stdout
        sys.stdout = self._stringio = StringIO()
        return self
    def __exit__(self, *args):
        self.extend(self._stringio.getvalue().splitlines())
        del self._stringio    # free up some memory
        sys.stdout = self._stdout

Uso:

with Capturing() as output:
    do_something(my_object)

output es ahora una lista que contiene las líneas impresas por la llamada a la función.

Uso avanzado:

Lo que puede no ser obvio es que esto se puede hacer más de una vez y los resultados concatenados:

with Capturing() as output:
    print('hello world')

print('displays on screen')

with Capturing(output) as output:  # note the constructor argument
    print('hello world2')

print('done')
print('output:', output)

Salida:

displays on screen                     
done                                   
output: ['hello world', 'hello world2']

Actualización : Se añaden redirect_stdout()a contextliben Python 3.4 (junto con redirect_stderr()). Así que podría usarlo io.StringIOpara lograr un resultado similar (aunque Capturingpodría decirse que ser una lista y un administrador de contexto es más conveniente).

un poco
fuente
¡Gracias! Y gracias por agregar la sección avanzada ... Originalmente usé una asignación de corte para pegar el texto capturado en la lista, luego me di un golpe en la cabeza y lo usé en su .extend()lugar para que pudiera usarse de forma concatenativa, tal como lo notó. :-)
poco todo
PD: Si se va a usar repetidamente, sugiero agregar un self._stringio.truncate(0)después de la self.extend()llamada en el __exit__()método para liberar parte de la memoria que tiene el _stringiomiembro.
martineau
25
Gran respuesta, gracias. Para Python 3, use en from io import StringIOlugar de la primera línea en el administrador de contexto.
Wtower
1
¿Es esto seguro para subprocesos? ¿Qué sucede si algún otro hilo / llamada usa print () mientras se ejecuta do_something?
Derorrist
1
Esta respuesta no funcionará para la salida de bibliotecas compartidas de C, consulte esta respuesta en su lugar.
craymichael
81

En python> = 3.4, contextlib contiene un redirect_stdoutdecorador. Se puede usar para responder a su pregunta de la siguiente manera:

import io
from contextlib import redirect_stdout

f = io.StringIO()
with redirect_stdout(f):
    do_something(my_object)
out = f.getvalue()

De los documentos :

Administrador de contexto para redirigir temporalmente sys.stdout a otro archivo u objeto similar a un archivo.

Esta herramienta agrega flexibilidad a las funciones o clases existentes cuya salida está cableada a stdout.

Por ejemplo, la salida de help () normalmente se envía a sys.stdout. Puede capturar esa salida en una cadena redirigiendo la salida a un objeto io.StringIO:

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

Para enviar la salida de help () a un archivo en el disco, redirija la salida a un archivo normal:

 with open('help.txt', 'w') as f:
     with redirect_stdout(f):
         help(pow)

Para enviar la salida de help () a sys.stderr:

with redirect_stdout(sys.stderr):
    help(pow)

Tenga en cuenta que el efecto secundario global en sys.stdout significa que este administrador de contexto no es adecuado para su uso en código de biblioteca y la mayoría de aplicaciones con subprocesos. Tampoco tiene ningún efecto sobre la salida de los subprocesos. Sin embargo, sigue siendo un enfoque útil para muchos scripts de utilidades.

Este administrador de contexto es reentrante.

ForeverWintr
fuente
cuando se prueba f = io.StringIO() with redirect_stdout(f): logger = getLogger('test_logger') logger.debug('Test debug message') out = f.getvalue() self.assertEqual(out, 'DEBUG:test_logger:Test debug message'). Me da un error:AssertionError: '' != 'Test debug message'
Eziz Durdyyev
lo que significa que hice algo mal o no pude detectar el registro de salida estándar.
Eziz Durdyyev
@EzizDurdyyev, logger.debugno escribe en stdout por defecto. Si reemplaza su registro de llamadas con print(), debería ver el mensaje.
ForeverWintr
Sí, lo sé, pero yo sí hago escribir en la salida estándar de este modo: stream_handler = logging.StreamHandler(sys.stdout). Y agregue ese controlador a mi registrador. así que debería escribir en stdout y redirect_stdoutdebería capturarlo, ¿verdad?
Eziz Durdyyev
Sospecho que el problema está en la forma en que ha configurado su registrador. Verificaría que se imprime en stdout sin redirect_stdout. Si lo hace, tal vez el búfer no se vacíe hasta que salga el administrador de contexto.
ForeverWintr
0

Aquí hay una solución asincrónica que usa canalizaciones de archivos.

import threading
import sys
import os

class Capturing():
    def __init__(self):
        self._stdout = None
        self._stderr = None
        self._r = None
        self._w = None
        self._thread = None
        self._on_readline_cb = None

    def _handler(self):
        while not self._w.closed:
            try:
                while True:
                    line = self._r.readline()
                    if len(line) == 0: break
                    if self._on_readline_cb: self._on_readline_cb(line)
            except:
                break

    def print(self, s, end=""):
        print(s, file=self._stdout, end=end)

    def on_readline(self, callback):
        self._on_readline_cb = callback

    def start(self):
        self._stdout = sys.stdout
        self._stderr = sys.stderr
        r, w = os.pipe()
        r, w = os.fdopen(r, 'r'), os.fdopen(w, 'w', 1)
        self._r = r
        self._w = w
        sys.stdout = self._w
        sys.stderr = self._w
        self._thread = threading.Thread(target=self._handler)
        self._thread.start()

    def stop(self):
        self._w.close()
        if self._thread: self._thread.join()
        self._r.close()
        sys.stdout = self._stdout
        sys.stderr = self._stderr

Uso de ejemplo:

from Capturing import *
import time

capturing = Capturing()

def on_read(line):
    # do something with the line
    capturing.print("got line: "+line)

capturing.on_readline(on_read)
capturing.start()
print("hello 1")
time.sleep(1)
print("hello 2")
time.sleep(1)
print("hello 3")
capturing.stop()
miXo
fuente