Obtenir une sortie en temps réel à l'aide d'un sous-processus

135

J'essaie d'écrire un script wrapper pour un programme de ligne de commande (svnadmin verify) qui affichera un bel indicateur de progression pour l'opération. Cela me oblige à être en mesure de voir chaque ligne de sortie du programme encapsulé dès sa sortie.

J'ai pensé que j'exécuterais simplement le programme en utilisant subprocess.Popen, utiliserais stdout=PIPE, puis lisais chaque ligne au fur et à mesure et agirais en conséquence. Cependant, lorsque j'ai exécuté le code suivant, la sortie semblait être mise en mémoire tampon quelque part, ce qui l'a amenée à apparaître en deux morceaux, les lignes 1 à 332, puis 333 à 439 (la dernière ligne de sortie)

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

Après avoir regardé un peu la documentation sur le sous-processus, j'ai découvert le bufsizeparamètre à Popen, donc j'ai essayé de définir bufsize sur 1 (tampon chaque ligne) et 0 (pas de tampon), mais aucune des valeurs ne semblait changer la façon dont les lignes étaient livrées.

À ce stade, je commençais à comprendre les pailles, j'ai donc écrit la boucle de sortie suivante:

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

mais a obtenu le même résultat.

Est-il possible d'obtenir la sortie de programme «en temps réel» d'un programme exécuté à l'aide d'un sous-processus? Existe-t-il une autre option en Python qui est compatible avec l'avant (non exec*)?

Chris Lieb
la source
1
Avez-vous essayé d'omettre le sydout=PIPEpour que le sous-processus écrit directement sur votre console, en contournant le processus parent?
S.Lott
5
Le fait est que je veux lire la sortie. S'il est sorti directement sur la console, comment pourrais-je faire cela? De plus, je ne veux pas que l'utilisateur voie la sortie du programme encapsulé, juste ma sortie.
Chris Lieb
Alors pourquoi un affichage "en temps réel"? Je ne comprends pas le cas d'utilisation.
S.Lott
8
N'utilisez pas shell = True. Il invoque inutilement votre shell. Utilisez p = Popen (['svnadmin', 'verify', '/ var / svn / repos / config'], stdout = PIPE, stderr = STDOUT) à la place
nosklo
2
@ S.Lott Fondamentalement, svnadmin verify imprime une ligne de sortie pour chaque révision vérifiée. Je voulais créer un bon indicateur de progression qui ne causerait pas de quantités excessives de sortie. Un peu comme wget, par exemple
Chris Lieb

Réponses:

82

J'ai essayé ceci, et pour une raison quelconque pendant que le code

for line in p.stdout:
  ...

tamponne agressivement, la variante

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

ne fait pas. Apparemment, il s'agit d'un bogue connu: http://bugs.python.org/issue3907 (Le problème est désormais "clos" depuis le 29 août 2018)

Dave
la source
Ce n'est pas le seul désordre dans les anciennes implémentations d'E / S Python. C'est pourquoi Py2.6 et Py3k se sont retrouvés avec une toute nouvelle bibliothèque IO.
Tim Lin
3
Ce code sera interrompu si le sous-processus renvoie une ligne vide. Une meilleure solution serait d'utiliser à la while p.poll() is Noneplace de while Trueet de supprimer leif not line
exhuma
6
@exhuma: ça marche bien. readline renvoie "\ n" sur une ligne vide, qui n'est pas évaluée comme vraie. il ne renvoie une chaîne vide que lorsque le tube se ferme, ce qui le sera lorsque le sous-processus se termine.
Alice Purcell le
1
@Dave Pour la future réf: imprimer les lignes utf-8 dans py2 + avec print(line.decode('utf-8').rstrip()).
Jonathan Komar
3
Aussi pour avoir la lecture en temps réel de la sortie du processus, vous devrez dire à python que vous ne voulez PAS de mise en mémoire tampon. Cher Python, donnez-moi juste la sortie directement. Et voici comment: Vous devez définir la variable d'environnement PYTHONUNBUFFERED=1. Ceci est particulièrement utile pour les sorties qui sont infinies
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()
Corey Goldberg
la source
1
@nbro probablement parce que ce p.stdout.close()n'est pas clair.
anatoly techtonik
1
@nbro probablement parce que le code a été donné sans explication ...: /
Aaron Hall
3
De quoi s'agit-il?
ManuelSchneid3r
29

Vous pouvez diriger la sortie du sous-processus vers les flux directement. Exemple simplifié:

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)
Aidan Feldman
la source
Cela vous permet-il également d'obtenir le contenu après coup .communicate()? Ou le contenu est-il perdu dans les flux stderr / stdout parents?
theferrit32
Non, pas de communicate()méthode sur le retour CompletedProcess. De plus, capture_outputest mutuellement exclusif avec stdoutet stderr.
Aidan Feldman
20

Vous pouvez essayer ceci:

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 vous utilisez readline au lieu de read, il y aura des cas où le message d'entrée ne sera pas imprimé. Essayez-le avec une commande qui nécessite une entrée en ligne et voyez par vous-même.

Nadia Alramli
la source
Oui, l'utilisation de readline () arrêtera l'impression (même en appelant sys.stdout.flush ())
Mark Ma
3
Est-ce censé être suspendu indéfiniment? Je souhaiterais qu'une solution donnée inclue également un code standard pour l'édition de la boucle lorsque le sous-processus initial est terminé. Désolé, peu importe le nombre de fois que je l'examine, le sous-processus, etc., est quelque chose que je ne peux jamais me mettre au travail.
ThorSummoner
1
Pourquoi tester pour '' alors qu'en Python, nous pouvons simplement l'utiliser sinon dehors?
Greg Bell
2
c'est la meilleure solution pour les travaux de longue durée. mais il devrait utiliser n'est pas None et non! = None. Vous ne devez pas utiliser! = Avec None.
Cari
Est-ce que stderr est également affiché?
Pieter Vogelaar
7

Le sous-processus Streaming stdin and stdout with asyncio in Python blog post par Kevin McCarthy montre comment le faire avec 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())
Pablo
la source
cela fonctionne avec une légère modification du code affiché
Jeef
Salut @Jeef, pouvez-vous indiquer le correctif afin que je puisse mettre à jour la réponse?
Pablo
Salut, cela a fonctionné pour moi mais j'ai dû ajouter ce qui suit pour me débarrasser de certains messages d'erreur: import nest_asyncio; nest_asyncio.apply()et utiliser la commande shell, c'est-à-dire process = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)au lieu de process = await create_subprocess_exec(...). À votre santé!
user319436
4

Problème de sortie en temps réel résolu: j'ai rencontré un problème similaire en Python, lors de la capture de la sortie en temps réel du programme c. J'ai ajouté " fflush (stdout) ;" dans mon code C. Cela a fonctionné pour moi. Voici le snip le code

<< Programme C >>

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

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

<< SORTIE >> Impression: Comptage 1 Impression: Comptage 2 Impression: Nombre 3

J'espère que ça aide.

~ sairam

sairam
la source
1
C'était la seule chose qui a réellement aidé. J'ai utilisé le même code ( flush(stdout)) en C ++. Merci!
Gerhard Hagerer
J'avais le même problème avec un script python appelant un autre script python en tant que sous-processus. Sur les impressions du sous-processus, "flush" était nécessaire (print ("hello", flush = True) en python 3). De plus, il y a encore beaucoup d'exemples là-bas (2020) python 2, c'est python 3, donc +1
smajtkst
3

J'ai rencontré le même problème il y a quelque temps. Ma solution était d'abandonner l'itération de la readméthode, qui reviendra immédiatement même si votre sous-processus n'est pas terminé, etc.

Eli Courtwright
la source
3

Selon le cas d'utilisation, vous pouvez également désactiver la mise en mémoire tampon dans le sous-processus lui-même.

Si le sous-processus sera un processus Python, vous pouvez le faire avant l'appel:

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

Ou transmettez-le dans l' envargument àPopen .

Sinon, si vous êtes sous Linux / Unix, vous pouvez utiliser l' stdbufoutil. Par exemple, comme:

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

Voir aussi ici à propos destdbuf ou d'autres options.

(Voir aussi ici pour la même réponse.)

Albert
la source
2

J'ai utilisé cette solution pour obtenir une sortie en temps réel sur un sous-processus. Cette boucle s'arrêtera dès que le processus sera terminé, laissant de côté le besoin d'une instruction break ou d'une possible boucle infinie.

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()
Jason Hedlund
la source
5
est-il possible que cela quitte la boucle sans que le tampon stdout soit vide?
jayjay
J'ai beaucoup cherché une réponse appropriée qui ne s'est pas accrochée à la fin! J'ai trouvé cela comme une solution en ajoutant if out=='': breakaprèsout = sub_process...
Sos
2

Trouvé cette fonction "plug-and-play" ici . A travaillé comme un charme!

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)
Deena
la source
1
L'ajout de stderr=subprocess.STDOUTaide en fait beaucoup à capturer des données en streaming. Je suis en train de voter.
khan
1
Le bœuf principal ici semble provenir de la réponse acceptée
tripleee
2

Vous pouvez utiliser un itérateur sur chaque octet dans la sortie du sous-processus. Cela permet la mise à jour en ligne (les lignes se terminant par '\ r' écrasent la ligne de sortie précédente) à partir du sous-processus:

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.")
rhyno183
la source
2

Dans Python 3.x, le processus peut se bloquer car la sortie est un tableau d'octets au lieu d'une chaîne. Assurez-vous de le décoder en une chaîne.

À partir de Python 3.6, vous pouvez le faire en utilisant le paramètre encodingde Popen Constructor . L'exemple complet:

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)

Notez que ce code redirige stderr vers stdoutet gère les erreurs de sortie .

Pavelnazimok
la source
1

L'utilisation de pexpect [ http://www.noah.org/wiki/Pexpect ] avec des readlines non bloquantes résoudra ce problème. Cela vient du fait que les tubes sont mis en mémoire tampon et que la sortie de votre application est donc mise en mémoire tampon par le tube.Par conséquent, vous ne pouvez pas accéder à cette sortie tant que le tampon ne se remplit pas ou que le processus n'est pas mort.

Gabe
la source
0

Solution complète:

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()
Andres Restrepo
la source
1
Puisque vous utilisez universal_newlines=Truesur l' Popen()appel, vous n'avez probablement pas besoin de les gérer vous-même, c'est tout l'intérêt de l'option.
martineau
1
cela semble compliqué inutile. Cela ne résout pas les problèmes de mise en mémoire tampon. Voir liens dans ma réponse .
jfs
C'est le seul moyen pour moi d'obtenir la sortie de progression rsync en temps réel (- outbuf = L)! merci
Mohammadhzp
0

C'est le squelette de base que j'utilise toujours pour cela. Il facilite la mise en œuvre des délais d'expiration et est capable de gérer les processus de blocage inévitables.

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()
Badslacks
la source
0

(Cette solution a été testée avec Python 2.7.15)
Il vous suffit de sys.stdout.flush () après chaque ligne lecture / écriture:

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()
dan
la source
0

Peu de réponses suggérant python 3.x ou pthon 2.x, le code ci-dessous fonctionnera pour les deux.

 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
Djai
la source