Echtzeitausgabe mithilfe eines Unterprozesses

135

Ich versuche, ein Wrapper-Skript für ein Befehlszeilenprogramm (svnadmin verify) zu schreiben, das eine schöne Fortschrittsanzeige für den Vorgang anzeigt. Dies erfordert, dass ich jede Ausgabezeile des umschlossenen Programms sehen kann, sobald es ausgegeben wird.

Ich dachte mir, ich würde das Programm einfach ausführen subprocess.Popen, verwenden stdout=PIPE, dann jede Zeile beim Eingang lesen und entsprechend handeln. Als ich jedoch den folgenden Code ausführte, schien die Ausgabe irgendwo gepuffert zu sein, was dazu führte, dass sie in zwei Blöcken angezeigt wurde, den Zeilen 1 bis 332 und dann 333 bis 439 (die letzte Ausgabezeile).

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

Nachdem ich mir die Dokumentation zum Unterprozess ein wenig angesehen hatte, entdeckte ich den bufsizeParameter to Popenund versuchte, die Puffergröße auf 1 (Puffer für jede Zeile) und 0 (kein Puffer) zu setzen, aber keiner der Werte schien die Art und Weise zu ändern, in der die Zeilen geliefert wurden.

Zu diesem Zeitpunkt fing ich an, nach Strohhalmen zu greifen, also schrieb ich die folgende Ausgabeschleife:

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

bekam aber das gleiche Ergebnis.

Ist es möglich, eine Echtzeit-Programmausgabe eines Programms zu erhalten, das unter Verwendung eines Unterprozesses ausgeführt wird? Gibt es in Python eine andere Option, die vorwärtskompatibel ist (nicht exec*)?

Chris Lieb
quelle
1
Haben Sie versucht, das wegzulassen, sydout=PIPEdamit der Unterprozess direkt auf Ihre Konsole schreibt und den übergeordneten Prozess umgeht?
S.Lott
5
Die Sache ist, dass ich die Ausgabe lesen möchte. Wie kann ich das tun, wenn es direkt an die Konsole ausgegeben wird? Außerdem möchte ich nicht, dass der Benutzer die Ausgabe des umschlossenen Programms sieht, sondern nur meine Ausgabe.
Chris Lieb
Warum dann eine "Echtzeit" -Anzeige? Ich verstehe den Anwendungsfall nicht.
S.Lott
8
Verwenden Sie nicht shell = True. Es ruft unnötig Ihre Shell auf. Verwenden Sie stattdessen p = Popen (['svnadmin', 'verify', '/ var / svn / repos / config'], stdout = PIPE, stderr = STDOUT)
nosklo
2
@ S.Lott Grundsätzlich druckt svnadmin verify für jede überprüfte Revision eine Ausgabezeile. Ich wollte einen schönen Fortschrittsindikator erstellen, der keine übermäßigen Ausgaben verursacht. Ein bisschen wie wget zum Beispiel
Chris Lieb

Antworten:

82

Ich habe es versucht, und aus irgendeinem Grund während des Codes

for line in p.stdout:
  ...

puffert aggressiv die Variante

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

nicht. Anscheinend ist dies ein bekannter Fehler: http://bugs.python.org/issue3907 (Das Problem ist ab dem 29. August 2018 "geschlossen")

Dave
quelle
Dies ist nicht das einzige Durcheinander in den alten Python IO-Implementierungen. Aus diesem Grund haben Py2.6 und Py3k eine völlig neue E / A-Bibliothek erhalten.
Tim Lin
3
Dieser Code wird unterbrochen, wenn der Unterprozess eine leere Zeile zurückgibt. Eine bessere Lösung wäre, while p.poll() is Noneanstelle while Truederif not line
exhuma
6
@exhuma: es funktioniert gut. readline gibt "\ n" in einer leeren Zeile zurück, die nicht als wahr ausgewertet wird. Es wird nur eine leere Zeichenfolge zurückgegeben, wenn die Pipe geschlossen wird. Dies ist der Fall, wenn der Unterprozess beendet wird.
Alice Purcell
1
@ Dave Für zukünftige Ref: drucke utf-8 Zeilen in py2 + mit print(line.decode('utf-8').rstrip()).
Jonathan Komar
3
Um die Ausgabe des Prozesses in Echtzeit lesen zu können, müssen Sie Python mitteilen, dass Sie KEINE Pufferung wünschen. Lieber Python, gib mir einfach die Ausgabe direkt. Und so geht's: Sie müssen die Umgebungsvariable festlegen PYTHONUNBUFFERED=1. Dies ist besonders nützlich für Ausgaben, die unendlich sind
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
quelle
1
@nbro wahrscheinlich weil p.stdout.close()unklar ist.
Anatoly Techtonik
1
@nbro wahrscheinlich, weil Code ohne Erklärung gegeben wurde ...: /
Aaron Hall
3
Worum geht es in diesem b ''?
ManuelSchneid3r
29

Sie können die Unterprozessausgabe direkt an die Streams weiterleiten. Vereinfachtes Beispiel:

subprocess.run(['ls'], stderr=sys.stderr, stdout=sys.stdout)
Aidan Feldman
quelle
Können Sie damit auch den Inhalt nachträglich abrufen .communicate()? Oder gehen die Inhalte für die übergeordneten stderr / stdout-Streams verloren?
Theferrit32
Nein, keine communicate()Methode für die Rückgabe CompletedProcess. Auch capture_outputschließt sich mit stdoutund gegenseitig aus stderr.
Aidan Feldman
20

Sie können dies versuchen:

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

Wenn Sie readline anstelle von read verwenden, wird die Eingabenachricht in einigen Fällen nicht gedruckt. Probieren Sie es mit einem Befehl aus, für den eine Inline-Eingabe erforderlich ist, und überzeugen Sie sich selbst.

Nadia Alramli
quelle
Ja, die Verwendung von readline () beendet den Druckvorgang (auch wenn sys.stdout.flush () aufgerufen wird)
Mark Ma
3
Soll das auf unbestimmte Zeit hängen? Ich würde mir wünschen, dass eine bestimmte Lösung auch Boilerplate-Code zum Bearbeiten der Schleife enthält, wenn der erste Unterprozess abgeschlossen ist. Es tut mir leid, egal wie oft ich mich damit beschäftige, Unterprozesse usw. sind etwas, das ich einfach nie zur Arbeit bringen kann.
ThorSummoner
1
Warum auf '' testen, wenn wir in Python nur verwenden können, wenn nicht out?
Greg Bell
2
Dies ist die beste Lösung für langfristige Jobs. aber es sollte nicht None und nicht verwenden! = None. Sie sollten nicht! = Mit None verwenden.
Cari
Wird stderr auch dadurch angezeigt?
Pieter Vogelaar
7

Der Streaming-Unterprozess stdin und stdout mit asyncio im Python- Blogbeitrag von Kevin McCarthy zeigt, wie es mit asyncio geht:

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
quelle
Dies funktioniert mit einer geringfügigen Änderung des Codes veröffentlicht
Jeef
Hallo @Jeef, kannst du auf das Update hinweisen, damit ich die Antwort aktualisieren kann?
Pablo
Hallo, das hat bei mir funktioniert, aber ich musste Folgendes hinzufügen, um einige Fehlermeldungen zu entfernen: import nest_asyncio; nest_asyncio.apply()und den Shell-Befehl verwenden, dh process = await create_subprocess_shell(*command, stdout=PIPE, stderr=PIPE, shell=True)anstelle von process = await create_subprocess_exec(...). Prost!
user319436
4

Problem mit der Echtzeitausgabe behoben: Beim Erfassen der Echtzeitausgabe des c-Programms ist ein ähnliches Problem in Python aufgetreten. Ich fügte hinzu " fflush (stdout) ;" in meinem C-Code. Es hat bei mir funktioniert. Hier ist der Ausschnitt des Codes

<< C Programm >>

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

<< Python-Programm >>

#!/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)

<< AUSGABE >> Drucken: Anzahl 1 Druck: Anzahl 2 Druck: Anzahl 3

Ich hoffe es hilft.

~ Sairam

Sairam
quelle
1
Dies war das einzige, was tatsächlich half. Ich habe den gleichen Code verwendet (flush(stdout) ) in C ++ verwendet. Vielen Dank!
Gerhard Hagerer
Ich hatte das gleiche Problem mit einem Python-Skript, das ein anderes Python-Skript als Unterprozess aufrief. Bei den Unterprozessdrucken war "Flush" erforderlich (print ("Hallo", Flush = True) in Python 3). Außerdem gibt es dort noch viele (2020) Python 2, dies ist Python 3, also +1
smajtkst
3

Ich bin vor einiger Zeit auf dasselbe Problem gestoßen. Meine Lösung bestand darin, die Iteration für die readMethode aufzugeben , die sofort zurückgegeben wird, selbst wenn Ihr Unterprozess nicht vollständig ausgeführt wurde usw.

Eli Courtwright
quelle
3

Je nach Anwendungsfall möchten Sie möglicherweise auch die Pufferung im Unterprozess selbst deaktivieren.

Wenn der Unterprozess ein Python-Prozess ist, können Sie dies vor dem Aufruf tun:

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

Oder geben Sie dies alternativ in der env Argument an Popen.

Andernfalls können Sie unter Linux / Unix die verwenden stdbuf Tool verwenden. ZB wie:

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

Siehe auch hier über stdbufoder andere Optionen.

(Siehe auch hier für die gleiche Antwort.)

Albert
quelle
2

Ich habe diese Lösung verwendet, um Echtzeitausgaben für einen Unterprozess zu erhalten. Diese Schleife wird beendet, sobald der Prozess abgeschlossen ist, sodass keine break-Anweisung oder mögliche Endlosschleife erforderlich ist.

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
quelle
5
Ist es möglich, dass dies die Schleife verlässt, ohne dass der Standardpuffer leer ist?
Jayjay
Ich habe viel nach einer passenden Antwort gesucht, die nach Fertigstellung nicht hängen blieb! Ich fand dies als Lösung, indem ich if out=='': breaknachout = sub_process...
Sos
2

Ich habe diese "Plug-and-Play" -Funktion hier gefunden . Lief wie am Schnürchen!

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
quelle
1
Das Hinzufügen von stderr=subprocess.STDOUThilft tatsächlich sehr bei der Erfassung von Streaming-Daten. Ich stimme zu.
Khan
1
Das
Hauptrindfleisch
2

Sie können einen Iterator über jedes Byte in der Ausgabe des Unterprozesses verwenden. Dies ermöglicht eine Inline-Aktualisierung (Zeilen, die mit '\ r' enden, überschreiben die vorherige Ausgabezeile) aus dem Unterprozess:

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
quelle
2

In Python 3.x kann der Prozess hängen bleiben, da die Ausgabe ein Byte-Array anstelle einer Zeichenfolge ist. Stellen Sie sicher, dass Sie es in eine Zeichenfolge dekodieren.

Ab Python 3.6 können Sie dies mit dem Parameter encodingin Popen Constructor tun . Das vollständige Beispiel:

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)

Beachten Sie, dass dieser Code zu Ausgabefehlern umleitet und diese behandelt .stderrstdout

pavelnazimok
quelle
1

Die Verwendung von pexpect [ http://www.noah.org/wiki/Pexpect ] mit nicht blockierenden Readlines löst dieses Problem. Dies ist darauf zurückzuführen, dass Pipes gepuffert werden und die Ausgabe Ihrer App daher von der Pipe gepuffert wird. Daher können Sie diese Ausgabe erst erreichen, wenn der Puffer voll ist oder der Prozess abbricht.

Gabe
quelle
0

Komplette Lösung:

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
quelle
1
Da Sie universal_newlines=Trueden Popen()Anruf verwenden, müssen Sie wahrscheinlich auch nicht selbst damit umgehen - das ist der springende Punkt der Option.
Martineau
1
es scheint unnötig kompliziert. Es löst keine Pufferprobleme. Siehe Links in meiner Antwort .
JFS
Nur so kann ich die Ausgabe des rsync-Fortschritts in Echtzeit erhalten (- outbuf = L)! danke
Mohammadhzp
0

Dies ist das Grundgerüst, das ich immer dafür benutze. Es erleichtert die Implementierung von Zeitüberschreitungen und ist in der Lage, unvermeidliche Hängeprozesse zu bewältigen.

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
quelle
0

(Diese Lösung wurde mit Python 2.7.15 getestet.)
Sie müssen nur sys.stdout.flush () nach jeder Zeile lesen / schreiben:

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
quelle
0

Nur wenige Antworten, die Python 3.x oder Pthon 2.x vorschlagen. Der folgende Code funktioniert für beide.

 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
quelle