Stampa costantemente l'output del sottoprocesso mentre il processo è in esecuzione


202

Per avviare programmi dai miei script Python, sto usando il seguente metodo:

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)

Quindi, quando lancio un processo come Process.execute("mvn clean install"), il mio programma attende fino a quando il processo è terminato, e solo allora ottengo l'output completo del mio programma. Questo è fastidioso se sto eseguendo un processo che richiede del tempo per terminare.

Posso consentire al mio programma di scrivere l'output del processo riga per riga, eseguendo il polling dell'output del processo prima che termini in un ciclo o qualcosa del genere?

** [EDIT] Mi dispiace non aver cercato molto bene prima di pubblicare questa domanda. Il threading è in realtà la chiave. Abbiamo trovato un esempio qui che mostra come farlo: ** Python Subprocess.Popen da un thread


Discussione invece di sottoprocesso, penso
Ant

9
No, non hai bisogno di discussioni. L'intera idea di piping funziona perché puoi ottenere lettura / scrittura dai processi mentre sono in esecuzione.
tokland

Risposte:


264

È possibile utilizzare iter per elaborare linee come appena uscite di comando li: lines = iter(fd.readline, ""). Ecco un esempio completo che mostra un tipico caso d'uso (grazie a @jfs per l'aiuto):

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
Ho provato questo codice (con un programma che richiede molto tempo per essere eseguito) e posso confermare che emette le linee man mano che vengono ricevute, piuttosto che attendere il completamento dell'esecuzione. Questa è la risposta superiore imo.
Andrew Martin,

11
Nota: in Python 3, è possibile utilizzare for line in popen.stdout: print(line.decode(), end=''). Per supportare sia Python 2 che 3, usa i byte letterali: b''altrimenti lines_iteratornon finisce mai su Python 3.
jfs

3
Il problema con questo approccio è che se il processo si interrompe per un po 'senza scrivere nulla su stdout, non c'è più input da leggere. Sarà necessario un ciclo per verificare se il processo è terminato o meno. Ho provato questo usando subprocess32 su Python 2.7
Har

7
dovrebbe funzionare. Per lucidarlo, è possibile aggiungere bufsize=1(può migliorare le prestazioni su Python 2), chiudere popen.stdoutesplicitamente la pipe (senza aspettare che la garbage collection si prenda cura di essa) e rilanciare subprocess.CalledProcessError(come check_call(), check_output()fare). L' printaffermazione è diversa su Python 2 e 3: potresti usare l'hack del softspace print line,(nota: virgola) per evitare di raddoppiare tutte le nuove righe come fa il tuo codice e passare universal_newlines=TruePython 3, per ottenere testo invece di byte - risposta relativa .
jfs,

6
@binzhang Non è un errore, stdout è bufferizzato di default sugli script Python (anche per molti strumenti Unix). Prova execute(["python", "-u", "child_thread.py"]). Maggiori informazioni: stackoverflow.com/questions/14258500/...
tokland

84

Ok, sono riuscito a risolverlo senza thread (qualsiasi suggerimento per cui sarebbe meglio usare i thread sarebbe apprezzato) usando uno snippet da questa domanda Intercettare lo stdout di un sottoprocesso mentre è in esecuzione

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
Unire il codice di ifischer e tokland funziona abbastanza bene (ho dovuto cambiare print line,in sys.stdout.write(nextline); sys.stdout.flush(). Altrimenti, verrebbe stampato ogni due righe. Quindi, questo sta usando l'interfaccia del Notebook di IPython, quindi forse stava succedendo qualcos'altro - indipendentemente, che chiamava esplicitamente flush()opere.
eacousineau,

3
signore, sei il mio salvavita !! davvero strano che questo tipo di cose non sono build-in nella stessa biblioteca .. perchè se scrivo cliapp, voglio mostrare tutto ciò che è la lavorazione a ciclo istantaneamente .. s'rsly ..
Holms

3
Questa soluzione può essere modificata per stampare costantemente sia l' output che gli errori? Se cambio stderr=subprocess.STDOUTa stderr=subprocess.PIPEe poi chiamo process.stderr.readline()dall'interno del loop, mi sembra di scappare dal vero deadlock che è avvertito nella documentazione per il subprocessmodulo.
davidrmcharles,

7
@DavidCharles Penso che ciò che stai cercando sia stdout=subprocess.PIPE,stderr=subprocess.STDOUTcatturare stderr e credo (ma non ho testato) che catturi anche lo stdin.
Andrew Martin,

grazie per aver aspettato il codice di uscita. Non sapevo come risolverlo
Vitaly Isaev

68

Per stampare l'output del sottoprocesso riga per riga non appena il buffer stdout viene scaricato in 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)

Avviso: non è necessario p.poll(): il loop termina quando viene raggiunta la eof. E non è necessario iter(p.stdout.readline, ''): il bug read-ahead è stato corretto in Python 3.

Vedi anche Python: leggi l'input di streaming da subprocess.communicate () .


3
Questa soluzione ha funzionato per me. La soluzione accettata di cui sopra ha continuato a stampare righe vuote per me.
Nome in codice

3
Ho dovuto aggiungere sys.stdout.flush () per ottenere immediatamente le stampe.
Nome in codice

3
@Codename: non dovresti aver bisogno sys.stdout.flush()nel genitore - stdout ha un buffer di linea se non viene reindirizzato a un file / pipe e quindi la stampa linescarica automaticamente il buffer. Non è necessario anche sys.stdout.flush()nel bambino: passa -uinvece l'opzione della riga di comando.
jfs,

1
@Codename: se si desidera utilizzare, >quindi eseguire python -u your-script.py > some-file. Avviso: -uopzione che ho menzionato sopra (non è necessario utilizzare sys.stdout.flush()).
jfs,

1
@mvidelgauz non è necessario chiamare: p.wait()viene chiamato all'uscita dal withblocco. Usa p.returncode.
jfs

8

In realtà c'è un modo davvero semplice per farlo quando vuoi solo stampare l'output:

import subprocess
import sys

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

Qui stiamo semplicemente puntando il sottoprocesso al nostro stdout e usando API di successo o di eccezione esistenti.


1
Questa soluzione è più semplice e più pulita della soluzione di @ tokland, per Python 3.6. Ho notato che l'argomento shell = True non è necessario.
Buona volontà

Buona cattura, buona volontà. Rimossoshell=True
Andrew Ring,

Molto astuto e funziona perfettamente con poco codice. Forse dovresti reindirizzare lo sottoprocesso stderr anche a sys.stderr?
Manu,

Manu certamente puoi. Non l'ho fatto, perché il tentativo nella domanda stava reindirizzando stderr a stdout.
Andrew Ring,

Puoi spiegare qual è la differenza tra sys.stdout e subprocess.STDOUT?
Ron Serruya,

7

@tokland

provato il tuo codice e corretto per 3.4 e windows dir.cmd è un semplice comando dir, salvato come file 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
potresti semplificare il tuo codice . iter()e end='\r\n'non sono necessari. Python utilizza la modalità newline universale per impostazione predefinita, ad esempio, qualsiasi '\n'viene tradotto '\r\n'durante la stampa. 'latin'è probabilmente una codifica errata, è possibile utilizzare universal_newlines=Trueper ottenere l'output del testo in Python 3 (decodificato utilizzando la codifica preferita della locale). Non fermarti .poll(), potrebbero esserci dati non letti nel buffer. Se lo script Python è in esecuzione in una console, il suo output è bufferizzato in linea; puoi forzare il buffering di linea usando l' -uopzione - non ti serve flush=Truequi.
jfs,

4

Nel caso in cui qualcuno voglia leggere da entrambi stdoute stderrallo stesso tempo usare i thread, questo è quello che mi è venuto in mente:

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)

Volevo solo condividere questo, poiché ho finito con questa domanda cercando di fare qualcosa di simile, ma nessuna delle risposte ha risolto il mio problema. Spero che aiuti qualcuno!

Si noti che nel mio caso d'uso, un processo esterno uccide il processo che noi Popen().


1
Ho dovuto usare qualcosa di esattamente simile a questo per Python2. Mentre qualcosa di simile avrebbe dovuto essere fornito in python2, non è così qualcosa di assolutamente perfetto.
Stuart Axon,

3

Per chiunque cerchi le risposte a questa domanda per ottenere lo stdout da uno script Python, nota che Python memorizza il suo stdout nel buffer, e quindi potrebbe essere necessario un po 'di tempo per vedere lo stdout.

Questo può essere corretto aggiungendo quanto segue dopo ogni scrittura stdout nello script di destinazione:

sys.stdout.flush()

1
Ma eseguire Python come sottoprocesso di Python è pazzo in primo luogo. La tua sceneggiatura dovrebbe semplicemente importl'altra sceneggiatura; esaminare multiprocessingo threadingse è necessaria un'esecuzione parallela.
Tripleee,

3
@triplee Esistono diversi scenari in cui è appropriato eseguire Python come sottoprocesso di Python. Ho un numero di script batch Python che vorrei eseguire in sequenza, quotidianamente. Questi possono essere orchestrati da uno script Python master che avvia l'esecuzione e mi invia un'email se lo script figlio non riesce. Ogni script è in modalità sandbox dall'altro, senza conflitti di denominazione. Non sto parallelizzando, quindi multiprocessing e threading non sono rilevanti.
user1379351

Potresti anche avviare l'altro programma Python usando un eseguibile Python diverso da quello su cui è in esecuzione il programma Python principale, ad esempio,subprocess.run("/path/to/python/executable", "pythonProgramToRun.py")
Kyle Bridenstine,

3

In Python> = 3.5 utilizzo subprocess.runfunziona per me:

import subprocess

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

(ottenere l'output durante l'esecuzione funziona anche senza shell=True) https://docs.python.org/3/library/subprocess.html#subprocess.run


2
Questo non è "durante l'esecuzione". La subprocess.run()chiamata ritorna solo quando il sottoprocesso ha terminato l'esecuzione.
Tripleee,

1
Puoi spiegare come non è "durante l'esecuzione"? Qualcosa del genere >>> import subprocess; subprocess.run('top')sembra anche stampare "durante l'esecuzione" (e la parte superiore non finisce mai). Forse non sto afferrando qualche sottile differenza?
user7017793

Se reindirizzi l'output su Python, ad esempio con stdout=subprocess.PIPEte, puoi leggerlo solo al toptermine. Il programma Python viene bloccato durante l'esecuzione del sottoprocesso.
triplo

1
Bene, ha senso. Il runmetodo funziona ancora se sei interessato solo a vedere l'output mentre viene generato. Se vuoi fare qualcosa con l'output in python in modo asincrono, hai ragione che non funziona.
user7017793

3

Per rispondere alla domanda originale, il modo migliore IMO è semplicemente reindirizzare i sottoprocessi stdoutdirettamente al programma stdout(facoltativamente, lo stesso può essere fatto stderr, come nell'esempio seguente)

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

3
Non specifica nulla per stdoute stderrfa la stessa cosa con meno codice. Anche se suppongo che esplicito sia meglio di implicito.
Tripleee,

1

Questo PoC legge costantemente l'output di un processo e può essere consultato quando necessario. Viene conservato solo l'ultimo risultato, tutti gli altri output vengono scartati, impedendo così al PIPE di crescere 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)

output: puoi vedere chiaramente che c'è solo un output nell'intervallo di ~ 2.5s.

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

0

Funziona almeno in Python3.4

import subprocess

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

1
Questo ha il problema che si blocca nel ciclo fino a quando il processo non ha terminato l'esecuzione.
triplo

0

Nessuna delle risposte qui ha risposto a tutte le mie esigenze.

  1. Nessun thread per stdout (nessuna coda, ecc.)
  2. Non bloccante in quanto ho bisogno di controllare altre cose in corso
  3. Usa PIPE come avevo bisogno per fare più cose, ad esempio l'output di flusso, scrivere in un file di registro e restituire una copia stringa dell'output.

Un po 'di storia: sto usando un ThreadPoolExecutor per gestire un pool di thread, ognuno dei quali avvia un sottoprocesso ed esegue la concorrenza. (In Python2.7, ma dovrebbe funzionare anche con la versione 3.x più recente). Non voglio usare i thread solo per la raccolta dell'output poiché ne voglio quanti più disponibili possibile per altre cose (un pool di 20 processi userebbe 40 thread solo per essere eseguito; 1 per il thread del processo e 1 per stdout ... e altro se vuoi stderr immagino)

Sto eliminando molte eccezioni e simili qui quindi questo si basa sul codice che funziona in produzione. Spero di non averlo rovinato nella copia e incolla. Inoltre, feedback molto gradito!

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

Sono sicuro che ci sia un sovraccarico da aggiungere qui, ma non è un problema nel mio caso. Funzionalmente fa quello che mi serve. L'unica cosa che non ho risolto è il motivo per cui questo funziona perfettamente per i messaggi di registro, ma vedo alcuni printmessaggi apparire più tardi e tutti in una volta.


-2

In Python 3.6 ho usato questo:

import subprocess

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

1
Questa non è una risposta a questa particolare domanda. Attendere il completamento del sottoprocesso prima di ottenere l'output è specificamente e precisamente ciò che l'OP sta cercando di evitare. La vecchia funzione legacy subprocess.call()ha alcune verruche che sono riparate da nuove funzioni; in Python 3.6 useresti generalmente subprocess.run()per questo; per comodità, subprocess.check_output()è ancora disponibile la funzione wrapper precedente : restituisce l'output effettivo del processo (questo codice restituisce solo il codice di uscita, ma stampa anche qualcosa di indefinito).
tripleee,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.