Ottenere output in tempo reale usando il sottoprocesso


135

Sto cercando di scrivere uno script wrapper per un programma a riga di comando (verifica svnadmin) che visualizzerà un indicatore di avanzamento gradevole per l'operazione. Ciò richiede che io sia in grado di vedere ogni riga di output dal programma spostato non appena viene emesso.

Ho pensato che avrei semplicemente eseguito il programma usando subprocess.Popen, usando stdout=PIPE, quindi leggendo ogni riga appena arrivata e agendo di conseguenza su di esso. Tuttavia, quando ho eseguito il seguente codice, l'output sembrava essere bufferizzato da qualche parte, facendolo apparire in due blocchi, le righe da 1 a 332, quindi da 333 a 439 (l'ultima riga di output)

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', '')

Dopo aver esaminato un po 'la documentazione sui sottoprocessi, ho scoperto il bufsizeparametro toPopen , quindi ho provato a impostare bufsize su 1 (buffer per riga) e 0 (nessun buffer), ma nessuno dei due valori sembrava cambiare il modo in cui venivano consegnate le righe.

A questo punto stavo iniziando a cogliere la paglia, quindi ho scritto il seguente loop di output:

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

ma ha ottenuto lo stesso risultato.

È possibile ottenere l'output del programma "in tempo reale" di un programma eseguito utilizzando il sottoprocesso? C'è qualche altra opzione in Python che è compatibile con il futuro (no exec*)?


1
Hai provato a omettere sydout=PIPEcosì il sottoprocesso scrive direttamente sulla tua console, aggirando il processo genitore?
S.Lott

5
Il fatto è che voglio leggere l'output. Se viene emesso direttamente sulla console, come potrei farlo? Inoltre, non voglio che l'utente veda l'output del programma wrapped, ma solo il mio output.
Chris Lieb,

Allora perché un display "in tempo reale"? Non capisco il caso d'uso.
S.Lott

8
Non usare shell = True. Inutilmente invoca il tuo guscio. Usa p = Popen (['svnadmin', 'confirm', '/ var / svn / repos / config'], stdout = PIPE, stderr = STDOUT) invece
nosklo,

2
@ S.Lott Fondamentalmente, svnadmin verifica stampa una riga di output per ogni revisione verificata. Volevo fare un buon indicatore di progresso che non avrebbe causato quantità eccessive di output. Un po 'come wget, per esempio
Chris Lieb,

Risposte:


82

Ho provato questo, e per qualche ragione mentre il codice

for line in p.stdout:
  ...

respinge in modo aggressivo, la variante

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

non. Apparentemente si tratta di un bug noto: http://bugs.python.org/issue3907 (il problema è ora "Chiuso" dal 29 agosto 2018)


Questo non è l'unico disastro nelle vecchie implementazioni di Python IO. Questo è il motivo per cui Py2.6 e Py3k sono finiti con una libreria IO completamente nuova.
Tim Lin,

3
Questo codice si interromperà se il sottoprocesso restituisce una riga vuota. Una soluzione migliore sarebbe quella di utilizzare while p.poll() is Noneinvece di while Truerimuovereif not line
exhuma il

6
@exhuma: funziona benissimo. readline restituisce "\ n" su una riga vuota, che non viene valutata come vera. restituisce una stringa vuota solo alla chiusura della pipe, che sarà quando il sottoprocesso termina.
Alice Purcell,

1
@Dave Per riferimento futuro: stampa le righe utf-8 in py2 + con print(line.decode('utf-8').rstrip()).
Jonathan Komar,

3
Inoltre per avere la lettura in tempo reale dell'output del processo dovrai dire a Python che NON vuoi alcun buffering. Caro Python, dammi direttamente l'output. Ed ecco come: è necessario impostare la variabile di ambiente PYTHONUNBUFFERED=1. Ciò è particolarmente utile per output infiniti
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()

1
@nbro probabilmente perché p.stdout.close()non è chiaro.
Anatoly Techtonik,

1
@nbro probabilmente perché il codice è stato dato senza spiegazioni ...: /
Aaron Hall

3
Di cosa parla questa b ''?
ManuelSchneid3r

29

È possibile indirizzare direttamente l'output del sottoprocesso agli stream. Esempio semplificato:

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)

Questo ti consente di ottenere anche i contenuti dopo il fatto .communicate()? O i contenuti vengono persi nei flussi stderr / stdout principali?
theferrit32

No, nessun communicate()metodo sul reso CompletedProcess. Inoltre, capture_outputsi escludono a vicenda con stdoute stderr.
Aidan Feldman,

20

Puoi provare questo:

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()

Se si utilizza readline anziché read, in alcuni casi il messaggio di input non viene stampato. Provalo con un comando che richiede un input inline e vedi di persona.


Sì, l'uso di readline () interromperà la stampa (anche con la chiamata a sys.stdout.flush ())
Mark Ma

3
Dovrebbe essere appeso indefinitamente? Vorrei che una data soluzione includesse anche il codice boilerplate per la modifica del loop al termine del sottoprocesso iniziale. Mi dispiace non importa quante volte ci guardo, sottoprocesso eccetera è qualcosa che non riesco mai a lavorare.
ThorSummoner,

1
Perché testare per "" quando in Python possiamo semplicemente usare se non fuori?
Greg Bell,

2
questa è la soluzione migliore per lavori di lunga durata. ma dovrebbe usare non è Nessuno e non! = Nessuno. Non dovresti usare! = Con Nessuno.
Cari,

Anche stderr viene visualizzato da questo?
Pieter Vogelaar,

7

Il sottoprocesso di streaming stdin e stdout con asyncio nel post sul blog Python di Kevin McCarthy mostra come farlo 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())

questo funziona con una leggera modifica al codice pubblicato
Jeef

Ciao @Jeef puoi indicare la correzione in modo che io possa aggiornare la risposta?
Pablo,

Ciao, ha funzionato per me, ma ho dovuto aggiungere quanto segue per eliminare alcuni messaggi di errore: import nest_asyncio; nest_asyncio.apply()e usare il comando shell, cioè process = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)invece di process = await create_subprocess_exec(...). Saluti!
user319436,

4

Problema di output in tempo reale risolto: ho riscontrato un problema simile in Python, catturando l'output in tempo reale dal programma c. Ho aggiunto " fflush (stdout) ;" nel mio codice C. Ha funzionato per me. Ecco lo snip il codice

<< Programma C >>

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

<< Programma 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)

<< USCITA >> Stampa: Conteggio 1 Stampa: Conteggio 2 Stampa: Conteggio 3

Spero che sia d'aiuto.

~ Sairam


1
Questa è stata l'unica cosa che ha effettivamente aiutato. Ho usato lo stesso codice ( flush(stdout)) in C ++. Grazie!
Gerhard Hagerer,

Stavo avendo lo stesso problema con uno script Python che chiamava un altro script Python come sottoprocesso. Sulle stampe dei sottoprocessi, era necessario "flush" (print ("hello", flush = True) in Python 3). Inoltre, molti esempi laggiù sono ancora (2020) Python 2, questo è Python 3, quindi +1
smajtkst

3

Mi sono imbattuto nello stesso problema qualche tempo fa. La mia soluzione era quella di abbandonare l'iterazione per il readmetodo, che tornerà immediatamente anche se il tuo sottoprocesso non è stato completato, ecc.


3

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

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

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

O in alternativa passare questo envnell'argomento aPopen .

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

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

Vedi anche qui circastdbuf o altre opzioni.

(Vedi anche qui per la stessa risposta.)


2

Ho usato questa soluzione per ottenere output in tempo reale su un sottoprocesso. Questo ciclo si interromperà non appena il processo verrà completato, lasciando fuori la necessità di un'istruzione break o di un possibile 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()

5
è possibile che questo esca dal ciclo senza che il buffer stdout sia vuoto?
jayjay

Ho cercato moltissimo una risposta adeguata che non si fosse bloccata al completamento! Ho trovato questa soluzione aggiungendo if out=='': breakdopoout = sub_process...
Sos

2

Ho trovato questa funzione "plug-and-play" qui . Ha funzionato come un fascino!

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)

1
L'aggiunta di stderr=subprocess.STDOUTeffettivamente aiuta molto nella cattura dei dati di streaming. Lo sto votando.
Khan,

1
La carne bovina principale qui sembra provenire dalla risposta accettata
triplo

2

È possibile utilizzare un iteratore su ogni byte nell'output del sottoprocesso. Ciò consente l'aggiornamento in linea (le righe che terminano con '\ r' sovrascrivono la riga di output precedente) dal sottoprocesso:

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.")

2

In Python 3.x il processo potrebbe bloccarsi perché l'output è un array di byte anziché una stringa. Assicurati di decodificarlo in una stringa.

A partire da Python 3.6 puoi farlo usando il parametro encodingin Popen Constructor . L'esempio 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)

Si noti che questo codice reindirizzamenti stderr per stdoute gestisce gli errori di output .


1

L'uso di pexpect [ http://www.noah.org/wiki/Pexpect ] con readline non bloccanti risolverà questo problema. Deriva dal fatto che le pipe sono bufferizzate e quindi l'output della tua app viene bufferizzato dalla pipe, quindi non puoi arrivare a quell'output fino a quando il buffer non si riempie o il processo termina.


0

Soluzione 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()

1
Dal momento che stai usando universal_newlines=Trueper la Popen()chiamata, probabilmente non hai bisogno di gestirne anche tu - questo è il punto centrale dell'opzione.
martineau,

1
sembra inutile complicato. Non risolve i problemi di buffering. Vedi i collegamenti nella mia risposta .
jfs,

Questo è l'unico modo per ottenere l'output dei progressi di rsync in tempo reale (- outbuf = L)! grazie
Mohammadhzp,

0

Questo è lo scheletro di base che uso sempre per questo. Semplifica l'implementazione dei timeout ed è in grado di affrontare inevitabili processi di sospensione.

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()

0

(Questa soluzione è stata testata con Python 2.7.15)
Devi solo sys.stdout.flush () dopo ogni riga di lettura / scrittura:

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()

0

Poche risposte suggeriscono python 3.xo pthon 2.x, il codice sottostante funzionerà per entrambi.

 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
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.