Imprimer constamment la sortie du sous-processus pendant que le processus est en cours d'exécution


202

Pour lancer des programmes à partir de mes scripts Python, j'utilise la méthode suivante:

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)

Ainsi, lorsque je lance un processus comme Process.execute("mvn clean install"), mon programme attend que le processus soit terminé et ce n'est qu'alors que j'obtiens la sortie complète de mon programme. C'est ennuyeux si j'exécute un processus qui prend du temps pour se terminer.

Puis-je laisser mon programme écrire la sortie de processus ligne par ligne, en interrogeant la sortie de processus avant qu'elle ne se termine en boucle ou quelque chose?

** [EDIT] Désolé, je n'ai pas bien cherché avant de poster cette question. L'enfilage est en fait la clé. Trouvé un exemple ici qui montre comment le faire: ** Sous-processus Python.Ouvrir à partir d'un thread


Thread au lieu de sous-processus, je pense
Ant

9
Non, vous n'avez pas besoin de fils. L'idée de tuyauterie entière fonctionne parce que vous pouvez obtenir la lecture / écriture des processus pendant leur exécution.
tokland

Réponses:


264

Vous pouvez utiliser iter pour traiter les lignes dès que les sorties de commande: eux lines = iter(fd.readline, ""). Voici un exemple complet montrant un cas d'utilisation typique (merci à @jfs pour son aide):

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
J'ai essayé ce code (avec un programme qui prend beaucoup de temps à exécuter) et je peux confirmer qu'il génère des lignes à mesure qu'elles sont reçues, plutôt que d'attendre la fin de l'exécution. C'est la réponse supérieure imo.
Andrew Martin

11
Remarque: En Python 3, vous pouvez utiliser for line in popen.stdout: print(line.decode(), end=''). Pour prendre en charge Python 2 et 3, utilisez littéral octets: b''sinon, lines_iteratorne se termine jamais sur Python 3.
jfs

3
Le problème avec cette approche est que si le processus s'arrête un peu sans écrire quoi que ce soit sur stdout, il n'y a plus d'entrée à lire. Vous aurez besoin d'une boucle pour vérifier si le processus est terminé ou non. J'ai essayé ceci en utilisant subprocess32 sur python 2.7
Har

7
ça devrait marcher. Pour le peaufiner, vous pouvez ajouter bufsize=1(cela peut améliorer les performances sur Python 2), fermer popen.stdoutexplicitement le canal (sans attendre que le garbage collection s'en occupe), et augmenter subprocess.CalledProcessError(comme check_call(), check_output()faire). L' printinstruction est différente sur Python 2 et 3: vous pouvez utiliser le hack de softspace print line,(note: virgule) pour éviter de doubler toutes les nouvelles lignes comme le fait votre code et de passer universal_newlines=Truesur Python 3, pour obtenir du texte au lieu d'octets - réponse liée .
jfs

6
@binzhang Ce n'est pas une erreur, stdout est tamponné par défaut sur les scripts Python (également pour de nombreux outils Unix). Essayez execute(["python", "-u", "child_thread.py"]). Plus d'infos: stackoverflow.com/questions/14258500/…
tokland

84

Ok, j'ai réussi à le résoudre sans threads (toute suggestion pourquoi l'utilisation de threads serait mieux appréciée) en utilisant un extrait de cette question Interception de la sortie standard d'un sous-processus pendant son exécution

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
La fusion du code d'ifischer et de tokland fonctionne très bien (j'ai dû changer print line,pour sys.stdout.write(nextline); sys.stdout.flush(). Sinon, il s'imprimerait toutes les deux lignes. Là encore, cela utilise l'interface Notebook d'IPython, alors peut-être que quelque chose d'autre se passait - indépendamment, appelant explicitement flush()works.
eacousineau

3
monsieur, vous êtes mon sauveur de vie !! vraiment étrange que ce genre de choses ne soit pas intégré dans la bibliothèque elle-même .. parce que si j'écris cliapp, je veux tout montrer ce qui est en boucle instantanément .. s'rsly ..
holms

3
Cette solution peut-elle être modifiée pour imprimer constamment à la fois la sortie et les erreurs? Si je passe stderr=subprocess.STDOUTà stderr=subprocess.PIPEpuis appelle à process.stderr.readline()partir de la boucle, je semble aller à l'encontre du blocage même qui est averti dans la documentation du subprocessmodule.
davidrmcharles

7
@DavidCharles Je pense que ce que vous cherchez, stdout=subprocess.PIPE,stderr=subprocess.STDOUTc'est capturer stderr, et je crois (mais je n'ai pas testé) qu'il capture également stdin.
Andrew Martin

merci d'avoir attendu le code de sortie. Je ne savais pas comment le
résoudre

68

Pour imprimer la sortie ligne du sous-processus ligne par ligne dès que son tampon stdout est vidé en 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)

Remarque: vous n'avez pas besoin p.poll()- la boucle se termine lorsque eof est atteint. Et vous n'avez pas besoin iter(p.stdout.readline, '')- le bogue de lecture anticipée est corrigé dans Python 3.

Voir aussi, Python: lire l'entrée de streaming depuis subprocess.communicate () .


3
Cette solution a fonctionné pour moi. La solution acceptée ci-dessus a simplement été imprimée pour moi.
Nom de

3
J'ai dû ajouter sys.stdout.flush () pour obtenir des impressions immédiatement.
Nom de

3
@Codename: vous ne devriez pas avoir besoin sys.stdout.flush()dans le parent - stdout est mis en mémoire tampon s'il n'est pas redirigé vers un fichier / pipe et donc l'impression linevide automatiquement le tampon. Vous n'avez pas besoin sys.stdout.flush()de l'enfant aussi - passez -uplutôt l'option de ligne de commande.
jfs

1
@Codename: si vous souhaitez utiliser >puis exécutez python -u your-script.py > some-file. Remarque: -uoption que j'ai mentionnée ci-dessus (pas besoin d'utiliser sys.stdout.flush()).
jfs

1
@mvidelgauz pas besoin d'appeler p.wait()- il est appelé à la sortie du withbloc. Utilisez p.returncode.
jfs

8

Il existe en fait un moyen très simple de le faire lorsque vous souhaitez simplement imprimer la sortie:

import subprocess
import sys

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

Ici, nous pointons simplement le sous-processus vers notre propre sortie standard et utilisons l'api de réussite ou d'exception existante.


1
Cette solution est plus simple et plus propre que la solution de @ tokland, pour Python 3.6. J'ai remarqué que l'argument shell = True n'est pas nécessaire.
Bonne volonté le

Bonne prise, bonne volonté. Suppriméshell=True
Andrew Ring

Très astucieux et fonctionne parfaitement avec peu de code. Peut-être devriez-vous également rediriger le sous-processus stderr vers sys.stderr?
Manu

Manu, vous pouvez certainement. Je ne l'ai pas fait ici, car la tentative dans la question redirigeait stderr vers stdout.
Andrew Ring

Pouvez-vous expliquer quelle est la différence entre sys.stdout et subprocess.STDOUT?
Ron Serruya

7

@tokland

essayé votre code et corrigé pour 3.4 et windows dir.cmd est une simple commande dir, enregistrée en tant que fichier 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
vous pourriez simplifier votre code . iter()et end='\r\n'sont inutiles. Python utilise le mode de saut de ligne universel par défaut, c'est-à-dire que tout '\n'est traduit '\r\n'pendant l'impression. 'latin'est probablement un mauvais encodage, vous pouvez utiliser universal_newlines=Truepour obtenir la sortie de texte en Python 3 (décodé en utilisant l'encodage préféré des paramètres régionaux). Ne vous arrêtez pas .poll(), il pourrait y avoir des données non lues en mémoire tampon. Si le script Python s'exécute dans une console, sa sortie est mise en mémoire tampon de ligne; vous pouvez forcer la mise en mémoire tampon de ligne en utilisant l' -uoption - vous n'avez pas besoin flush=Trueici.
jfs

4

Dans le cas où quelqu'un veut lire les deux stdoutet stderren même temps en utilisant des threads, voici ce que j'ai trouvé:

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)

Je voulais juste partager cela, car je me suis retrouvé sur cette question en essayant de faire quelque chose de similaire, mais aucune des réponses n'a résolu mon problème. Espérons que cela aide quelqu'un!

Notez que dans mon cas d'utilisation, un processus externe tue le processus que nous Popen().


1
J'ai dû utiliser quelque chose presque exactement comme ça pour python2. Alors que quelque chose comme ça aurait dû être fourni en python2, ce n'est pas si quelque chose comme ça est absolument parfait.
Stuart Axon

3

Pour quiconque essaie de répondre à cette question pour obtenir la sortie standard à partir d'un script Python, notez que Python met en mémoire tampon sa sortie standard, et donc cela peut prendre un certain temps pour voir la sortie standard.

Cela peut être corrigé en ajoutant les éléments suivants après chaque écriture stdout dans le script cible:

sys.stdout.flush()

1
Mais exécuter Python en tant que sous-processus de Python est fou en premier lieu. Votre script doit simplement être importl'autre script; regardez multiprocessingou threadingsi vous avez besoin d'une exécution parallélisée.
tripleee

3
@triplee Il existe plusieurs scénarios dans lesquels l'exécution de Python en tant que sous-processus de Python est appropriée. J'ai un certain nombre de scripts batch python que je souhaite exécuter séquentiellement, quotidiennement. Ceux-ci peuvent être orchestrés par un script Python maître qui lance l'exécution et m'envoie un e-mail si le script enfant échoue. Chaque script est mis en sandbox les uns des autres - pas de conflits de nommage. Je ne fais pas de parallélisation, donc le multitraitement et le filetage ne sont pas pertinents.
user1379351

Vous pouvez également démarrer l'autre programme python en utilisant un exécutable python différent de celui sur lequel le programme python principal s'exécute, par exemple,subprocess.run("/path/to/python/executable", "pythonProgramToRun.py")
Kyle Bridenstine

3

En Python> = 3.5 en utilisant des subprocess.runœuvres pour moi:

import subprocess

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

(obtenir la sortie pendant l'exécution fonctionne également sans shell=True) https://docs.python.org/3/library/subprocess.html#subprocess.run


2
Ce n'est pas "pendant l'exécution". L' subprocess.run()appel ne revient que lorsque le sous-processus a fini de s'exécuter.
tripleee

1
Pouvez-vous expliquer comment ce n'est pas "pendant l'exécution"? Quelque chose comme >>> import subprocess; subprocess.run('top')semble également imprimer "pendant l'exécution" (et le dessus ne se termine jamais). Peut-être que je ne saisis pas une subtile différence?
user7017793

Si vous redirigez la sortie vers Python, par exemple avec stdout=subprocess.PIPEvous ne pouvez la lire qu'après avoir topterminé. Votre programme Python est bloqué lors de l'exécution du sous-processus.
tripleee

1
Bon, ça a du sens. La runméthode fonctionne toujours si vous souhaitez uniquement voir la sortie telle qu'elle est générée. Si vous voulez faire quelque chose avec la sortie en python de manière asynchrone, vous avez raison, cela ne fonctionne pas.
user7017793

3

Pour répondre à la question d'origine, la meilleure façon pour IMO est simplement de rediriger le sous-processus stdoutdirectement vers votre programme stdout(en option, la même chose peut être faite pour stderr, comme dans l'exemple ci-dessous)

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

3
Ne rien spécifier stdoutet stderrfait la même chose avec moins de code. Bien que je suppose qu'explicite vaut mieux qu'implicite.
tripleee

1

Ce PoC lit en permanence la sortie d'un processus et est accessible en cas de besoin. Seul le dernier résultat est conservé, toutes les autres sorties sont ignorées, ce qui empêche le PIPE de se développer hors de la mémoire:

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)

sortie: Vous pouvez clairement voir qu'il n'y a qu'une sortie de ~ 2,5 s d'intervalle rien entre les deux.

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

0

Cela fonctionne au moins en Python3.4

import subprocess

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

1
Cela a le problème qu'il bloque dans la boucle jusqu'à ce que le processus soit terminé.
tripleee

0

Aucune des réponses ici ne répond à tous mes besoins.

  1. Pas de threads pour stdout (pas de files d'attente, etc.)
  2. Non bloquant car je dois vérifier que d'autres choses se passent
  3. Utilisez PIPE car j'avais besoin de faire plusieurs choses, par exemple la sortie de flux, écrire dans un fichier journal et renvoyer une copie de chaîne de la sortie.

Un peu de contexte: j'utilise un ThreadPoolExecutor pour gérer un pool de threads, chacun lançant un sous-processus et les exécutant simultanément. (En Python2.7, mais cela devrait également fonctionner dans la version 3.x plus récente). Je ne veux pas utiliser de threads uniquement pour la collecte de sortie car je veux autant de disponibles que possible pour d'autres choses (un pool de 20 processus utiliserait 40 threads juste pour s'exécuter; 1 pour le thread de processus et 1 pour stdout ... et plus si vous voulez stderr je suppose)

Je retire beaucoup d'exceptions et telles ici, donc cela est basé sur du code qui fonctionne en production. J'espère que je ne l'ai pas gâché dans le copier-coller. De plus, les commentaires sont les bienvenus!

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

Je suis sûr que des frais généraux sont ajoutés ici, mais ce n'est pas un problème dans mon cas. Fonctionnellement, il fait ce dont j'ai besoin. La seule chose que je n'ai pas résolue est la raison pour laquelle cela fonctionne parfaitement pour les messages de journal, mais je vois certains printmessages apparaître plus tard et en même temps.


-2

En Python 3.6, j'ai utilisé ceci:

import subprocess

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

1
Ce n'est pas une réponse à cette question particulière. Attendre la fin du sous-processus avant d'obtenir sa sortie est précisément et précisément ce que l'OP essaie d'éviter. L'ancienne fonction héritée subprocess.call()a quelques verrues qui sont corrigées par de nouvelles fonctions; en Python 3.6, vous utiliseriez généralement subprocess.run()cela; pour plus de commodité, l'ancienne fonction wrapper subprocess.check_output()est également toujours disponible - elle renvoie la sortie réelle du processus (ce code retournerait uniquement le code de sortie, mais même à la place, imprime quelque chose d'indéfini).
tripleee
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.