Постійно друкувати вихід підпроцесу під час запуску процесу


201

Для запуску програм з моїх Python-скриптів я використовую наступний метод:

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)

Тож коли я запускаю такий процес Process.execute("mvn clean install"), моя програма чекає, поки процес закінчиться, і тільки тоді я отримую повний результат своєї програми. Це дратує, якщо я запускаю процес, який потребує певного часу.

Чи можу я дозволити моїй програмі записувати вихідний процес за рядком, опитуючи висновок процесу, перш ніж він закінчується в циклі чи щось таке?

** [EDIT] Вибачте, що я не дуже добре шукав, перш ніж розміщувати це питання. Нитка - це насправді ключ. Тут знайдено приклад, який показує, як це зробити: ** Підпроцес Python. Відкрийте з потоку


Тема замість підпроцесу, я думаю
Ant Ant

9
Ні, вам не потрібні нитки. Вся ідея трубопроводів працює, тому що ви можете отримувати читання / запис з процесів під час їх запуску.
tokland

Відповіді:


264

Ви можете використовувати ITER для обробки рядків , як тільки команда виводить їх: lines = iter(fd.readline, ""). Ось повний приклад, який показує типовий випадок використання (завдяки @jfs за допомогу):

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
Я спробував цей код (з програмою, яка потребує значного часу на запуск) і можу підтвердити, що він виводить рядки по мірі їх отримання, а не чекати завершення виконання. Це найкраща відповідь imo.
Ендрю Мартін

11
Примітка. У Python 3 ви можете використовувати for line in popen.stdout: print(line.decode(), end=''). Щоб підтримувати і Python 2, і 3, використовуйте байти буквально: b''інакше lines_iteratorніколи не закінчується на Python 3.
jfs

3
Проблема такого підходу полягає в тому, що якщо процес трохи призупиняється, не записуючи нічого до stdout, більше немає вводу для читання. Вам знадобиться цикл, щоб перевірити, чи завершився процес чи ні. Я спробував це, використовуючи subprocess32 на python 2.7
Har

7
це має працювати. Щоб відполірувати його, ви можете додати bufsize=1(це може покращити продуктивність на Python 2), закрити popen.stdoutтрубу явно (не чекаючи, коли збирання сміття потурбується про неї) та підняти subprocess.CalledProcessError(як check_call(), check_output()зробити). У printPython 2 і 3 заява різна: ви можете використовувати хакерський простір print line,(примітка: кома), щоб уникнути подвоєння всіх нових рядків, як ваш код, і передачі universal_newlines=Trueна Python 3, щоб отримати текст замість байтів - відповідь, пов'язаний з цим .
jfs

6
@binzhang Це не помилка, stdout буферизований за замовчуванням у сценаріях Python (також для багатьох інструментів Unix). Спробуйте execute(["python", "-u", "child_thread.py"]). Більш детальна інформація: stackoverflow.com/questions/14258500 / ...
tokland

84

Гаразд, мені вдалося вирішити це без потоків (будь-які пропозиції, чому краще використовувати теми, оцінюються), використовуючи фрагмент із цього питання Перехоплення строки підпроцесу під час його запуску.

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
Об'єднання коду в ifischer в і tokland працює досить добре (мені довелося змінити print line,в sys.stdout.write(nextline); sys.stdout.flush()іншому випадку, він буде друкувати кожні два рядки , потім знову, це використовує інтерфейс ноутбука IPython, так що, може бути , ще що - то відбувається - .. Незважаючи на це , явно викликаючи flush()роботи.
eacousineau

3
господине, ви моє рятувальне життя !! дійсно дивно , що такого роду речі не вбудовуватися в бібліотеці сама по собі .. тому що якщо я пишу cliapp, я хочу , щоб показати всі , що обробки в циклі миттєво .. s'rsly ..
Holms

3
Може Чи це рішення бути змінено , щоб постійно друкувати як висновок і помилки? Якщо я перейду stderr=subprocess.STDOUTна, stderr=subprocess.PIPEа потім подзвонюprocess.stderr.readline() всередині циклу, я , здається, перервала дуже тупикової , що попереджений про в документації на subprocessмодуль.
davidrmcharles

7
@DavidCharles Я думаю, що ви шукаєте stdout=subprocess.PIPE,stderr=subprocess.STDOUTце захоплення більш жорсткого, і я вважаю (але я не перевіряв), що він також захоплює stdin.
Ендрю Мартін

дякую за очікування виходу коду. Не знав, як це зробити
Віталій Ісаєв

67

Щоб надрукувати вихідний підпроцес по рядку, як тільки його буфер stdout буде спущено на 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)

Зверніть увагу: вам не потрібно p.poll()- петля закінчується, коли буде досягнуто eof. І вам не потрібноiter(p.stdout.readline, '') - помилка, що читається вперед, виправлена ​​в Python 3.

Дивіться також, Python: читайте потоковий вхід з subprocess.communicate () .


3
Це рішення спрацювало на мене. Прийняте вище рішення, яке було подано вище, просто надрукувало для мене друк порожніх рядків.
Кодове ім’я

3
Мені довелося додати sys.stdout.flush (), щоб негайно отримати відбитки.
Кодове ім’я

3
@Codename: вам не потрібно sys.stdout.flush()в батьківському - stdout буферний, якщо він не перенаправлений на файл / трубу, і тому друк lineавтоматично промиває буфер. У sys.stdout.flush()дитини теж не потрібно - -uзамість цього передайте параметр командного рядка.
jfs

1
@Codename: якщо ви хочете використовувати, >тоді запустіть python -u your-script.py > some-file. Примітка: -uваріант, який я згадував вище (використовувати не потрібно sys.stdout.flush()).
jfs

1
@mvidelgauz не потрібно телефонувати p.wait()- він викликається при виході з withблоку. Використовуйте p.returncode.
jfs

8

Насправді це дійсно простий спосіб зробити це, коли ви просто хочете надрукувати вихід:

import subprocess
import sys

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

Тут ми просто вказуємо на підпроцес до власної версії та використовуючи існуючий api успіх чи виняток.


1
Це рішення простіше і чіткіше, ніж рішення @ tokland, для Python 3.6. Я помітив, що оболонка = True аргумент не потрібна.
Добрий Буде

Хороший улов, добра воля. Видаленоshell=True
Андрій Кільце

Дуже вимогливий і прекрасно працює з невеликим кодом. Можливо, вам слід також перенаправити підпроцесор stderr на sys.stderr?
Ману

Ману ви точно можете. Я цього не зробив, тому що спроба в цьому питанні перенаправляла stderr на stdout.
Андрій Кільце

Чи можете ви пояснити, у чому різниця між sys.stdout та subprocess.STDOUT?
Рон Серруя

7

@tokland

спробував ваш код і виправив його для 3,4 та windows dir.cmd - це проста команда dir, збережена як 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
ви можете спростити свій код . iter()і end='\r\n'непотрібні. Python використовує універсальний режим нових рядків за замовчуванням, тобто будь- '\n'який переводиться '\r\n'під час друку. 'latin'це, мабуть, неправильне кодування, яке можна використати universal_newlines=Trueдля отримання виводу тексту в Python 3 (розшифроване за допомогою кращого кодування мови). Не зупиняйтеся .poll(), там можуть бути непрочитані дані. Якщо сценарій Python працює в консолі, то його вихід буферний; ви можете примусити буферизацію ліній за допомогою -uпараметра - тут вам не потрібно flush=True.
jfs

4

Якщо хтось хоче читати з обох stdoutі stderrодночасно за допомогою ниток, ось що я придумав:

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)

Мені просто хотілося поділитися цим, оскільки я закінчився цим питанням, намагаючись зробити щось подібне, але жодна з відповідей не вирішила моєї проблеми. Сподіваємось, це комусь допомагає!

Зауважте, що в моєму випадку використання зовнішній процес вбиває процес, який ми Popen().


1
Мені довелося використовувати щось майже подібне для python2. Хоча щось подібне повинно було надаватися в python2, це не так, що щось подібне - це абсолютно добре.
Стюарт Ексон

3

Для тих, хто намагається відповісти на це питання, щоб отримати stdout із сценарію Python, зауважте, що Python буферизує свій stdout, і тому, можливо, знадобиться деякий час, щоб побачити stdout.

Це можна усунути, додавши наступне після кожного написання stdout у цільовому сценарії:

sys.stdout.flush()

1
Але запускати Python як підпроцес Python в першу чергу шалено. Ваш сценарій повинен бути просто importіншим; вивчіть multiprocessingчи threadingпотрібно паралелізоване виконання.
tripleee

3
@triplee Існує кілька сценаріїв, у яких доцільно запускати Python як підпроцес Python. У мене є ряд пакетних сценаріїв python, які я хочу запускати послідовно, щодня. Вони можуть бути організовані головним сценарієм Python, який ініціює виконання, і надсилає мені електронне повідомлення, якщо дочірній сценарій не працює. Кожен скрипт є пісочницею від іншого - жодних конфліктів імен. Я не паралелюю, тому багатообробка та нарізка не є актуальними.
користувач1379351

Ви також можете запустити іншу програму python, використовуючи інший виконуваний файл python, ніж той, на якому працює головна програма python, наприклад,subprocess.run("/path/to/python/executable", "pythonProgramToRun.py")
Kyle Bridenstine

3

У Python> = 3.5 використовуючи subprocess.runдля мене роботи:

import subprocess

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

(отримання результату під час виконання також працює без shell=True) https://docs.python.org/3/library/subprocess.html#subprocess.run


2
Це не "під час виконання". subprocess.run()Виклик повертає тільки тоді , коли подпроцесс закінчить роботу.
трійчатка

1
Чи можете ви пояснити, як це не "під час виконання"? Щось подібне >>> import subprocess; subprocess.run('top')також видається надрукованим "під час виконання" (а вершина ніколи не закінчується). Можливо, я не розумію якоїсь тонкої різниці?
користувач7017793

Якщо ви перенаправляєте висновок назад на Python, наприклад, stdout=subprocess.PIPEви можете прочитати його лише після topзавершення. Ваша програма Python блокується під час виконання підпроцесу.
трійчатка

1
Правильно, це має сенс. runМетод все ще працює , якщо ви зацікавлені тільки в бачачи виходу , як це генерується. Якщо ви хочете зробити щось з виведенням в python асинхронно, ви маєте рацію, що це не працює.
користувач7017793

3

Щоб відповісти на початкове запитання, найкращим способом IMO є просто перенаправлення підпроцесу stdoutбезпосередньо до програми вашої програми stdout(необов'язково, те саме можна зробити stderr, як у прикладі нижче)

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

3
Не вказуючи нічого для, stdoutі stderrте ж саме робить з меншим кодом. Хоча я вважаю, що явне краще, ніж неявне.
трійчатка

1

Цей PoC постійно зчитує результати з процесу та може отримати доступ до нього за потреби. Зберігається лише останній результат, весь інший вихід відкидається, отже, запобігає зростанню пам’яті 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)

висновок: Ви чітко бачите, що між інтервалом ~ 2,5s виходить тільки нічого між ними.

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

0

Це працює принаймні в Python3.4

import subprocess

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

1
У зв'язку з цим проблема блокується в циклі, поки процес не закінчиться.
трійчатка

0

Жодна з відповідей тут не відповідала всім моїм потребам.

  1. Немає потоків для stdout (немає черг тощо)
  2. Не блокуючи, як мені потрібно перевірити, чи не відбувається інше
  3. Використовуйте PIPE так, як мені потрібно було робити декілька речей, наприклад, потоковий вихід, записувати у файл журналу та повертати рядкову копію результату.

Невеликий фон: я використовую ThreadPoolExecutor для управління пулом ниток, кожен запускаючи підпроцес і виконуючи їх одночасність. (У Python2.7, але це також має працювати в новіших 3.x). Я не хочу використовувати нитки тільки для збору результатів, оскільки я хочу, щоб якомога більше доступних для інших речей (пул з 20 процесів використовував би 40 потоків просто для запуску; 1 для потокового процесу та 1 для stdout ... і більше, якщо ви хочете, що більш жорсткий, я думаю)

Я знімаю багато винятків, і таке тут, тому це засноване на коді, який працює у виробництві. Сподіваюся, я не зіпсував це в копії та вставці. Також відгуки дуже вітаємо!

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

Я впевнений, що тут додаються накладні витрати, але це не викликає занепокоєння в моєму випадку. Функціонально він робить те, що мені потрібно. Єдине, що я не вирішив, це те, що це ідеально підходить для повідомлень журналу, але я бачу, що деякі printповідомлення з’являються пізніше і все відразу.


-2

У Python 3.6 я використав це:

import subprocess

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

1
Це не відповідь на це конкретне питання. Чекати, коли підпроцес закінчиться, перш ніж отримати його вихід, конкретно і саме те, що ОП намагається уникнути. У старій застарілій функції subprocess.call()є деякі бородавки, які фіксуються новішими функціями; в Python 3.6 ви зазвичай використовуєте subprocess.run()для цього; для зручності старіша функція обгортки subprocess.check_output()також доступна - вона повертає фактичний вихід з процесу (цей код повертає лише вихідний код, але навіть потім друкує щось невизначене).
трійчатка
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.