Imprima constantemente la salida del subproceso mientras se ejecuta el proceso


202

Para iniciar programas desde mis scripts Python, estoy usando el siguiente método:

def execute(command):
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    output = process.communicate()[0]
    exitCode = process.returncode

    if (exitCode == 0):
        return output
    else:
        raise ProcessException(command, exitCode, output)

Entonces, cuando inicio un proceso como Process.execute("mvn clean install"), mi programa espera hasta que finalice el proceso, y solo entonces obtengo la salida completa de mi programa. Esto es molesto si estoy ejecutando un proceso que tarda un tiempo en finalizar.

¿Puedo dejar que mi programa escriba la salida del proceso línea por línea, sondeando la salida del proceso antes de que finalice en un bucle o algo así?

** [EDITAR] Lo siento, no busqué muy bien antes de publicar esta pregunta. Enhebrar es en realidad la clave. Encontré un ejemplo aquí que muestra cómo hacerlo: ** Subproceso de Python. Abrir desde un hilo


Hilo en lugar de subproceso, creo
Ant

9
No, no necesitas hilos. La idea completa de tuberías funciona porque puede obtener lectura / escritura de los procesos mientras se están ejecutando.
tokland

Respuestas:


264

Puede utilizar iter para procesar líneas Tan pronto como las salidas mandase: lines = iter(fd.readline, ""). Aquí hay un ejemplo completo que muestra un caso de uso típico (gracias a @jfs por ayudarnos):

from __future__ import print_function # Only Python 2.x
import subprocess

def execute(cmd):
    popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
    for stdout_line in iter(popen.stdout.readline, ""):
        yield stdout_line 
    popen.stdout.close()
    return_code = popen.wait()
    if return_code:
        raise subprocess.CalledProcessError(return_code, cmd)

# Example
for path in execute(["locate", "a"]):
    print(path, end="")

24
He probado este código (con un programa que tarda mucho tiempo en ejecutarse) y puedo confirmar que genera líneas a medida que se reciben, en lugar de esperar a que se complete la ejecución. Esta es la respuesta superior de la OMI.
Andrew Martin

11
Nota: En Python 3, puedes usar for line in popen.stdout: print(line.decode(), end=''). Para admitir Python 2 y 3, use bytes literales: de lo b''contrario, lines_iteratornunca termina en Python 3.
jfs

3
El problema con este enfoque es que si el proceso se detiene un poco sin escribir nada en stdout, no hay más entradas para leer. Necesitará un bucle para verificar si el proceso ha finalizado o no. Intenté esto usando subprocess32 en python 2.7
Har

77
deberia de funcionar. Para pulirlo, puede agregar bufsize=1(puede mejorar el rendimiento en Python 2), cerrar la popen.stdouttubería explícitamente (sin esperar a que la recolección de basura se encargue de ello) y subir subprocess.CalledProcessError(como check_call(), check_output()hacer). La printdeclaración es diferente en Python 2 y 3: podría usar el hack de softspace print line,(nota: coma) para evitar duplicar todas las nuevas líneas como lo hace su código y pasar universal_newlines=TruePython 3, para obtener texto en lugar de una respuesta relacionada con bytes .
jfs

66
@binzhang Eso no es un error, stdout está almacenado de manera predeterminada en los scripts de Python (también para muchas herramientas de Unix). Tratar execute(["python", "-u", "child_thread.py"]). Más información: stackoverflow.com/questions/14258500/…
tokland el

84

Ok, logré resolverlo sin hilos (cualquier sugerencia de por qué sería mejor usar hilos sería apreciada) usando un fragmento de esta pregunta Interceptando stdout de un subproceso mientras se está ejecutando

def execute(command):
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    # Poll process for new output until finished
    while True:
        nextline = process.stdout.readline()
        if nextline == '' and process.poll() is not None:
            break
        sys.stdout.write(nextline)
        sys.stdout.flush()

    output = process.communicate()[0]
    exitCode = process.returncode

    if (exitCode == 0):
        return output
    else:
        raise ProcessException(command, exitCode, output)

3
La fusión de código del tokland de ifischer y funciona bastante bien (que tenía que cambiar print line,de sys.stdout.write(nextline); sys.stdout.flush()lo contrario, sería imprimir cada dos líneas Por otra parte, esto está utilizando la interfaz portátil de IPython, así que tal vez algo más estaba sucediendo -.. En cualquier caso, una llamada explícita a flush()las obras.
eacousineau

3
señor eres mi salvavidas !! realmente extraño que este tipo de cosas no estén integradas en la biblioteca en sí ... porque si escribo cliapp, quiero mostrar todo lo que se procesa en bucle al instante ... s'rsly ...
holms

3
Esta solución puede ser modificado para imprimir constantemente tanto de salida y los errores? Si cambio stderr=subprocess.STDOUTa stderr=subprocess.PIPEy luego llamar process.stderr.readline()desde dentro del bucle, me parece chocar con el punto muerto que se le advierte sobre la documentación para el subprocessmódulo.
davidrmcharles

77
@DavidCharles Creo que lo que estás buscando es stdout=subprocess.PIPE,stderr=subprocess.STDOUTesto captura stderr, y creo (pero no lo he probado) que también captura stdin.
Andrew Martin

Gracias por esperar el código de salida. No sabía cómo resolverlo
Vitaly Isaev

68

Para imprimir la salida del subproceso línea por línea tan pronto como su búfer stdout se vacíe en Python 3:

from subprocess import Popen, PIPE, CalledProcessError

with Popen(cmd, stdout=PIPE, bufsize=1, universal_newlines=True) as p:
    for line in p.stdout:
        print(line, end='') # process line here

if p.returncode != 0:
    raise CalledProcessError(p.returncode, p.args)

Aviso: no es necesario p.poll(): el ciclo termina cuando se alcanza eof. Y no es necesario iter(p.stdout.readline, ''): el error de lectura anticipada se solucionó en Python 3.

Vea también, Python: lea la entrada de transmisión de subprocess.communicate () .


3
Esta solución funcionó para mí. La solución aceptada dada anteriormente simplemente siguió imprimiendo líneas en blanco para mí.
Nombre clave

3
Tuve que agregar sys.stdout.flush () para obtener impresiones de inmediato.
Nombre clave

3
@Codename: no debería necesitarlo sys.stdout.flush()en el padre: stdout tiene un buffer de línea si no se redirige a un archivo / tubería y, por lo tanto, la impresión linevacía el búfer automáticamente. No es necesario sys.stdout.flush()en el niño también: pase la -uopción de línea de comandos en su lugar.
jfs

1
@Codename: si quieres usarlo >, ejecuta python -u your-script.py > some-file. Aviso: -uopción que he mencionado anteriormente (no es necesario usar sys.stdout.flush()).
jfs

1
@mvidelgauz no necesita llamar: p.wait()se llama a la salida del withbloque. Uso p.returncode.
jfs

8

En realidad, hay una forma realmente simple de hacer esto cuando solo desea imprimir la salida:

import subprocess
import sys

def execute(command):
    subprocess.check_call(command, stdout=sys.stdout, stderr=subprocess.STDOUT)

Aquí simplemente estamos apuntando el subproceso a nuestra propia stdout, y usando la API de éxito o excepción existente.


1
Esta solución es más simple y limpia que la solución de @ tokland, para Python 3.6. Noté que el argumento shell = True no es necesario.
Buena voluntad

Buena captura, buena voluntad. Eliminadoshell=True
Andrew Ring

Muy astuto, y funciona perfectamente con poco código. ¿Quizás debería redirigir el subproceso stderr a sys.stderr también?
Manu

Manu ciertamente puedes. No lo hice, porque el intento en la pregunta estaba redirigiendo stderr a stdout.
Andrew Ring

¿Puedes explicar cuál es la diferencia entre sys.stdout y subprocess.STDOUT?
Ron Serruya

7

@tokland

probé su código y lo corregí para 3.4 y windows dir.cmd es un comando dir simple, guardado como archivo cmd

import subprocess
c = "dir.cmd"

def execute(command):
    popen = subprocess.Popen(command, stdout=subprocess.PIPE,bufsize=1)
    lines_iterator = iter(popen.stdout.readline, b"")
    while popen.poll() is None:
        for line in lines_iterator:
            nline = line.rstrip()
            print(nline.decode("latin"), end = "\r\n",flush =True) # yield line

execute(c)

3
Podrías simplificar tu código . iter()y end='\r\n'son innecesarios Python usa el modo universal de líneas nuevas de forma predeterminada, '\n'es decir, cualquiera se traduce '\r\n'durante la impresión. 'latin'probablemente sea una codificación incorrecta, podría usarla universal_newlines=Truepara obtener salida de texto en Python 3 (decodificada usando la codificación preferida de la configuración regional). No se detenga .poll(), podría haber datos no leídos almacenados en el búfer. Si la secuencia de comandos de Python se ejecuta en una consola, su salida tiene un buffer de línea; puede forzar el almacenamiento en línea usando la -uopción: no es necesario flush=Trueaquí.
jfs

4

En caso de que alguien quiera leer de ambos stdouty stderral mismo tiempo usar hilos, esto es lo que se me ocurrió:

import threading
import subprocess
import Queue

class AsyncLineReader(threading.Thread):
    def __init__(self, fd, outputQueue):
        threading.Thread.__init__(self)

        assert isinstance(outputQueue, Queue.Queue)
        assert callable(fd.readline)

        self.fd = fd
        self.outputQueue = outputQueue

    def run(self):
        map(self.outputQueue.put, iter(self.fd.readline, ''))

    def eof(self):
        return not self.is_alive() and self.outputQueue.empty()

    @classmethod
    def getForFd(cls, fd, start=True):
        queue = Queue.Queue()
        reader = cls(fd, queue)

        if start:
            reader.start()

        return reader, queue


process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdoutReader, stdoutQueue) = AsyncLineReader.getForFd(process.stdout)
(stderrReader, stderrQueue) = AsyncLineReader.getForFd(process.stderr)

# Keep checking queues until there is no more output.
while not stdoutReader.eof() or not stderrReader.eof():
   # Process all available lines from the stdout Queue.
   while not stdoutQueue.empty():
       line = stdoutQueue.get()
       print 'Received stdout: ' + repr(line)

       # Do stuff with stdout line.

   # Process all available lines from the stderr Queue.
   while not stderrQueue.empty():
       line = stderrQueue.get()
       print 'Received stderr: ' + repr(line)

       # Do stuff with stderr line.

   # Sleep for a short time to avoid excessive CPU use while waiting for data.
   sleep(0.05)

print "Waiting for async readers to finish..."
stdoutReader.join()
stderrReader.join()

# Close subprocess' file descriptors.
process.stdout.close()
process.stderr.close()

print "Waiting for process to exit..."
returnCode = process.wait()

if returnCode != 0:
   raise subprocess.CalledProcessError(returnCode, command)

Solo quería compartir esto, ya que terminé con esta pregunta tratando de hacer algo similar, pero ninguna de las respuestas resolvió mi problema. ¡Ojalá ayude a alguien!

Tenga en cuenta que en mi caso de uso, un proceso externo mata el proceso que nosotros Popen().


1
Tuve que usar algo casi exactamente como esto para python2. Si bien algo como esto debería haberse proporcionado en python2, no es así que algo como esto esté absolutamente bien.
Stuart Axon

3

Para cualquiera que intente las respuestas a esta pregunta para obtener el stdout de una secuencia de comandos de Python, tenga en cuenta que Python amortigua su stdout y, por lo tanto, puede tomar un tiempo ver el stdout.

Esto se puede rectificar agregando lo siguiente después de cada escritura estándar en el script de destino:

sys.stdout.flush()

1
Pero ejecutar Python como un subproceso de Python es una locura en primer lugar. Su guión debería ser simplemente importel otro guión; investigue multiprocessingo threadingsi necesita ejecución paralela.
tripleee

3
@triplee Hay varios escenarios en los que es apropiado ejecutar Python como un subproceso de Python. Tengo varias secuencias de comandos por lotes de Python que deseo ejecutar de forma secuencial, diariamente. Estos pueden ser orquestados por un script maestro de Python que inicia la ejecución y me envía un correo electrónico si el script secundario falla. Cada secuencia de comandos está aislada del otro, sin conflictos de nombres. No estoy paralelizando, por lo que el multiprocesamiento y el subprocesamiento no son relevantes.
user1379351

También puede iniciar el otro programa de Python usando un ejecutable de Python diferente al que se ejecuta en el programa de Python principal, por ejemplo,subprocess.run("/path/to/python/executable", "pythonProgramToRun.py")
Kyle Bridenstine

3

En Python> = 3.5 usando subprocess.runfunciona para mí:

import subprocess

cmd = 'echo foo; sleep 1; echo foo; sleep 2; echo foo'
subprocess.run(cmd, shell=True)

(obtener el resultado durante la ejecución también funciona sin shell=True) https://docs.python.org/3/library/subprocess.html#subprocess.run


2
Esto no es "durante la ejecución". La subprocess.run()llamada solo regresa cuando el subproceso ha terminado de ejecutarse.
tripleee

1
¿Puedes explicar cómo no es "durante la ejecución"? Algo así >>> import subprocess; subprocess.run('top')también parece imprimirse "durante la ejecución" (y la parte superior nunca termina). ¿Quizás no estoy captando alguna diferencia sutil?
user7017793

Si redirige la salida de nuevo a Python, por ejemplo stdout=subprocess.PIPE, solo puede leerla después de topfinalizar. Su programa Python se bloquea durante la ejecución del subproceso.
tripleee

1
Bien, eso tiene sentido. El runmétodo aún funciona si solo está interesado en ver la salida a medida que se genera. Si desea hacer algo con la salida en python de forma asincrónica, tiene razón en que no funciona.
user7017793

3

Para responder a la pregunta original, la mejor manera en que IMO es simplemente redirigiendo el subproceso stdoutdirectamente a su programa stdout(opcionalmente, se puede hacer lo mismo stderr, como en el ejemplo a continuación)

p = Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
p.communicate()

3
No especifica nada para stdouty stderrhace lo mismo con menos código. Aunque supongo que explícito es mejor que implícito.
tripleee

1

Este PoC lee constantemente la salida de un proceso y se puede acceder cuando sea necesario. Solo se mantiene el último resultado, todos los demás resultados se descartan, por lo tanto, evita que el PIPE se quede sin memoria:

import subprocess
import time
import threading
import Queue


class FlushPipe(object):
    def __init__(self):
        self.command = ['python', './print_date.py']
        self.process = None
        self.process_output = Queue.LifoQueue(0)
        self.capture_output = threading.Thread(target=self.output_reader)

    def output_reader(self):
        for line in iter(self.process.stdout.readline, b''):
            self.process_output.put_nowait(line)

    def start_process(self):
        self.process = subprocess.Popen(self.command,
                                        stdout=subprocess.PIPE)
        self.capture_output.start()

    def get_output_for_processing(self):
        line = self.process_output.get()
        print ">>>" + line


if __name__ == "__main__":
    flush_pipe = FlushPipe()
    flush_pipe.start_process()

    now = time.time()
    while time.time() - now < 10:
        flush_pipe.get_output_for_processing()
        time.sleep(2.5)

    flush_pipe.capture_output.join(timeout=0.001)
    flush_pipe.process.kill()

print_date.py

#!/usr/bin/env python
import time

if __name__ == "__main__":
    while True:
        print str(time.time())
        time.sleep(0.01)

salida: puede ver claramente que solo hay una salida de ~ 2.5s de intervalo, nada en el medio.

>>>1520535158.51
>>>1520535161.01
>>>1520535163.51
>>>1520535166.01

0

Esto funciona al menos en Python3.4

import subprocess

process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE)
for line in process.stdout:
    print(line.decode().strip())

1
Esto tiene el problema de que bloquea en el bucle hasta que el proceso haya terminado de ejecutarse.
tripleee

0

Ninguna de las respuestas aquí abordó todas mis necesidades.

  1. Sin hilos para stdout (sin colas, etc.)
  2. Sin bloqueo ya que necesito verificar si hay otras cosas sucediendo
  3. Use PIPE como necesitaba para hacer varias cosas, por ejemplo, salida de flujo, escribir en un archivo de registro y devolver una copia de cadena de la salida.

Un poco de historia: estoy usando un ThreadPoolExecutor para administrar un grupo de subprocesos, cada uno de los cuales ejecuta un subproceso y los ejecuta simultáneamente. (En Python2.7, pero esto también debería funcionar en la versión 3.x más reciente). No quiero usar subprocesos solo para la recopilación de salida, ya que quiero tantos disponibles como sea posible para otras cosas (un grupo de 20 procesos usaría 40 subprocesos solo para ejecutarse; 1 para el subproceso de proceso y 1 para stdout ... y más si quieres stderr supongo)

Estoy eliminando muchas excepciones y tal aquí, así que esto se basa en el código que funciona en la producción. Espero no haberlo arruinado en copiar y pegar. Además, ¡los comentarios son bienvenidos!

import time
import fcntl
import subprocess
import time

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

# Make stdout non-blocking when using read/readline
proc_stdout = proc.stdout
fl = fcntl.fcntl(proc_stdout, fcntl.F_GETFL)
fcntl.fcntl(proc_stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK)

def handle_stdout(proc_stream, my_buffer, echo_streams=True, log_file=None):
    """A little inline function to handle the stdout business. """
    # fcntl makes readline non-blocking so it raises an IOError when empty
    try:
        for s in iter(proc_stream.readline, ''):   # replace '' with b'' for Python 3
            my_buffer.append(s)

            if echo_streams:
                sys.stdout.write(s)

            if log_file:
                log_file.write(s)
    except IOError:
        pass

# The main loop while subprocess is running
stdout_parts = []
while proc.poll() is None:
    handle_stdout(proc_stdout, stdout_parts)

    # ...Check for other things here...
    # For example, check a multiprocessor.Value('b') to proc.kill()

    time.sleep(0.01)

# Not sure if this is needed, but run it again just to be sure we got it all?
handle_stdout(proc_stdout, stdout_parts)

stdout_str = "".join(stdout_parts)  # Just to demo

Estoy seguro de que aquí se agrega una sobrecarga, pero no es una preocupación en mi caso. Funcionalmente hace lo que necesito. Lo único que no he resuelto es por qué esto funciona perfectamente para los mensajes de registro, pero veo que algunos printmensajes aparecen más tarde y de una vez.


-2

En Python 3.6 usé esto:

import subprocess

cmd = "command"
output = subprocess.call(cmd, shell=True)
print(process)

1
Esta no es una respuesta a esta pregunta en particular. Esperar a que el subproceso termine antes de obtener su salida es específica y precisamente lo que el OP está tratando de evitar. La antigua función heredada subprocess.call()tiene algunas verrugas que son reparadas por funciones más nuevas; en Python 3.6 generalmente lo usarías subprocess.run()para esto; Por conveniencia, la función de contenedor más antigua subprocess.check_output()también está disponible: devuelve la salida real del proceso (este código devolvería solo el código de salida, pero incluso imprimirá algo indefinido).
tripleee
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.