cattura stdout in tempo reale da sottoprocesso


89

voglio subprocess.Popen() rsync.exe in Windows e stampare lo stdout in Python.

Il mio codice funziona, ma non rileva lo stato di avanzamento finché non viene completato il trasferimento di un file! Voglio stampare l'avanzamento di ogni file in tempo reale.

Usando Python 3.1 ora da quando ho sentito che dovrebbe essere migliore nella gestione dell'IO.

import subprocess, time, os, sys

cmd = "rsync.exe -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()


1
(Provenienti da google?) Tutti i PIPE si bloccheranno quando uno dei buffer dei PIPE viene riempito e non viene letto. es. stdout deadlock quando stderr è riempito. Non passare mai un TUBO che non intendi leggere.
Nasser Al-Wohaibi

Qualcuno potrebbe spiegare perché non è possibile impostare stdout su sys.stdout invece di subprocess.PIPE?
Mike

Risposte:


101

Alcune regole pratiche per subprocess.

  • Non usare maishell=True . Invoca inutilmente un processo di shell aggiuntivo per chiamare il programma.
  • Quando si chiamano processi, gli argomenti vengono passati come elenchi. sys.argvin python è una lista, e così è argvin C. Quindi passi una lista aPopen per chiamare sottoprocessi, non una stringa.
  • Non reindirizzare stderra un filePIPE quando non lo stai leggendo.
  • Non reindirizzare stdinquando non ci scrivi.

Esempio:

import subprocess, time, os, sys
cmd = ["rsync.exe", "-vaz", "-P", "source/" ,"dest/"]

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

for line in iter(p.stdout.readline, b''):
    print(">>> " + line.rstrip())

Detto questo, è probabile che rsync bufferizzi il suo output quando rileva che è connesso a una pipe invece che a un terminale. Questo è il comportamento predefinito: quando si è connessi a una pipe, i programmi devono svuotare esplicitamente lo stdout per ottenere risultati in tempo reale, altrimenti la libreria C standard verrà bufferizzata.

Per verificarlo, prova a eseguire questo invece:

cmd = [sys.executable, 'test_out.py']

e crea un test_out.pyfile con il contenuto:

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

L'esecuzione di quel sottoprocesso dovrebbe darti "Hello" e attendere 10 secondi prima di dare "World". Se ciò accade con il codice python sopra e non con rsync, significa che esso rsyncstesso esegue il buffering dell'output, quindi sei sfortunato.

Una soluzione sarebbe connettersi direttamente a un pty, usando qualcosa di simile pexpect.


12
shell=Falseè cosa giusta quando si costruisce la riga di comando soprattutto dai dati inseriti dall'utente. Tuttavia shell=Trueè anche utile quando si ottiene l'intera riga di comando da una fonte attendibile (ad es. Codificata nello script).
Denis Otkidach,

10
@Denis Otkidach: Non credo che ciò garantisca l'utilizzo di shell=True. Pensaci: stai invocando un altro processo sul tuo sistema operativo, che coinvolge l'allocazione della memoria, l'utilizzo del disco, la pianificazione del processore, solo per dividere una stringa ! E uno ti sei unito a te stesso !! Potresti dividere in Python, ma è comunque più facile scrivere ogni parametro separatamente. Inoltre, utilizzando una lista significa che non c'è bisogno di fuggire i caratteri speciali della shell: spazi, ;, >, <, &.. I suoi parametri possono contenere quei caratteri e non ci si deve preoccupare! Non vedo un motivo per usarlo shell=True, davvero, a meno che tu non stia eseguendo un comando di sola shell.
nosklo

nosklo, che dovrebbe essere: p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
Senthil Kumaran

1
@mathtick: non sono sicuro del motivo per cui dovresti eseguire queste operazioni come processi separati ... puoi tagliare il contenuto dei file ed estrarre facilmente il primo campo in Python usando il csvmodulo. Ma ad esempio, la tua pipeline in python sarebbe: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultNota che puoi lavorare con nomi di file lunghi e caratteri speciali della shell senza dover scappare, ora che la shell non è coinvolta. Inoltre è molto più veloce poiché c'è un processo in meno.
nosklo

11
usa for line in iter(p.stdout.readline, b'')invece che for line in p.stdoutin Python 2 altrimenti le righe non vengono lette in tempo reale anche se il processo sorgente non bufferizza il suo output.
jfs

43

So che questo è un vecchio argomento, ma ora c'è una soluzione. Chiama rsync con l'opzione --outbuf = L. Esempio:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())

3
Funziona e dovrebbe essere migliorato per evitare che i lettori futuri scorrano attraverso tutta la finestra di dialogo sopra.
VectorVictor

1
@VectorVictor Non spiega cosa sta succedendo e perché sta succedendo. Potrebbe essere che il tuo programma funzioni, fino a quando: 1. aggiungi preexec_fn=os.setpgrpper far sopravvivere il programma al suo script genitore 2. salti la lettura dalla pipe del processo 3. il processo genera molti dati, riempiendo la pipe 4. sei bloccato per ore , cercando di capire perché il programma che stai eseguendo si chiude dopo un periodo di tempo casuale . La risposta di @nosklo mi ha aiutato molto.
danuker

16

Su Linux, ho avuto lo stesso problema di sbarazzarmi del buffering. Alla fine ho usato "stdbuf -o0" (o, unbuffer da aspettarmi) per sbarazzarmi del buffering PIPE.

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

Potrei quindi usare select.select su stdout.

Vedi anche /unix/25372/


2
Per chiunque cerchi di acquisire lo stdout del codice C da Python, posso confermare che questa soluzione è stata l'unica che ha funzionato per me. Per essere chiari, sto parlando di aggiungere "stdbuf", "-o0" al mio elenco di comandi esistente in Popen.
Sconsiderato

Grazie! stdbuf -o0si è rivelato davvero utile con un mucchio di test pytest / pytest-bdd che ho scritto per generare un'app C ++ e verificare che emetta determinate istruzioni di log. Senza stdbuf -o0, questi test hanno richiesto 7 secondi per ottenere l'output (bufferizzato) dal programma C ++. Ora funzionano quasi istantaneamente!
evadeflow

Questa risposta mi ha salvato oggi! Eseguendo un'applicazione come sottoprocessi come parte di pytest, era impossibile per me ottenere il suo output. stdbuflo fa.
Janos

14

A seconda del caso d'uso, potresti anche voler disabilitare il buffering nel sottoprocesso stesso.

Se il sottoprocesso sarà un processo Python, potresti farlo prima della chiamata:

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

Oppure, in alternativa, passalo envnell'argomento aPopen .

Altrimenti, se sei su Linux / Unix, puoi usare lo stdbufstrumento. Ad esempio come:

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

Vedi anche qui su stdbufo altre opzioni.


1
Mi salvi la giornata, grazie per PYTHONUNBUFFERED = 1
diewland

9
for line in p.stdout:
  ...

si blocca sempre fino al successivo avanzamento riga.

Per un comportamento "in tempo reale" devi fare qualcosa del genere:

while True:
  inchar = p.stdout.read(1)
  if inchar: #neither empty string nor None
    print(str(inchar), end='') #or end=None to flush immediately
  else:
    print('') #flush for implicit line-buffering
    break

Il ciclo while viene lasciato quando il processo figlio chiude lo stdout o esce. read()/read(-1)si bloccherebbe fino a quando il processo figlio non chiudesse lo stdout o uscisse.


1
incharnon viene mai Noneusato if not inchar:( read()restituisce una stringa vuota su EOF). btw, è peggio for line in p.stdoutnon stampare nemmeno le righe complete in tempo reale in Python 2 ( for line in iter (p.stdout.readline, '') `potrebbe essere usato invece).
jfs

1
L'ho testato con python 3.4 su osx e non funziona.
qed

1
@qed: for line in p.stdout:funziona su Python 3. Assicurati di comprendere la differenza tra ''(stringa Unicode) e b''(byte). Vedi Python: leggi l'input in streaming da subprocess.communicate ()
jfs

8

Il tuo problema è:

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()

l'iteratore stesso ha un buffer aggiuntivo.

Prova a fare così:

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

5

Non è possibile ottenere stdout per stampare senza buffer su una pipe (a meno che non sia possibile riscrivere il programma che stampa su stdout), quindi ecco la mia soluzione:

Reindirizza lo stdout a sterr, che non è bufferizzato. '<cmd> 1>&2'dovrebbe farlo. Apri il processo come segue:myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
Non puoi distinguere da stdout o stderr, ma ottieni immediatamente tutto l'output.

Spero che questo aiuti chiunque ad affrontare questo problema.


4
L'hai provato? Perché non funziona .. Se stdout è memorizzato nel buffer in quel processo, non verrà reindirizzato a stderr nello stesso modo in cui non viene reindirizzato a un PIPE o un file ..
Filipe Pina

5
Questo è semplicemente sbagliato. il buffering dello stdout si verifica all'interno del programma stesso. La sintassi della shell 1>&2cambia solo i file a cui puntano i descrittori di file prima di avviare il programma. Il programma stesso non può distinguere tra il reindirizzamento di stdout a stderr ( 1>&2) o viceversa ( 2>&1), quindi questo non avrà alcun effetto sul comportamento di buffering del programma. In entrambi i casi la 1>&2sintassi viene interpretata dalla shell. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)fallirebbe perché non hai specificato shell=True.
Will Manley,

Nel caso in cui le persone leggessero questo: ho provato a usare stderr invece di stdout, mostra esattamente lo stesso comportamento.
martedì

3

Modificare lo stdout dal processo rsync in modo che non sia bufferizzato.

p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=0,  # 0=unbuffered, 1=line-buffered, else buffer-size
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

3
Il buffering avviene sul lato rsync, la modifica dell'attributo bufsize sul lato python non aiuta.
nosklo

14
Per chiunque altro cerchi, la risposta di nosklo è completamente sbagliata: la visualizzazione dell'avanzamento di rsync non è bufferizzata; il vero problema è che il sottoprocesso restituisce un oggetto file e l'interfaccia dell'iteratore di file ha un buffer interno scarsamente documentato anche con bufsize = 0, richiedendo di chiamare readline () ripetutamente se hai bisogno di risultati prima che il buffer si riempia.
Chris Adams

3

Per evitare la memorizzazione nella cache dell'output potresti provare pexpect,

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

PS : So che questa domanda è piuttosto vecchia, ma fornisce ancora la soluzione che ha funzionato per me.

PPS : ho ottenuto questa risposta da un'altra domanda


3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

Sto scrivendo una GUI per rsync in Python e ho gli stessi problemi. Questo problema mi ha turbato per diversi giorni fino a quando non l'ho trovato in pyDoc.

Se universal_newlines è True, gli oggetti file stdout e stderr vengono aperti come file di testo in modalità newline universale. Le righe possono essere terminate da "\ n", la convenzione di fine riga di Unix, "\ r", la vecchia convenzione Macintosh o "\ r \ n", la convenzione di Windows. Tutte queste rappresentazioni esterne sono viste come "\ n" dal programma Python.

Sembra che rsync restituisca '\ r' quando la traduzione è in corso.


1

Ho notato che non si fa menzione dell'utilizzo di un file temporaneo come intermedio. Quanto segue aggira i problemi di buffering eseguendo l'output in un file temporaneo e consente di analizzare i dati provenienti da rsync senza connettersi a un pty. Ho testato quanto segue su una macchina Linux e l'output di rsync tende a differire tra le piattaforme, quindi le espressioni regolari per analizzare l'output possono variare:

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...

non è in tempo reale. Un file non risolve il problema di buffering sul lato di rsync.
jfs

tempfile.TemporaryFile può cancellarsi per una più facile pulizia in caso di eccezioni
jfs

3
while not p.poll()porta a un ciclo infinito se il sottoprocesso esce correttamente con 0, usa p.poll() is Noneinvece
jfs

Windows potrebbe vietare di aprire file già aperti, quindi open(file_name)potrebbe non riuscire
jfs

1
Ho appena trovato questa risposta, sfortunatamente solo per Linux, ma funziona come un collegamento di fascino Quindi estendo il mio comando come segue: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argve chiamo: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) e ora posso leggere senza buffering
Arvid Terzibaschian

0

se esegui qualcosa di simile in un thread e salvi la proprietà ffmpeg_time in una proprietà di un metodo in modo da potervi accedere, funzionerebbe molto bene ottengo output come questo: l' output è come se usi il threading in tkinter

input = 'path/input_file.mp4'
output = 'path/input_file.mp4'
command = "ffmpeg -y -v quiet -stats -i \"" + str(input) + "\" -metadata title=\"@alaa_sanatisharif\" -preset ultrafast -vcodec copy -r 50 -vsync 1 -async 1 \"" + output + "\""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
for line in self.process.stdout:
    reg = re.search('\d\d:\d\d:\d\d', line)
    ffmpeg_time = reg.group(0) if reg else ''
    print(ffmpeg_time)

-1

In Python 3, ecco una soluzione, che elimina un comando dalla riga di comando e fornisce stringhe ben decodificate in tempo reale non appena vengono ricevute.

Ricevitore ( receiver.py):

import subprocess
import sys

cmd = sys.argv[1:]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in p.stdout:
    print("received: {}".format(line.rstrip().decode("utf-8")))

Esempio di programma semplice che potrebbe generare output in tempo reale ( dummy_out.py):

import time
import sys

for i in range(5):
    print("hello {}".format(i))
    sys.stdout.flush()  
    time.sleep(1)

Produzione:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
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.