web-dev-qa-db-fra.com

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

J'essaie d'écrire un script wrapper pour un programme en ligne de commande (svnadmin verify) qui affichera un indicateur de progression de Nice pour l'opération. Cela nécessite que je sois capable de voir chaque ligne de sortie du programme enveloppé dès qu'elle est sortie.

Je pensais que je ne ferais qu'exécuter le programme en utilisant subprocess.Popen, utiliser stdout=PIPE, puis lire chaque ligne au fur et à mesure de son entrée et agir en conséquence. Cependant, lorsque j'ai exécuté le code suivant, la sortie semblait être tamponnée quelque part, ce qui l'a fait apparaître dans 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 examiné un peu la documentation sur le sous-processus, j'ai découvert le paramètre bufsize à Popen. J'ai donc essayé de définir bufsize sur 1 (mémoire tampon pour chaque ligne) et 0 (pas de mémoire tampon), mais aucune valeur ne semblait modifier le mode de livraison des lignes. .

À ce stade, je commençais à saisir les paillettes. 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 dans Python compatible avec les versions ultérieures (pas exec*)?

109
Chris Lieb

J'ai essayé cela, et pour une raison quelconque, alors que le code

for line in p.stdout:
  ...

tampons agressivement, la variante

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

ne fait pas. Apparemment, il s'agit d'un bug connu: http://bugs.python.org/issue3907 (Le problème est maintenant "Fermé" à partir du 29 août 2018)

70
Dave
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1)
for line in iter(p.stdout.readline, b''):
    print line,
p.stdout.close()
p.wait()
34
Corey Goldberg

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.

18
Nadia Alramli

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

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

J'ai rencontré le même problème il y a quelque temps. Ma solution consistait à abandonner l'itération pour la méthode read, qui sera renvoyée immédiatement même si votre sous-processus n'a pas fini de s'exécuter, etc.

3
Eli Courtwright

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

Problème de sortie en temps réel résolu: J'ai rencontré un problème similaire en Python, tout en capturant 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 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: compte 1 Impression: compte 2 Impression: compte 3.

J'espère que ça aide.

~ sairam

2
sairam

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 est un processus Python, vous pouvez le faire avant l'appel:

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

Ou bien passez ceci dans l'argument env à Popen.

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

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

Voir aussi ici à propos de stdbuf ou d’autres options.

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

1
Albert

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é, ce qui laissera de côté une instruction break ou une boucle infinie possible. 

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()
1
Jason Hedlund

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

1
Gabe

C’est le squelette de base que j’utilise toujours pour cela. Il facilite la mise en œuvre des délais d'attente et permet de faire face aux inévitables processus de suspension.

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
Badslacks

(Cette solution a été testée avec Python 2.7.15)
Vous avez juste besoin de sys.stdout.flush () après chaque ligne en 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()
0
dan

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()
0
Andres Buritica

Trouvé cette fonction "plug-and-play" ici . 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)
0
Deena

Le sous-processus Streaming stdin et stdout avec asyncio en Python post par Kevin McCarthy montre comment 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 __== "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
0
Pablo