Obtener salida en tiempo real usando subproceso
-
03-07-2019 - |
Pregunta
Estoy tratando de escribir un script de envoltura para un programa de línea de comandos (svnadmin verificar) que mostrará un buen indicador de progreso para la operación. Esto requiere que yo pueda ver cada línea de salida del programa envuelto tan pronto como salga.
Pensé que simplemente ejecutaría el programa usando subprocess.Popen
, usaría 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 parámetro bufsize
en 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 entregaron las líneas.
En este punto estaba empezando 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 obtuvo 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*
)?
Solución
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 lo hace. Aparentemente, este es un error conocido: http://bugs.python.org/issue3907 (El problema es ahora " Cerrado " a partir del 29 de agosto de 2018)
Otros consejos
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1)
for line in iter(p.stdout.readline, b''):
print line,
p.stdout.close()
p.wait()
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.
Puede dirigir la salida del subproceso a las secuencias directamente. Ejemplo simplificado:
subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)
Me encontré con el mismo problema hace un tiempo. Mi solución fue deshacerme de la iteración para el método read
, que regresará de inmediato incluso si su subproceso no ha terminado de ejecutarse, etc.
Problema de salida en tiempo real resuelto: Encontré un problema similar en Python, mientras capturaba la salida en tiempo real del programa c. Agregué & Quot; fflush (stdout) ; & Quot; en mi código C. A mi me funciono. Aquí está el fragmento del código
< < Programa C & Gt; & Gt;
#include <stdio.h>
void main()
{
int count = 1;
while (1)
{
printf(" Count %d\n", count++);
fflush(stdout);
sleep(1);
}
}
< < Programa Python & Gt; & Gt;
#!/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 & Gt; & Gt; Imprimir: Cuenta 1 Imprimir: Cuenta 2 Imprimir: Cuenta 3
Espero que ayude.
~ sairam
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) desde el 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.")
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 argumento env
a Popen
.
De lo contrario, si está en Linux / Unix, puede usar la herramienta stdbuf
. P.ej. como:
cmd = ["stdbuf", "-oL"] + cmd
Vea también aquí sobre <=> u otras opciones.
(Consulte también aquí para obtener la misma respuesta).
El Streaming subproceso stdin y stdout con asyncio en Python publicación de blog 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())
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 la aplicación se está almacenando en la memoria intermedia, por lo tanto, no puede llegar a esa salida hasta que el búfer se llene o el proceso muera.
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()
Encontramos esto " plug-and-play " función aquí . ¡Funcionó 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)
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()
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()
(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()