Постоянно печатать вывод подпроцесса во время работы процесса


203

Для запуска программ из моих 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"), моя программа ждет, пока процесс не завершится, и только тогда я получаю полный вывод моей программы. Это раздражает, если я запускаю процесс, который занимает некоторое время, чтобы закончить.

Могу ли я позволить моей программе записывать вывод процесса построчно, опрашивая вывод процесса до его завершения в цикле или что-то еще?

** [РЕДАКТИРОВАТЬ] Извините, я не очень хорошо искать, прежде чем отправлять этот вопрос. Потоки на самом деле ключ. Здесь нашел пример, который показывает, как это сделать: ** Python Subprocess.Popen from the thread


Тема вместо подпроцесса, я думаю
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
Я попробовал этот код (с программой, которая требует значительного времени для запуска) и может подтвердить, что она выводит строки по мере их поступления, а не ожидает завершения выполнения. Это лучший ответ ИМО.
Эндрю Мартин

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

3
Проблема с этим подходом состоит в том, что если процесс немного останавливается без записи чего-либо в стандартный вывод, больше нет ввода для чтения. Вам понадобится цикл, чтобы проверить, завершился ли процесс. Я попробовал это, используя subprocess32 на python 2.7
Har

7
он должен работать. Чтобы bufsize=1исправить это, вы можете добавить (это может улучшить производительность в Python 2), popen.stdoutявно закрыть канал (не дожидаясь, пока сборщик мусора позаботится об этом), и повысить subprocess.CalledProcessError(например check_call(), check_output()сделать). Это printутверждение отличается от Python 2 и 3: вы можете использовать хак софтспейс print line,(примечание: запятая), чтобы избежать удвоения всех новых строк, как это делает ваш код, и передачи universal_newlines=Trueна Python 3, чтобы получить текст вместо байтового ответа .
Jfs

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

84

Хорошо, мне удалось решить это без потоков (любые предложения, почему использование потоков было бы лучше) с помощью фрагмента из этого вопроса Перехват stdout подпроцесса во время его работы

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это захватывает stderr, и я верю (но я не проверял), что он также захватывает stdin.
Эндрю Мартин

спасибо за ожидание кода выхода. Не знал, как это сделать
Виталий Исаев

69

Чтобы выводить вывод подпроцесса построчно, как только его буфер 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
@ Кодовое имя: вам не нужно указывать sys.stdout.flush()в родительском элементе - stdout буферизуется строкой, если он не перенаправлен в файл / канал и, следовательно, печать lineочищает буфер автоматически. Вам тоже не нужен sys.stdout.flush()ребенок - -uвместо этого передайте опцию командной строки.
Jfs

1
@ Кодовое имя: если вы хотите использовать, >запустите 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)

Здесь мы просто указываем подпроцесс на наш собственный stdout и используем существующие API-интерфейсы success или исключений.


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

Хороший улов, добрая воля. Удаленоshell=True
Эндрю Ринг

Очень проницательный и отлично работает с небольшим кодом. Может быть, вам также следует перенаправить подпроцесс stderr в sys.stderr?
Ману

Ману ты конечно можешь. Я не сделал этого, потому что попытка в этом вопросе была перенаправить stderr на стандартный вывод.
Андрей Ринг

Можете ли вы объяснить, в чем разница между 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

Для тех, кто пытается получить ответы на этот вопрос, чтобы получить стандартный вывод из скрипта Python, обратите внимание, что Python буферизует свой стандартный вывод, и поэтому для его просмотра может потребоваться некоторое время.

Это можно исправить, добавив следующее после каждой записи stdout в целевой скрипт:

sys.stdout.flush()

1
Но запуск Python в качестве подпроцесса Python сумасшедший во-первых. Ваш сценарий должен просто importдругой сценарий; посмотрите multiprocessingили threadingесли вам нужно распараллеленное выполнение.
tripleee

3
@triplee Существует несколько сценариев, в которых целесообразно запускать Python как подпроцесс Python. У меня есть несколько пакетных скриптов Python, которые я хочу запускать последовательно, ежедневно. Они могут быть организованы с помощью основного сценария Python, который запускает выполнение, и отправляет мне электронное письмо в случае сбоя дочернего сценария. Каждый сценарий изолирован от другого - никаких конфликтов имен. Я не распараллеливаюсь, поэтому многопроцессорность и многопоточность не актуальны.
user1379351

Вы также можете запустить другую программу 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()Вызов возвращает только тогда , когда подпроцесс закончит работу.
tripleee

1
Можете ли вы объяснить, как это не «во время исполнения»? Нечто подобное >>> import subprocess; subprocess.run('top')также, похоже, печатается «во время выполнения» (а top никогда не заканчивается). Может я не уловил какой-то тонкой разницы?
user7017793

Если вы перенаправите вывод обратно в Python, например, stdout=subprocess.PIPEвы можете прочитать его только после topзавершения. Ваша программа Python заблокирована во время выполнения подпроцесса.
tripleee

1
Да, это имеет смысл. runМетод все еще работает , если вы заинтересованы только в видя выхода , как это генерируется. Если вы хотите что-то сделать с выводом в python асинхронно, вы правы, что это не работает.
user7017793

3

Чтобы ответить на исходный вопрос, лучшим способом IMO является просто перенаправление подпроцесса stdoutнепосредственно на вашу программу stdout(опционально, то же самое можно сделать для stderr, как в примере ниже)

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

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

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,5 с ничего нет.

>>>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
Проблема заключается в том, что он блокируется в цикле до завершения процесса.
tripleee

0

Ни один из ответов здесь не отвечает всем моим потребностям.

  1. Нет потоков для стандартного вывода (нет очередей и т. Д.)
  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()также по-прежнему доступна - она ​​возвращает фактический вывод процесса (этот код будет возвращать только код завершения, но даже вместо этого вывести что-то неопределенное).
tripleee
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.