Drucken Sie die Ausgabe des Unterprozesses ständig, während der Prozess ausgeführt wird


201

Um Programme von meinen Python-Skripten aus zu starten, verwende ich die folgende Methode:

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)

Wenn ich also einen Prozess wie Process.execute("mvn clean install")starte, wartet mein Programm, bis der Prozess abgeschlossen ist, und erst dann erhalte ich die vollständige Ausgabe meines Programms. Dies ist ärgerlich, wenn ich einen Prozess ausführe, dessen Abschluss eine Weile dauert.

Kann ich mein Programm die Prozessausgabe zeilenweise schreiben lassen, indem ich die Prozessausgabe abfrage, bevor sie in einer Schleife oder so endet?

** [BEARBEITEN] Entschuldigung, ich habe nicht sehr gut gesucht, bevor ich diese Frage gestellt habe. Threading ist eigentlich der Schlüssel. Hier finden Sie ein Beispiel, das zeigt, wie es geht: ** Python Subprocess.Popen aus einem Thread


Thread statt Unterprozess, denke ich
Ant

9
Nein, du brauchst keine Threads. Die gesamte Piping-Idee funktioniert, da Sie Lese- / Schreibvorgänge von Prozessen erhalten können, während diese ausgeführt werden.
Tokand

Antworten:


264

Sie können iter verwenden , um Zeilen zu verarbeiten, sobald der Befehl sie ausgibt : lines = iter(fd.readline, ""). Hier ist ein vollständiges Beispiel, das einen typischen Anwendungsfall zeigt (danke an @jfs für die Hilfe):

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
Ich habe diesen Code ausprobiert (mit einem Programm, dessen Ausführung viel Zeit in Anspruch nimmt) und kann bestätigen, dass er Zeilen beim Empfang ausgibt, anstatt auf den Abschluss der Ausführung zu warten. Dies ist die überlegene Antwort imo.
Andrew Martin

11
Hinweis: In Python 3 können Sie verwenden for line in popen.stdout: print(line.decode(), end=''). Verwenden Sie zur Unterstützung von Python 2 und 3 das Byte-Literal: b''Andernfalls lines_iteratorendet es nie mit Python 3.
jfs

3
Das Problem bei diesem Ansatz ist, dass, wenn der Prozess für eine Weile angehalten wird, ohne etwas in stdout zu schreiben, keine Eingabe mehr zum Lesen vorhanden ist. Sie benötigen eine Schleife, um zu überprüfen, ob der Vorgang abgeschlossen ist. Ich habe versucht, dies mit subprocess32 auf Python 2.7
Har

7
es sollte funktionieren. Polieren es, könnte man hinzufügen bufsize=1(es kann die Leistung auf Python verbessern 2), in der Nähe der popen.stdoutRohr explizit (ohne für die Garbage Collection wartet der sich darum zu kümmern) und Erhöhung subprocess.CalledProcessError(wie check_call(), check_output()tun). Die printAnweisung unterscheidet sich in Python 2 und 3: Sie können den Softspace-Hack print line,(Hinweis: Komma) verwenden, um zu vermeiden, dass alle Zeilenumbrüche wie in Ihrem Code verdoppelt und universal_newlines=TruePython 3 weitergegeben werden, um Text anstelle von bytebezogener Antwort zu erhalten .
JFS

5
@binzhang Das ist kein Fehler, stdout wird standardmäßig in Python-Skripten gepuffert (auch für viele Unix-Tools). Versuchen Sie es execute(["python", "-u", "child_thread.py"]). Weitere Informationen: stackoverflow.com/questions/14258500/…
tokland

84

Ok, ich habe es geschafft, es ohne Threads zu lösen (Vorschläge, warum die Verwendung von Threads besser wäre, sind willkommen), indem ich einen Ausschnitt aus dieser Frage verwendet habe. Abfangen eines Standardprozesses während der Ausführung

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
Zusammenführen der ifischer des und tokland Code funktioniert recht gut (ich hatte zu ändern , print line,um sys.stdout.write(nextline); sys.stdout.flush()Andernfalls würde es alle zwei Zeilen auszudrucken Dann wieder diese IPython die Notebook - Schnittstelle verwendet, so vielleicht etwas anderes geschah -.. Egal, expliziten Aufruf flush()funktioniert.
eacousineau

3
Herr, du bist mein Lebensretter !! wirklich seltsam, dass diese Art von Dingen nicht in der Bibliothek selbst eingebaut sind. Wenn ich Cliapp schreibe, möchte ich sofort alles zeigen, was in der Schleife verarbeitet wird. s'rsly.
Holms

3
Kann diese Lösung so geändert werden, dass sowohl Ausgabe als auch Fehler ständig gedruckt werden? Wenn ich ändern stderr=subprocess.STDOUTzu stderr=subprocess.PIPEund rufen Sie dann process.stderr.readline()aus der Schleife, scheine ich afoul die sehr laufen Deadlock , dass etwa in der Dokumentation für das gewarnt subprocessModul.
Davidrmcharles

7
@DavidCharles Ich denke, was Sie suchen, ist, dass stdout=subprocess.PIPE,stderr=subprocess.STDOUTdies stderr erfasst, und ich glaube (aber ich habe es nicht getestet), dass es auch stdin erfasst.
Andrew Martin

Vielen Dank für das Warten auf den Exit-Code.
Ich

67

So drucken Sie die Ausgabe des Unterprozesses zeilenweise, sobald der Standardpuffer in Python 3 geleert ist:

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)

Hinweis: Sie brauchen nicht p.poll()- die Schleife endet, wenn eof erreicht ist. Und das brauchen Sie nicht iter(p.stdout.readline, '')- der Read-Ahead-Fehler ist in Python 3 behoben.

Siehe auch Python: Streaming-Eingabe von subprocess.communicate () lesen .


3
Diese Lösung hat bei mir funktioniert. Die oben angegebene akzeptierte Lösung druckte für mich nur leere Zeilen.
Codename

3
Ich musste sys.stdout.flush () hinzufügen, um sofort Ausdrucke zu erhalten.
Codename

3
@Codename: Sie sollten es nicht sys.stdout.flush()im übergeordneten Element benötigen - stdout ist zeilengepuffert, wenn es nicht in eine Datei / Pipe umgeleitet wird und der Druck lineden Puffer daher automatisch leert. Sie brauchen sys.stdout.flush()das Kind auch nicht - übergeben Sie -ustattdessen die Befehlszeilenoption.
JFS

1
@Codename: Wenn Sie verwenden möchten, >dann ausführen python -u your-script.py > some-file. Hinweis: -uOption, die ich oben erwähnt habe (keine Notwendigkeit zu verwenden sys.stdout.flush()).
JFS

1
@mvidelgauz muss nicht angerufen werden p.wait()- es wird beim Verlassen des withBlocks aufgerufen . Verwenden Sie p.returncode.
JFS

7

@tokland

habe deinen Code ausprobiert und für 3.4 korrigiert und windows dir.cmd ist ein einfacher dir-Befehl, der als cmd-Datei gespeichert wird

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
Sie könnten Ihren Code vereinfachen . iter()und end='\r\n'sind unnötig. Python verwendet standardmäßig den universellen Zeilenumbruchmodus, dh, jeder '\n'wird '\r\n'während des Druckvorgangs übersetzt. 'latin'Wenn es sich wahrscheinlich um eine falsche Codierung handelt, können Sie die universal_newlines=TrueTextausgabe in Python 3 abrufen (dekodiert mit der bevorzugten Codierung des Gebietsschemas). Hören Sie nicht auf .poll(), es könnten ungelesene Daten gepuffert sein. Wenn das Python-Skript in einer Konsole ausgeführt wird, ist seine Ausgabe zeilengepuffert. Sie können die Zeilenpufferung mit der -uOption erzwingen - das brauchen Sie flush=Truehier nicht.
JFS

7

Es gibt tatsächlich eine sehr einfache Möglichkeit, dies zu tun, wenn Sie nur die Ausgabe drucken möchten :

import subprocess
import sys

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

Hier zeigen wir den Unterprozess einfach auf unser eigenes Standardout und verwenden die vorhandene API für Erfolg oder Ausnahme.


1
Diese Lösung ist einfacher und sauberer als die Lösung von @ tokland für Python 3.6. Mir ist aufgefallen, dass das Argument shell = True nicht erforderlich ist.
Guter Wille

Guter Fang, guter Wille. Entferntshell=True
Andrew Ring

Sehr erstaunlich und funktioniert perfekt mit wenig Code. Vielleicht sollten Sie den Unterprozess stderr auch auf sys.stderr umleiten?
Manu

Manu kannst du sicher. Ich habe es hier nicht getan, weil der Versuch in der Frage darin bestand, stderr nach stdout umzuleiten.
Andrew Ring

Können Sie erklären, was der Unterschied zwischen sys.stdout und subprocess.STDOUT ist?
Ron Serruya

4

Für den Fall, dass jemand von beiden lesen stdoutund stderrgleichzeitig Threads verwenden möchte, habe ich mir Folgendes ausgedacht:

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)

Ich wollte dies nur teilen, als ich auf diese Frage kam und versuchte, etwas Ähnliches zu tun, aber keine der Antworten löste mein Problem. Hoffentlich hilft es jemandem!

Beachten Sie, dass in meinem Anwendungsfall ein externer Prozess den von uns durchgeführten Prozess beendet Popen().


1
Ich musste für python2 fast genau so etwas verwenden. Während so etwas in Python2 hätte bereitgestellt werden sollen, ist es nicht so, dass so etwas absolut in Ordnung ist.
Stuart Axon

3

Wenn Sie versuchen, die Antworten auf diese Frage zu erhalten, um die Standardausgabe aus einem Python-Skript zu erhalten, beachten Sie, dass Python die Standardausgabe puffert. Daher kann es eine Weile dauern, bis die Standardausgabe angezeigt wird.

Dies kann behoben werden, indem nach jedem Standardschreiben im Zielskript Folgendes hinzugefügt wird:

sys.stdout.flush()

1
Aber Python als Teilprozess von Python auszuführen ist in erster Linie verrückt. Ihr Skript sollte einfach importdas andere Skript sein. Schauen Sie in multiprocessingoder threadingwenn Sie eine parallelisierte Ausführung benötigen.
Tripleee

3
@triplee Es gibt verschiedene Szenarien, in denen das Ausführen von Python als Unterprozess von Python angemessen ist. Ich habe eine Reihe von Python-Batch-Skripten, die ich täglich nacheinander ausführen möchte. Diese können von einem Master-Python-Skript orchestriert werden, das die Ausführung initiiert und mir eine E-Mail sendet, wenn das untergeordnete Skript fehlschlägt. Jedes Skript ist von der anderen Sandbox getrennt - keine Namenskonflikte. Ich parallelisiere nicht, daher sind Multiprocessing und Threading nicht relevant.
user1379351

Sie können das andere Python-Programm auch mit einer anderen ausführbaren Python-Datei starten als der, in der das Haupt-Python-Programm ausgeführt wird, z. B.subprocess.run("/path/to/python/executable", "pythonProgramToRun.py")
Kyle Bridenstine

3

In Python> = 3.5 subprocess.runfunktioniert das Arbeiten für mich:

import subprocess

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

(Das Abrufen der Ausgabe während der Ausführung funktioniert auch ohne shell=True) https://docs.python.org/3/library/subprocess.html#subprocess.run


2
Dies ist nicht "während der Ausführung". Der subprocess.run()Aufruf wird erst zurückgegeben, wenn der Unterprozess beendet ist.
Tripleee

1
Können Sie erklären, wie es nicht "während der Ausführung" ist? So etwas >>> import subprocess; subprocess.run('top')scheint auch "während der Ausführung" zu drucken (und oben wird nie beendet). Vielleicht begreife ich keinen subtilen Unterschied?
user7017793

Wenn Sie die Ausgabe zurück zu Python umleiten, z. B. mit stdout=subprocess.PIPE, können Sie sie erst nach topAbschluss lesen . Ihr Python-Programm wird während der Ausführung des Unterprozesses blockiert.
Tripleee

1
Richtig, das macht Sinn. Die runMethode funktioniert immer noch, wenn Sie nur daran interessiert sind , die Ausgabe so zu sehen, wie sie generiert wurde. Wenn Sie etwas mit der Ausgabe in Python asynchron machen möchten, haben Sie Recht, dass es nicht funktioniert.
user7017793

3

Um die ursprüngliche Frage zu beantworten, leitet IMO den Unterprozess am besten stdoutdirekt zu Ihrem Programm um stdout(optional kann dies stderrauch wie im folgenden Beispiel durchgeführt werden).

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

3
Keine Angabe für stdoutund stderrmacht dasselbe mit weniger Code. Obwohl ich denke, explizit ist besser als implizit.
Tripleee

1

Dieser PoC liest ständig die Ausgabe eines Prozesses und kann bei Bedarf aufgerufen werden. Es bleibt nur das letzte Ergebnis erhalten, alle anderen Ausgaben werden verworfen, wodurch verhindert wird, dass der PIPE nicht mehr genügend Speicher hat:

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)

Ausgabe: Sie können deutlich sehen, dass nur im Intervall von ~ 2,5 Sekunden nichts dazwischen ausgegeben wird.

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

0

Dies funktioniert zumindest in Python3.4

import subprocess

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

1
Dies hat das Problem, dass es in der Schleife blockiert, bis der Prozess beendet ist.
Tripleee

0

Keine der Antworten hier ging auf alle meine Bedürfnisse ein.

  1. Keine Threads für stdout (auch keine Warteschlangen usw.)
  2. Nicht blockierend, da ich nach anderen Vorgängen suchen muss
  3. Verwenden Sie PIPE nach Bedarf, um mehrere Aufgaben auszuführen, z. B. Stream-Ausgabe, Schreiben in eine Protokolldatei und Zurückgeben einer Zeichenfolgenkopie der Ausgabe.

Ein kleiner Hintergrund: Ich verwende einen ThreadPoolExecutor, um einen Thread-Pool zu verwalten, wobei jeder einen Unterprozess startet und diese gleichzeitig ausführt. (In Python2.7 sollte dies aber auch in neuerem 3.x funktionieren). Ich möchte Threads nicht nur zum Sammeln von Ausgaben verwenden, da ich möchte, dass so viele wie möglich für andere Dinge verfügbar sind (ein Pool von 20 Prozessen würde 40 Threads nur zum Ausführen verwenden; 1 für den Prozess-Thread und 1 für stdout ... und mehr, wenn du stderr willst, denke ich)

Ich entferne hier viele Ausnahmen und solche, so dass dies auf Code basiert , der in der Produktion funktioniert. Hoffentlich habe ich es beim Kopieren und Einfügen nicht ruiniert. Auch Feedback sehr willkommen!

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

Ich bin mir sicher, dass hier Overhead hinzugefügt wird, aber das ist in meinem Fall kein Problem. Funktionell macht es was ich brauche. Das einzige, was ich nicht gelöst habe, ist, warum dies perfekt für Protokollnachrichten funktioniert, aber ich sehe, dass einige printNachrichten später und auf einmal angezeigt werden.


-2

In Python 3.6 habe ich Folgendes verwendet:

import subprocess

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

1
Dies ist keine Antwort auf diese spezielle Frage. Das Warten auf den Abschluss des Teilprozesses vor dem Erhalt seiner Ausgabe ist genau das, was das OP zu vermeiden versucht. Die alte Legacy-Funktion subprocess.call()hat einige Warzen, die durch neuere Funktionen behoben werden. in Python 3.6 würden Sie dies normalerweise verwenden subprocess.run(); Der Einfachheit halber subprocess.check_output()ist auch die ältere Wrapper-Funktion weiterhin verfügbar - sie gibt die tatsächliche Ausgabe des Prozesses zurück (dieser Code würde nur den Exit-Code zurückgeben, aber selbst dann etwas Undefiniertes drucken).
Tripleee
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.