stdout in Echtzeit aus dem Unterprozess abfangen


85

Ich möchte subprocess.Popen()rsync.exe in Windows und die Standardausgabe in Python drucken.

Mein Code funktioniert, aber er erfasst den Fortschritt erst, wenn eine Dateiübertragung abgeschlossen ist! Ich möchte den Fortschritt für jede Datei in Echtzeit drucken.

Verwenden Sie Python 3.1 jetzt, da ich gehört habe, dass es besser mit E / A umgehen kann.

import subprocess, time, os, sys

cmd = "rsync.exe -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()


1
(Von Google?) Alle PIPEs blockieren, wenn einer der PIPEs-Puffer voll ist und nicht gelesen wird. zB stdout Deadlock, wenn stderr gefüllt ist. Übergeben Sie niemals ein Rohr, das Sie nicht lesen möchten.
Nasser Al-Wohaibi

Könnte jemand erklären, warum Sie stdout nicht einfach auf sys.stdout anstatt auf subprocess.PIPE setzen können?
Mike

Antworten:


96

Einige Faustregeln für subprocess.

  • Niemals benutzen shell=True. Es ruft unnötig einen zusätzlichen Shell-Prozess auf, um Ihr Programm aufzurufen.
  • Beim Aufrufen von Prozessen werden Argumente als Listen weitergegeben. sys.argvin Python ist eine Liste, ebenso wie argvin C. Sie übergeben also eine Liste , Popenum Unterprozesse aufzurufen, keine Zeichenfolge.
  • Leiten Sie nicht stderrzu a um, PIPEwenn Sie es nicht lesen.
  • Leiten Sie nicht um, stdinwenn Sie nicht darauf schreiben.

Beispiel:

import subprocess, time, os, sys
cmd = ["rsync.exe", "-vaz", "-P", "source/" ,"dest/"]

p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT)

for line in iter(p.stdout.readline, b''):
    print(">>> " + line.rstrip())

Das heißt, es ist wahrscheinlich, dass rsync seinen Ausgang puffert, wenn es erkennt, dass es mit einer Pipe anstelle eines Terminals verbunden ist. Dies ist das Standardverhalten. Wenn eine Verbindung zu einer Pipe besteht, müssen Programme stdout explizit leeren, um Echtzeitergebnisse zu erhalten. Andernfalls wird die Standard-C-Bibliothek gepuffert.

Um dies zu testen, führen Sie stattdessen Folgendes aus:

cmd = [sys.executable, 'test_out.py']

und erstellen Sie eine test_out.pyDatei mit dem Inhalt:

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

Wenn Sie diesen Unterprozess ausführen, sollten Sie "Hallo" erhalten und 10 Sekunden warten, bevor Sie "Welt" geben. Wenn dies mit dem obigen Python-Code und nicht mit passiert rsync, bedeutet dies, dass die rsyncAusgabe selbst gepuffert wird, sodass Sie kein Glück haben.

Eine Lösung wäre pty, sich mit so etwas direkt mit a zu verbinden pexpect.


12
shell=Falseist richtig, wenn Sie eine Befehlszeile erstellen, insbesondere aus vom Benutzer eingegebenen Daten. Dies shell=Trueist jedoch auch nützlich, wenn Sie die gesamte Befehlszeile von einer vertrauenswürdigen Quelle erhalten (z. B. im Skript fest codiert).
Denis Otkidach

10
@ Denis Otkidach: Ich denke nicht, dass dies die Verwendung von rechtfertigt shell=True. Denken Sie darüber nach - Sie rufen einen anderen Prozess auf Ihrem Betriebssystem auf, der Speicherzuweisung, Festplattennutzung und Prozessorplanung umfasst, nur um eine Zeichenfolge zu teilen ! Und einer, den du dir angeschlossen hast !! Sie könnten in Python aufteilen, aber es ist trotzdem einfacher, jeden Parameter einzeln zu schreiben. Auch eine Liste Mittel verwenden Sie müssen keine spezielle Shell - Zeichen zu entkommen: Räume ;, >, <, &.. Ihre Parameter können diese Zeichen enthalten und Sie müssen sich keine Sorgen machen! Ich kann keinen Grund für die Verwendung erkennen shell=True, es sei denn, Sie führen einen Nur-Shell-Befehl aus.
Nosklo

nosklo, das sollte sein: p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
Senthil Kumaran

1
@mathtick: Ich bin mir nicht sicher, warum Sie diese Operationen als separate Prozesse ausführen würden. Sie können den Dateiinhalt ausschneiden und das erste Feld einfach in Python extrahieren, indem Sie das csvModul verwenden. Als Beispiel wäre Ihre Pipeline in Python jedoch: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultBeachten Sie, dass Sie mit langen Dateinamen und Shell-Sonderzeichen arbeiten können, ohne entkommen zu müssen, da die Shell nicht beteiligt ist. Es ist auch viel schneller, da es einen Prozess weniger gibt.
Nosklo

11
Verwenden Sie for line in iter(p.stdout.readline, b'')statt for line in p.stdoutin Python 2, da sonst Zeilen nicht in Echtzeit gelesen werden, auch wenn der Quellprozess seine Ausgabe nicht puffert.
JFS

41

Ich weiß, dass dies ein altes Thema ist, aber jetzt gibt es eine Lösung. Rufen Sie den rsync mit der Option --outbuf = L auf. Beispiel:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())

3
Dies funktioniert und sollte verbessert werden, um zukünftige Leser daran zu hindern, durch alle oben genannten Dialoge zu scrollen.
VectorVictor

1
@VectorVictor Es erklärt nicht, was los ist und warum es los ist. Es kann sein, dass Ihr Programm funktioniert, bis: 1. Sie hinzufügen preexec_fn=os.setpgrp, damit das Programm sein übergeordnetes Skript überlebt 2. Sie das Lesen aus der Pipe des Prozesses überspringen 3. der Prozess viele Daten ausgibt und die Pipe füllt 4. Sie stundenlang stecken bleiben und versuchen herauszufinden, warum das Programm, das Sie ausführen, nach einer zufälligen Zeit beendet wird . Die Antwort von @nosklo hat mir sehr geholfen.
Danuker

14

Unter Linux hatte ich das gleiche Problem, die Pufferung zu beseitigen. Ich habe schließlich "stdbuf -o0" (oder, erwartungsgemäß nicht gepuffert) verwendet, um die PIPE-Pufferung zu entfernen.

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

Ich könnte dann select.select auf stdout verwenden.

Siehe auch /unix/25372/


2
Für jeden, der versucht, den C-Code stdout aus Python abzurufen, kann ich bestätigen, dass diese Lösung die einzige war, die für mich funktioniert hat. Um es klar auszudrücken, ich spreche davon, 'stdbuf', '-o0' zu meiner vorhandenen Befehlsliste in Popen hinzuzufügen.
Rücksichtsloser

Danke dir! Ich habe mich bei einer Reihe von Pytest / Pytest-Bdd-Tests stdbuf -o0als sehr nützlich erwiesen. Ich habe geschrieben, dass eine C ++ - App erzeugt und überprüft wird, ob bestimmte Protokollanweisungen ausgegeben werden. Ohne stdbuf -o0diese Tests wurden 7 Sekunden benötigt, um die (gepufferte) Ausgabe des C ++ - Programms zu erhalten. Jetzt rennen sie fast augenblicklich!
Evadeflow

9
for line in p.stdout:
  ...

blockiert immer bis zum nächsten Zeilenvorschub.

Für "Echtzeit" -Verhalten müssen Sie Folgendes tun:

while True:
  inchar = p.stdout.read(1)
  if inchar: #neither empty string nor None
    print(str(inchar), end='') #or end=None to flush immediately
  else:
    print('') #flush for implicit line-buffering
    break

Die while-Schleife bleibt erhalten, wenn der untergeordnete Prozess seine Standardausgabe schließt oder beendet. read()/read(-1)würde blockieren, bis der untergeordnete Prozess seine Standardausgabe geschlossen oder beendet hat.


1
incharwird stattdessen nie Noneverwendet if not inchar:( read()gibt eine leere Zeichenfolge in EOF zurück). Übrigens ist es schlimmer, for line in p.stdoutdass in Python 2 nicht einmal vollständige Zeilen in Echtzeit gedruckt werden for line in (stattdessen könnte iter (p.stdout.readline, '') `verwendet werden).
JFS

1
Ich habe dies mit Python 3.4 auf OSX getestet und es funktioniert nicht.
Qed

1
@qed: for line in p.stdout:funktioniert unter Python 3. Stellen Sie sicher, dass Sie den Unterschied zwischen ''(Unicode-Zeichenfolge) und b''(Bytes) verstehen . Siehe Python: Lesen Sie die Streaming-Eingabe von subprocess.communicate ()
jfs

9

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 übergeben Sie dies alternativ im envArgument an Popen.

Andernfalls können Sie unter Linux / Unix das stdbufTool verwenden. ZB wie:

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

Siehe auch hier über stdbufoder andere Optionen.


Sie retten meinen Tag, danke für PYTHONUNBUFFERED = 1
diewland

8

Ihr Problem ist:

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()

Der Iterator selbst verfügt über eine zusätzliche Pufferung.

Versuchen Sie es so:

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

5

Sie können stdout nicht dazu bringen, ungepuffert in eine Pipe zu drucken (es sei denn, Sie können das Programm, das in stdout druckt, neu schreiben). Hier ist meine Lösung:

Leiten Sie stdout zu sterr um, das nicht gepuffert ist. '<cmd> 1>&2'Sollte es tun. Öffnen Sie den Prozess wie folgt: myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
Sie können nicht von stdout oder stderr unterscheiden, aber Sie erhalten alle Ausgaben sofort.

Ich hoffe, dies hilft jedem, dieses Problem anzugehen.


4
Hast du es versucht? Weil es nicht funktioniert. Wenn stdout in diesem Prozess gepuffert wird, wird es nicht auf die gleiche Weise zu stderr umgeleitet, wie es nicht zu einem PIPE oder einer Datei umgeleitet wird.
Filipe Pina

5
Das ist einfach falsch. Die Standardpufferung erfolgt innerhalb des Programms. Die Shell-Syntax 1>&2ändert lediglich, auf welche Dateien die Dateideskriptoren verweisen, bevor das Programm gestartet wird. Das Programm selbst kann nicht zwischen der Umleitung von stdout nach stderr ( 1>&2) oder umgekehrt ( 2>&1) unterscheiden, sodass dies keine Auswirkungen auf das Pufferverhalten des Programms hat. In beiden Fällen wird die 1>&2Syntax von der Shell interpretiert. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)würde fehlschlagen, weil Sie nicht angegeben haben shell=True.
Will Manley

Für den Fall, dass die Leute dies lesen würden: Ich habe versucht, stderr anstelle von stdout zu verwenden, es zeigt genau das gleiche Verhalten.
Martinthenext

3

Ändern Sie die Standardausgabe des rsync-Prozesses so, dass sie nicht gepuffert wird.

p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=0,  # 0=unbuffered, 1=line-buffered, else buffer-size
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

3
Das Puffern erfolgt auf der rsync-Seite, das Ändern des bufsize-Attributs auf der Python-Seite hilft nicht.
Nosklo

14
Für alle anderen Suchenden ist die Antwort von nosklo völlig falsch: Die Fortschrittsanzeige von rsync wird nicht gepuffert. Das eigentliche Problem besteht darin, dass der Unterprozess ein Dateiobjekt zurückgibt und die Datei-Iterator-Schnittstelle selbst bei bufsize = 0 über einen schlecht dokumentierten internen Puffer verfügt. Sie müssen readline () wiederholt aufrufen, wenn Sie Ergebnisse benötigen, bevor der Puffer gefüllt wird.
Chris Adams

3

Um das Zwischenspeichern der Ausgabe zu vermeiden, sollten Sie pexpect ausprobieren.

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

PS : Ich weiß, dass diese Frage ziemlich alt ist und immer noch die Lösung bietet, die für mich funktioniert hat.

PPS : Ich habe diese Antwort von einer anderen Frage erhalten


3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

Ich schreibe eine GUI für rsync in Python und habe die gleichen Probleme. Dieses Problem hat mich mehrere Tage lang beunruhigt, bis ich es in pyDoc finde.

Wenn universal_newlines True ist, werden die Dateiobjekte stdout und stderr im universellen Newlines-Modus als Textdateien geöffnet. Zeilen können durch '\ n', die Unix-End-of-Line-Konvention, '\ r', die alte Macintosh-Konvention oder '\ r \ n', die Windows-Konvention, beendet werden. Alle diese externen Darstellungen werden vom Python-Programm als '\ n' angesehen.

Es scheint, dass rsync '\ r' ausgibt, wenn die Übersetzung ausgeführt wird.


1

Ich habe festgestellt, dass die Verwendung einer temporären Datei als Zwischenprodukt nicht erwähnt wird. Im Folgenden werden die Pufferungsprobleme durch Ausgabe in eine temporäre Datei umgangen und Sie können die von rsync kommenden Daten analysieren, ohne eine Verbindung zu einer pty herzustellen. Ich habe Folgendes auf einer Linux-Box getestet, und die Ausgabe von rsync unterscheidet sich tendenziell zwischen den Plattformen, sodass die regulären Ausdrücke zum Analysieren der Ausgabe variieren können:

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...

es ist nicht in Echtzeit. Eine Datei löst kein Pufferproblem auf der Seite von rsync.
JFS

tempfile.TemporaryFile kann sich selbst löschen, um die Bereinigung bei Ausnahmen zu vereinfachen
jfs

3
while not p.poll()führt zu einer Endlosschleife, wenn der Unterprozess mit 0 erfolgreich beendet wird. Verwenden Sie p.poll() is Nonestattdessen
jfs

Windows verbietet möglicherweise das Öffnen bereits geöffneter Dateien und open(file_name)
jfs

1
Ich habe diese Antwort gerade gefunden, leider nur für Linux, aber sie funktioniert wie ein Charm- Link. Also erweitere ich meinen Befehl wie folgt: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argvund rufe auf: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) und jetzt kann ich ohne Pufferung auslesen
Arvid Terzibaschian

0

Wenn Sie so etwas in einem Thread ausführen und die Eigenschaft ffmpeg_time in einer Eigenschaft einer Methode speichern, damit Sie darauf zugreifen können, funktioniert dies sehr gut. Ich erhalte folgende Ausgaben: Die Ausgabe ist so, als ob Sie Threading in tkinter verwenden

input = 'path/input_file.mp4'
output = 'path/input_file.mp4'
command = "ffmpeg -y -v quiet -stats -i \"" + str(input) + "\" -metadata title=\"@alaa_sanatisharif\" -preset ultrafast -vcodec copy -r 50 -vsync 1 -async 1 \"" + output + "\""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
for line in self.process.stdout:
    reg = re.search('\d\d:\d\d:\d\d', line)
    ffmpeg_time = reg.group(0) if reg else ''
    print(ffmpeg_time)

-1

In Python 3 gibt es eine Lösung, die einen Befehl von der Befehlszeile entfernt und beim Empfang gut dekodierte Zeichenfolgen in Echtzeit liefert.

Empfänger (receiver.py ):

import subprocess
import sys

cmd = sys.argv[1:]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in p.stdout:
    print("received: {}".format(line.rstrip().decode("utf-8")))

Beispiel eines einfachen Programms, das eine Echtzeitausgabe erzeugen könnte (dummy_out.py ):

import time
import sys

for i in range(5):
    print("hello {}".format(i))
    sys.stdout.flush()  
    time.sleep(1)

Ausgabe:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
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.