Obtener salida en tiempo real usando subproceso

135

Estoy tratando de escribir un script de contenedor para un programa de línea de comando (svnadmin verificar) que mostrará un buen indicador de progreso para la operación. Esto requiere que pueda ver cada línea de salida del programa envuelto tan pronto como salga.

Pensé que simplemente ejecutaría el programa usando subprocess.Popen, use stdout=PIPE, luego leería cada línea tal como venía y actuaría en consecuencia. Sin embargo, cuando ejecuté el siguiente código, la salida parecía estar almacenada en algún lugar, haciendo que apareciera en dos fragmentos, líneas 1 a 332, luego 333 a 439 (la última línea de salida)

from subprocess import Popen, PIPE, STDOUT

p = Popen('svnadmin verify /var/svn/repos/config', stdout = PIPE, 
        stderr = STDOUT, shell = True)
for line in p.stdout:
    print line.replace('\n', '')

Después de mirar un poco la documentación sobre el subproceso, descubrí el bufsizeparámetro Popen, así que intenté establecer bufsize en 1 (buffer cada línea) y 0 (sin buffer), pero ninguno de los valores parecía cambiar la forma en que se entregaban las líneas.

En este punto, comenzaba a comprender las pajillas, así que escribí el siguiente ciclo de salida:

while True:
    try:
        print p.stdout.next().replace('\n', '')
    except StopIteration:
        break

pero obtuve el mismo resultado.

¿Es posible obtener la salida del programa 'en tiempo real' de un programa ejecutado usando un subproceso? ¿Hay alguna otra opción en Python que sea compatible con versiones anteriores (no exec*)?

Chris Lieb
fuente
1
¿Has intentado omitir sydout=PIPEpara que el subproceso escriba directamente en tu consola, sin pasar por el proceso principal?
S.Lott
55
El caso es que quiero leer el resultado. Si se envía directamente a la consola, ¿cómo podría hacer eso? Además, no quiero que el usuario vea la salida del programa envuelto, solo mi salida.
Chris Lieb
Entonces, ¿por qué una pantalla "en tiempo real"? No entiendo el caso de uso.
S.Lott
8
No uses shell = True. Inútilmente invoca tu caparazón. Utilice p = Popen (['svnadmin', 'verificar', '/ var / svn / repos / config'], stdout = PIPE, stderr = STDOUT) en su lugar
nosklo
2
@ S.Lott Básicamente, svnadmin verified imprime una línea de salida para cada revisión que se verifica. Quería hacer un buen indicador de progreso que no causara cantidades excesivas de salida. Como wget, por ejemplo
Chris Lieb

Respuestas:

82

Intenté esto, y por alguna razón mientras el código

for line in p.stdout:
  ...

amortigua agresivamente, la variante

while True:
  line = p.stdout.readline()
  if not line: break
  ...

no. Aparentemente, este es un error conocido: http://bugs.python.org/issue3907 (El problema ahora está "cerrado" a partir del 29 de agosto de 2018)

Dave
fuente
Este no es el único desastre en las implementaciones antiguas de Python IO. Es por eso que Py2.6 y Py3k terminaron con una biblioteca IO completamente nueva.
Tim Lin el
3
Este código se romperá si el subproceso devuelve una línea vacía. Una mejor solución sería usar en while p.poll() is Nonelugar de while True, y eliminar elif not line
exhuma
66
@exhuma: funciona bien. readline devuelve "\ n" en una línea vacía, que no se evalúa como verdadera. solo devuelve una cadena vacía cuando se cierra la tubería, que será cuando finalice el subproceso.
Alice Purcell
1
@Dave Para futuras referencias: imprima líneas utf-8 en py2 + con print(line.decode('utf-8').rstrip()).
Jonathan Komar
3
Además, para tener una lectura real en tiempo real de la salida del proceso, deberá decirle a Python que NO desea ningún almacenamiento en búfer. Estimado Python, solo dame la salida directamente. Y así es cómo: debe establecer la variable de entorno PYTHONUNBUFFERED=1. Esto es especialmente útil para salidas que son infinitas
George Pligoropoulos
38
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1)
for line in iter(p.stdout.readline, b''):
    print line,
p.stdout.close()
p.wait()
Corey Goldberg
fuente
1
@nbro probablemente porque p.stdout.close()no está claro.
anatoly techtonik
1
@nbro probablemente porque el código se dio sin explicación ...: /
Aaron Hall
3
¿De qué se trata esta b?
ManuelSchneid3r
29

Puede dirigir la salida del subproceso a las secuencias directamente. Ejemplo simplificado:

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)
Aidan Feldman
fuente
¿Esto le permite obtener también el contenido después del hecho .communicate()? ¿O los contenidos se pierden en las secuencias stderr / stdout padre?
theferrit32
No, no hay communicate()método en la devolución CompletedProcess. Además, capture_outputes mutuamente exclusivo con stdouty stderr.
Aidan Feldman
20

Puedes probar esto:

import subprocess
import sys

process = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

while True:
    out = process.stdout.read(1)
    if out == '' and process.poll() != None:
        break
    if out != '':
        sys.stdout.write(out)
        sys.stdout.flush()

Si usa readline en lugar de read, habrá algunos casos en los que el mensaje de entrada no se imprime. Pruébelo con un comando que requiera una entrada en línea y compruébelo usted mismo.

Nadia Alramli
fuente
Sí, el uso de readline () dejará de imprimir (incluso llamando a sys.stdout.flush ())
Mark Ma
3
¿Se supone que esto cuelga indefinidamente? Desearía que una solución dada también incluyera un código repetitivo para editar el bucle cuando finalice el subproceso inicial. Lo siento, no importa cuántas veces lo investigue, el subproceso, etcétera, es algo que nunca puedo poner a trabajar.
ThorSummoner
1
¿Por qué probar '' cuando en Python podemos usar si no fuera?
Greg Bell
2
Esta es la mejor solución para trabajos de larga duración. pero debe usar no is None and not! = None. No deberías usar! = Con Ninguno.
Cari
¿Stderr también se muestra con esto?
Pieter Vogelaar
7

El subproceso Streaming stdin y stdout con asyncio en la publicación de blog de Python de Kevin McCarthy muestra cómo hacerlo con asyncio:

import asyncio
from asyncio.subprocess import PIPE
from asyncio import create_subprocess_exec


async def _read_stream(stream, callback):
    while True:
        line = await stream.readline()
        if line:
            callback(line)
        else:
            break


async def run(command):
    process = await create_subprocess_exec(
        *command, stdout=PIPE, stderr=PIPE
    )

    await asyncio.wait(
        [
            _read_stream(
                process.stdout,
                lambda x: print(
                    "STDOUT: {}".format(x.decode("UTF8"))
                ),
            ),
            _read_stream(
                process.stderr,
                lambda x: print(
                    "STDERR: {}".format(x.decode("UTF8"))
                ),
            ),
        ]
    )

    await process.wait()


async def main():
    await run("docker build -t my-docker-image:latest .")


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
Pablo
fuente
Esto funciona con una ligera modificación al código publican
Jeef
Hola @Jeef, ¿puedes señalar la solución para que pueda actualizar la respuesta?
Pablo
Hola, eso funcionó para mí, pero tuve que agregar lo siguiente para deshacerme de algunos mensajes de error: import nest_asyncio; nest_asyncio.apply()y usar el comando de shell, es decir, en process = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)lugar de process = await create_subprocess_exec(...). ¡Salud!
user319436
4

Problema de salida en tiempo real resuelto: Encontré un problema similar en Python, mientras capturaba la salida en tiempo real del programa c. Agregué " fflush (stdout) "; en mi código C. Funcionó para mi. Aquí está el fragmento del código

<< Programa C >>

#include <stdio.h>
void main()
{
    int count = 1;
    while (1)
    {
        printf(" Count  %d\n", count++);
        fflush(stdout);
        sleep(1);
    }
}

<< Programa Python >>

#!/usr/bin/python

import os, sys
import subprocess


procExe = subprocess.Popen(".//count", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

while procExe.poll() is None:
    line = procExe.stdout.readline()
    print("Print:" + line)

<< SALIDA >> Imprimir: Cuenta 1 Imprimir: Cuenta 2 Imprimir: Cuenta 3

Espero eso ayude.

~ sairam

sairam
fuente
1
Esto fue lo único que realmente ayudó. Usé el mismo código ( flush(stdout)) en C ++. ¡Gracias!
Gerhard Hagerer
Estaba teniendo el mismo problema con un script de Python que llamaba a otro script de Python como un subproceso. En las impresiones del subproceso, era necesario "flush" (print ("hola", flush = True) en python 3). Además, muchos ejemplos todavía son (2020) python 2, este es python 3, entonces +1
smajtkst
3

Me encontré con el mismo problema hace un tiempo. Mi solución fue deshacerme de la iteración del readmétodo, que regresará de inmediato incluso si su subproceso no ha terminado de ejecutarse, etc.

Eli Courtwright
fuente
3

Dependiendo del caso de uso, es posible que también desee deshabilitar el almacenamiento en búfer en el propio subproceso.

Si el subproceso será un proceso de Python, puede hacer esto antes de la llamada:

os.environ["PYTHONUNBUFFERED"] = "1"

O, alternativamente, pase esto en el envargumento aPopen .

De lo contrario, si está en Linux / Unix, puede usar la stdbufherramienta. Por ejemplo, como:

cmd = ["stdbuf", "-oL"] + cmd

Ver también aquí sobrestdbuf u otras opciones.

(Ver también aquí para la misma respuesta).

Albert
fuente
2

Utilicé esta solución para obtener resultados en tiempo real en un subproceso. Este ciclo se detendrá tan pronto como se complete el proceso, dejando de lado la necesidad de una declaración de interrupción o un posible ciclo infinito.

sub_process = subprocess.Popen(my_command, close_fds=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

while sub_process.poll() is None:
    out = sub_process.stdout.read(1)
    sys.stdout.write(out)
    sys.stdout.flush()
Jason Hedlund
fuente
55
¿Es posible que esto salga del bucle sin que el búfer stdout esté vacío?
jayjay
¡He buscado mucho una respuesta adecuada que no se haya bloqueado al finalizar! Encontré esto como una solución agregando if out=='': breakdespuésout = sub_process...
Sos
2

Encontré esta función "plug-and-play" aquí . ¡Trabajado como un encanto!

import subprocess

def myrun(cmd):
    """from http://blog.kagesenshi.org/2008/02/teeing-python-subprocesspopen-output.html
    """
    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    stdout = []
    while True:
        line = p.stdout.readline()
        stdout.append(line)
        print line,
        if line == '' and p.poll() != None:
            break
    return ''.join(stdout)
Deena
fuente
1
La adición de stderr=subprocess.STDOUTrealmente ayuda mucho en la captura de datos de transmisión. Lo estoy votando.
khan
1
La carne principal aquí parece provenir de la respuesta aceptada
tripleee
2

Puede usar un iterador sobre cada byte en la salida del subproceso. Esto permite la actualización en línea (las líneas que terminan con '\ r' sobrescriben la línea de salida anterior) del subproceso:

from subprocess import PIPE, Popen

command = ["my_command", "-my_arg"]

# Open pipe to subprocess
subprocess = Popen(command, stdout=PIPE, stderr=PIPE)


# read each byte of subprocess
while subprocess.poll() is None:
    for c in iter(lambda: subprocess.stdout.read(1) if subprocess.poll() is None else {}, b''):
        c = c.decode('ascii')
        sys.stdout.write(c)
sys.stdout.flush()

if subprocess.returncode != 0:
    raise Exception("The subprocess did not terminate correctly.")
rhyno183
fuente
2

En Python 3.x, el proceso puede bloquearse porque la salida es una matriz de bytes en lugar de una cadena. Asegúrate de decodificarlo en una cadena.

A partir de Python 3.6, puede hacerlo utilizando el parámetro encodingen Popen Constructor . El ejemplo completo:

process = subprocess.Popen(
    'my_command',
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    shell=True,
    encoding='utf-8',
    errors='replace'
)

while True:
    realtime_output = process.stdout.readline()

    if realtime_output == '' and process.poll() is not None:
        break

    if realtime_output:
        print(realtime_output.strip(), flush=True)

Tenga en cuenta que este código redirecciones stderr a stdouty controla los errores de salida .

pavelnazimok
fuente
1

El uso de pexpect [ http://www.noah.org/wiki/Pexpect ] con líneas de lectura sin bloqueo resolverá este problema. Se deriva del hecho de que las tuberías están almacenadas en búfer, por lo que la salida de su aplicación está siendo almacenada por la tubería, por lo tanto, no puede llegar a esa salida hasta que el búfer se llene o el proceso muera.

Gabe
fuente
0

Solución completa:

import contextlib
import subprocess

# Unix, Windows and old Macintosh end-of-line
newlines = ['\n', '\r\n', '\r']
def unbuffered(proc, stream='stdout'):
    stream = getattr(proc, stream)
    with contextlib.closing(stream):
        while True:
            out = []
            last = stream.read(1)
            # Don't loop forever
            if last == '' and proc.poll() is not None:
                break
            while last not in newlines:
                # Don't loop forever
                if last == '' and proc.poll() is not None:
                    break
                out.append(last)
                last = stream.read(1)
            out = ''.join(out)
            yield out

def example():
    cmd = ['ls', '-l', '/']
    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        # Make all end-of-lines '\n'
        universal_newlines=True,
    )
    for line in unbuffered(proc):
        print line

example()
Andres Restrepo
fuente
1
Dado que está utilizando universal_newlines=Truela Popen()llamada, probablemente no necesite poner su propio manejo de ellos, ese es el punto central de la opción.
Martineau
1
Parece innecesario complicado. No resuelve problemas de almacenamiento en búfer. Ver enlaces en mi respuesta .
jfs
¡Esta es la única forma en que podría obtener salida de progreso de rsync en tiempo real (- outbuf = L)! gracias
Mohammadhzp
0

Este es el esqueleto básico que siempre uso para esto. Facilita la implementación de tiempos de espera y es capaz de lidiar con los inevitables procesos de bloqueo.

import subprocess
import threading
import Queue

def t_read_stdout(process, queue):
    """Read from stdout"""

    for output in iter(process.stdout.readline, b''):
        queue.put(output)

    return

process = subprocess.Popen(['dir'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT,
                           bufsize=1,
                           cwd='C:\\',
                           shell=True)

queue = Queue.Queue()
t_stdout = threading.Thread(target=t_read_stdout, args=(process, queue))
t_stdout.daemon = True
t_stdout.start()

while process.poll() is None or not queue.empty():
    try:
        output = queue.get(timeout=.5)

    except Queue.Empty:
        continue

    if not output:
        continue

    print(output),

t_stdout.join()
Badslacks
fuente
0

(Esta solución ha sido probada con Python 2.7.15)
Solo necesita sys.stdout.flush () después de cada línea de lectura / escritura:

while proc.poll() is None:
    line = proc.stdout.readline()
    sys.stdout.write(line)
    # or print(line.strip()), you still need to force the flush.
    sys.stdout.flush()
dan
fuente
0

Pocas respuestas sugiriendo python 3.xo pthon 2.x, el siguiente código funcionará para ambos.

 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,)
    stdout = []
    while True:
        line = p.stdout.readline()
        if not isinstance(line, (str)):
            line = line.decode('utf-8')
        stdout.append(line)
        print (line)
        if (line == '' and p.poll() != None):
            break
Djai
fuente