Ciągłe drukowanie wyniku podprocesu podczas działania procesu

201

Aby uruchomić programy z moich skryptów Python, używam następującej metody:

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)

Więc kiedy uruchamiam taki proces Process.execute("mvn clean install"), mój program czeka, aż proces się zakończy, i dopiero wtedy otrzymuję pełne wyjście mojego programu. Jest to denerwujące, jeśli uruchamiam proces, który zajmuje trochę czasu.

Czy mogę pozwolić mojemu programowi zapisywać dane wyjściowe procesu wiersz po wierszu, odpytując dane wyjściowe procesu, zanim zakończy ono pętlę lub coś takiego?

** [EDYCJA] Przepraszam, nie szukałem zbyt dobrze przed opublikowaniem tego pytania. Wątek jest właściwie kluczem. Znaleziono tutaj przykład, który pokazuje, jak to zrobić: ** Python Subprocess.Popen z wątku

Ingo Fischer
źródło
Wydaje mi się, że wątek zamiast podprocesu
Ant
9
Nie, nie potrzebujesz nici. Cały pomysł instalacji rurowej działa, ponieważ można uzyskać odczyt / zapis z procesów podczas ich działania.
tokland

Odpowiedzi:

264

Można użyć ITER przetwarzać linie jak najszybciej wyjść sterujących nich lines = iter(fd.readline, ""). Oto pełny przykład pokazujący typowy przypadek użycia (podziękowania dla @jfs za pomoc):

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="")
tokland
źródło
24
Wypróbowałem ten kod (z programem, którego uruchomienie zajmuje dużo czasu) i mogę potwierdzić, że wypisuje wiersze po ich otrzymaniu, zamiast czekać na zakończenie wykonywania. To jest najlepsza odpowiedź imo.
Andrew Martin
11
Uwaga: W Pythonie 3 możesz użyć for line in popen.stdout: print(line.decode(), end=''). Aby obsługiwać zarówno Python 2, jak i 3, używaj bajtów dosłownych: w b''przeciwnym razie lines_iteratornigdy nie kończy się na Python 3.
jfs
3
Problem z tym podejściem polega na tym, że jeśli proces zostanie na chwilę wstrzymany bez zapisywania czegokolwiek na standardowe wyjście, nie będzie już żadnych danych wejściowych do odczytu. Będziesz potrzebować pętli, aby sprawdzić, czy proces się zakończył. Próbowałem tego przy użyciu subprocess32 na python 2.7
Har
7
powinno działać. Aby go wypolerować, możesz dodać bufsize=1(może to poprawić wydajność w Pythonie 2), zamknąć popen.stdoutpotok jawnie (bez czekania, aż zajmie się tym śmieci) i podnieść subprocess.CalledProcessError(jak check_call(), check_output()zrób). printStwierdzenie jest inna w Pythonie 2 i 3: można użyć hack Softspace print line,(uwaga: przecinek), aby uniknąć podwajania wszystkie znaki nowej linii jak kodzie robi i przechodząc universal_newlines=Truena Python 3, aby uzyskać tekst zamiast bytes- powiązanej odpowiedź .
jfs
6
@binzhang To nie jest błąd, standardowe wyjście jest buforowane w skryptach Pythona (także dla wielu narzędzi uniksowych). Spróbować execute(["python", "-u", "child_thread.py"]). Więcej informacji: stackoverflow.com/questions/14258500/…
tokland
84

Ok, udało mi się go rozwiązać bez wątków (doceniamy wszelkie sugestie, dlaczego użycie wątków byłoby lepsze) za pomocą fragmentu tego pytania Przechwytywanie standardowego przebiegu podprocesu podczas jego działania

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)
Ingo Fischer
źródło
3
Scalanie kodu ifischera i toklanda działa całkiem dobrze (musiałem zmienić print line,na sys.stdout.write(nextline); sys.stdout.flush(). W przeciwnym razie drukowałby co dwa wiersze. Z drugiej strony używa to interfejsu Notatnika IPython, więc może działo się coś innego - niezależnie od jawnego wywołania flush()działa.
eacousineau
3
Panie, jesteś moim ratownikiem! Naprawdę dziwne, że tego rodzaju rzeczy nie są wbudowany w bibliotece sama .. bo jeśli piszę cliapp, chcę pokazać wszystko, co jest przetwarzanie w pętli natychmiast .. s'rsly ..
holms
3
Czy to rozwiązanie można zmodyfikować w celu ciągłego drukowania zarówno dane wyjściowe, jak i błędy? Jeśli zmienię stderr=subprocess.STDOUTsię stderr=subprocess.PIPE, a następnie zadzwonić process.stderr.readline()od wewnątrz pętli, wydaje mi się uruchomić w konflikt z bardzo impasu, który ostrzegał w dokumentacji do subprocessmodułu.
davidrmcharles,
7
@DavidCharles Myślę, że to, czego szukasz, stdout=subprocess.PIPE,stderr=subprocess.STDOUTto przechwytywanie stderr i wierzę (ale nie testowałem), że przechwytuje także stdin.
Andrew Martin
dzięki za czekanie na kod wyjścia. Nie wiedziałem, jak to rozwiązać
Witalij Iwajew
67

Aby wydrukować wynik podprocesu wiersz po wierszu, jak tylko bufor stdout zostanie opróżniony w Pythonie 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)

Uwaga: nie potrzebujesz p.poll()- pętla kończy się po osiągnięciu eof. I nie potrzebujesz iter(p.stdout.readline, '')- błąd odczytu z wyprzedzeniem został naprawiony w Pythonie 3.

Zobacz także Python: odczyt danych strumieniowych z subprocess.communicate () .

jfs
źródło
3
To rozwiązanie działało dla mnie. Akceptowane rozwiązanie podane powyżej dotyczyło właśnie drukowania dla mnie pustych linii.
Kryptonim
3
Musiałem dodać sys.stdout.flush (), aby natychmiast uzyskać odbitki.
Codename
3
@Codename: nie powinno być potrzebne sys.stdout.flush()w obiekcie nadrzędnym - stdout jest buforowany w linii, jeśli nie jest przekierowywany do pliku / potoku, a zatem drukowanie lineautomatycznie opróżnia bufor. Nie potrzebujesz też sys.stdout.flush()w dziecku - -uzamiast tego podaj opcję wiersza poleceń.
jfs
1
@ Nazwa pliku: jeśli chcesz użyć, >uruchom python -u your-script.py > some-file. Uwaga: -uopcja, o której wspomniałem powyżej (nie trzeba jej używać sys.stdout.flush()).
jfs
1
@ mvidelgauz nie trzeba dzwonić p.wait()- jest wywoływany przy wyjściu z withbloku. Zastosowanie p.returncode.
jfs
8

Jest naprawdę bardzo prosty sposób, aby to zrobić, gdy chcesz po prostu wydrukować wynik:

import subprocess
import sys

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

Tutaj po prostu wskazujemy podproces na nasze standardowe wyjście i wykorzystujemy istniejące API powodzenia lub wyjątku.

Andrew Ring
źródło
1
To rozwiązanie jest prostsze i czystsze niż rozwiązanie @ tokland dla Pythona 3.6. Zauważyłem, że argument shell = True nie jest konieczny.
Dobra wola
Dobry chwyt, dobra wola. Usuniętoshell=True
Andrew Ring,
Bardzo zdumiewający i działa doskonale z niewielkim kodem. Może powinieneś również przekierować stderr podprocesu na sys.stderr?
Manu
Manu na pewno możesz. Nie zrobiłem tego tutaj, ponieważ próba w pytaniu przekierowała stderr na stdout.
Andrew Ring
Czy możesz wyjaśnić, jaka jest różnica między sys.stdout a subprocess.STDOUT?
Ron Serruya,
7

@tokland

wypróbowałem kod i poprawiłem go dla 3.4, a Windows dir.cmd to proste polecenie dir, zapisane jako plik 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)
użytkownik3759376
źródło
3
możesz uprościć swój kod . iter()i end='\r\n'są niepotrzebne. Python domyślnie używa uniwersalnego trybu nowego wiersza, tzn. Każdy '\n'jest tłumaczony '\r\n'podczas drukowania. 'latin'jest prawdopodobnie nieprawidłowym kodowaniem, możesz użyć go universal_newlines=Truedo uzyskania tekstu w Pythonie 3 (dekodowanego przy użyciu preferowanego kodowania regionalnego). Nie przestawaj .poll(), mogą być buforowane nieprzeczytane dane. Jeśli skrypt Pythona jest uruchomiony w konsoli, wówczas jego dane wyjściowe są buforowane w linii; możesz wymusić buforowanie linii za pomocą -uopcji - nie potrzebujesz flush=Truetutaj.
jfs
4

Jeśli ktoś chce czytać z obu stdouti stderrjednocześnie używać wątków, oto co wymyśliłem:

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)

Po prostu chciałem się tym podzielić, ponieważ skończyłem na tym pytaniu, próbując zrobić coś podobnego, ale żadna z odpowiedzi nie rozwiązała mojego problemu. Mam nadzieję, że to komuś pomaga!

Pamiętaj, że w moim przypadku proces zewnętrzny zabija proces, który my Popen().

Będzie
źródło
1
Musiałem użyć czegoś prawie dokładnie takiego do Python2. Chociaż coś takiego powinno być dostarczone w python2, nie jest tak, że coś takiego jest absolutnie w porządku.
Stuart Axon
3

Dla każdego, kto próbuje odpowiedzieć na to pytanie, aby uzyskać stdout ze skryptu Python, zauważ, że Python buforuje swoje standardowe wyjście, dlatego może minąć trochę czasu, zanim zobaczysz standardowe wyjście.

Można to naprawić, dodając następujące elementy po każdym zapisie standardowym w skrypcie docelowym:

sys.stdout.flush()
użytkownik1379351
źródło
1
Ale uruchamianie Pythona jako podprocesu Pythona jest szalone. Twój skrypt powinien po prostu importdrugi skrypt; zajrzyj do multiprocessinglub threadingjeśli potrzebujesz wykonania równoległego.
tripleee
3
@triplee Istnieje kilka scenariuszy, w których właściwe jest uruchomienie Pythona jako podprocesu Pythona. Mam wiele skryptów wsadowych w języku Python, które chcę uruchamiać sekwencyjnie, codziennie. Mogą one być koordynowane przez główny skrypt Pythona, który inicjuje wykonanie i wysyła mi e-maile, jeśli skrypt potomny zawiedzie. Każdy skrypt jest izolowany od drugiego - brak konfliktów nazw. Nie równolegle, więc przetwarzanie wielowątkowe i wątki nie są istotne.
user1379351
Możesz także uruchomić inny program python, używając innego pliku wykonywalnego Python niż ten, na którym działa główny program python, np.subprocess.run("/path/to/python/executable", "pythonProgramToRun.py")
Kyle Bridenstine,
3

W Pythonie> = 3.5 używanie subprocess.rundziała dla mnie:

import subprocess

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

(pobieranie danych wyjściowych podczas wykonywania również działa bez shell=True) https://docs.python.org/3/library/subprocess.html#subprocess.run

użytkownik7017793
źródło
2
To nie jest „podczas wykonywania”. subprocess.run()Wywołanie zwraca tylko wtedy, gdy podproces zakończy działanie.
tripleee
1
Czy możesz wyjaśnić, że nie jest to „podczas wykonywania”? Coś podobnego >>> import subprocess; subprocess.run('top')wydaje się również drukować „podczas wykonywania” (a top nigdy się nie kończy). Może nie rozumiem subtelnej różnicy?
user7017793
Jeśli przekierujesz wyjście z powrotem do Pythona, np. stdout=subprocess.PIPEMożesz go odczytać dopiero po topzakończeniu. Twój program Python jest blokowany podczas wykonywania podprocesu.
tripleee
1
Racja, to ma sens. runMetoda wciąż działa, jeśli jesteś zainteresowany tylko widząc wyjścia, jak to jest generowane. Jeśli chcesz zrobić coś z wyjściem w Pythonie asynchronicznie, masz rację, że to nie działa.
user7017793,
3

Aby odpowiedzieć na pierwotne pytanie, najlepszym sposobem IMO jest po prostu przekierowanie podprocesu stdoutbezpośrednio do twojego programu stdout(opcjonalnie to samo można zrobić dla stderr, jak w przykładzie poniżej)

p = Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
p.communicate()
Alleo
źródło
3
Nieokreślanie niczego stdouti stderrrobi to samo z mniejszym kodem. Chociaż przypuszczam, że wyraźne jest lepsze niż dorozumiane.
tripleee
1

Ten PoC stale odczytuje dane wyjściowe z procesu i można uzyskać do nich dostęp w razie potrzeby. Zachowywany jest tylko ostatni wynik, wszystkie inne dane wyjściowe są odrzucane, co zapobiega wzrostowi pamięci PIPE:

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)

Dane wyjściowe: Możesz wyraźnie zobaczyć, że dane wyjściowe pochodzą tylko z ~ 2,5 s odstępu między nimi.

>>>1520535158.51
>>>1520535161.01
>>>1520535163.51
>>>1520535166.01
Robert Nagtegaal
źródło
0

Działa to przynajmniej w Python3.4

import subprocess

process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE)
for line in process.stdout:
    print(line.decode().strip())
arod
źródło
1
Ma to problem polegający na tym, że blokuje się w pętli, dopóki proces się nie zakończy.
tripleee
0

Żadna z odpowiedzi tutaj nie zaspokoiła wszystkich moich potrzeb.

  1. Brak wątków dla standardowego wyjścia (bez kolejek itp.)
  2. Nie blokuje, ponieważ muszę sprawdzić, czy dzieją się inne rzeczy
  3. Użyj PIPE, ponieważ potrzebowałem zrobić wiele rzeczy, np. Strumień wyjściowy, zapisz do pliku dziennika i zwróć kopię ciągu danych wyjściowych.

Małe tło: używam ThreadPoolExecutor do zarządzania pulą wątków, z których każdy uruchamia podproces i uruchamia je jednocześnie. (W Python2.7, ale powinno to również działać w nowszych wersjach 3.x). Nie chcę używać wątków tylko do zbierania danych wyjściowych, ponieważ chcę jak najwięcej dostępnych dla innych rzeczy (pula 20 procesów użyłaby 40 wątków tylko do uruchomienia; 1 dla wątku procesu i 1 dla standardowego wyjścia ... i więcej, jeśli chcesz stderr, tak myślę)

Usuwam tutaj wiele wyjątków i takich, więc jest to oparte na kodzie, który działa w środowisku produkcyjnym. Mam nadzieję, że nie zrujnowałem go w kopii i wkleiłem. Również opinie bardzo mile widziane!

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

Jestem pewien, że dodano tutaj koszty ogólne, ale w moim przypadku nie stanowi to problemu. Funkcjonalnie robi to, czego potrzebuję. Jedyną rzeczą, której nie rozwiązałem, jest to, dlaczego działa to idealnie w przypadku komunikatów w dzienniku, ale widzę, że niektóre printwiadomości pojawiają się później i jednocześnie.

Rafe
źródło
-2

W Pythonie 3.6 użyłem tego:

import subprocess

cmd = "command"
output = subprocess.call(cmd, shell=True)
print(process)
Rajiv Sharma
źródło
1
To nie jest odpowiedź na to pytanie. Oczekiwanie na zakończenie podprocesu przed uzyskaniem jego wyniku jest dokładnie i dokładnie tym, czego OP próbuje uniknąć. Stara starsza funkcja subprocess.call()ma pewne brodawki, które są naprawiane przez nowsze funkcje; w Pythonie 3.6 zwykle byś subprocess.run()do tego użył ; dla wygody starsza funkcja opakowania subprocess.check_output()jest również nadal dostępna - zwraca rzeczywiste dane wyjściowe z procesu (ten kod zwróci tylko kod wyjścia, ale nawet wtedy wydrukuje coś niezdefiniowanego).
tripleee