оперативный вывод из команды подпроцесса
Я использую скрипт на Python в качестве драйвера для кода гидродинамики. Когда приходит время запускать симуляцию, я использую subprocess.Popen
для запуска кода, сбора выходных данных из stdout
и stderr
в subprocess.PIPE
--- затем я могу распечатать (и сохранить в лог-файл) выходную информацию и проверить, нет ли ошибок. Проблема в том, что я понятия не имею, как продвигается код. Если я запускаю его непосредственно из командной строки, он выдает мне информацию о том, на какой итерации он выполняется, в какое время, каков следующий временной шаг и т.д.
Есть ли способ как сохранить вывод (для ведения журнала и проверки ошибок), так и создать вывод в реальном времени?
Соответствующий раздел моего кода:
ret_val = subprocess.Popen( run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True )
output, errors = ret_val.communicate()
log_file.write(output)
print output
if( ret_val.returncode ):
print "RUN failed\n\n%s\n\n" % (errors)
success = False
if( errors ): log_file.write("\n\n%s\n\n" % errors)
Изначально я передавал run_command
через tee
, чтобы копия отправлялась непосредственно в файл журнала, а поток по-прежнему выводился непосредственно на терминал - но таким образом я не могу сохранить никаких ошибок (насколько я знаю).
Пока что мое временное решение:
ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, shell=True )
while not ret_val.poll():
log_file.flush()
затем в другом терминале запустите tail -f log.txt
(s.t. log_file = 'log.txt'
).
Переведено автоматически
Ответ 1
TLDR для Python 3:
import subprocess
import sys
with open("test.log", "wb") as f:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for c in iter(lambda: process.stdout.read(1), b""):
sys.stdout.buffer.write(c)
f.buffer.write(c)
У вас есть два способа сделать это, либо создав итератор из read
или readline
функций и выполнив:
import subprocess
import sys
# replace "w" with "wb" for Python 3
with open("test.log", "w") as f:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
# replace "" with b'' for Python 3
for c in iter(lambda: process.stdout.read(1), ""):
sys.stdout.write(c)
f.write(c)
или
import subprocess
import sys
# replace "w" with "wb" for Python 3
with open("test.log", "w") as f:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
# replace "" with b"" for Python 3
for line in iter(process.stdout.readline, ""):
sys.stdout.write(line)
f.write(line)
Или вы можете создать reader
и writer
файл. Передайте writer
в Popen
и прочитайте из reader
import io
import time
import subprocess
import sys
filename = "test.log"
with io.open(filename, "wb") as writer, io.open(filename, "rb", 1) as reader:
process = subprocess.Popen(command, stdout=writer)
while process.poll() is None:
sys.stdout.write(reader.read())
time.sleep(0.5)
# Read the remaining
sys.stdout.write(reader.read())
Таким образом, у вас будут данные, записанные в test.log
так же, как и в стандартном выводе.
Единственное преимущество файлового подхода заключается в том, что ваш код не блокируется. Таким образом, вы можете делать все, что хотите, в то же время и читать, когда захотите, из reader
неблокирующим способом. При использовании PIPE
, read
и readline
функции будут блокироваться до тех пор, пока в канал не будет записан либо один символ, либо строка соответственно.
Ответ 2
Краткое описание (или версия "tl; dr"): это легко, когда есть не более одного subprocess.PIPE
, в противном случае это сложно.
Возможно, пришло время немного объяснить, как subprocess.Popen
это работает.
(Предостережение: это для Python 2.x, хотя 3.x похож; и я довольно нечетко разбираюсь в варианте Windows. Я намного лучше разбираюсь в материалах POSIX.)
Popen
Функция должна обрабатывать потоки ввода-вывода от нуля до трех одновременно. Они обозначаются stdin
, stdout
и stderr
как обычно.
Вы можете предоставить:
None
указывает, что вы не хотите перенаправлять поток. Вместо этого он унаследует их, как обычно. Обратите внимание, что, по крайней мере, в системах POSIX это не означает, что он будет использовать Pythonsys.stdout
, только фактический стандартный вывод Python; смотрите Демонстрацию в конце.- Значение
int
. Это "необработанный" файловый дескриптор (по крайней мере, в POSIX). (Примечание:PIPE
иSTDOUT
на самом делеint
являются s внутри, но являются "невозможными" дескрипторами, -1 и -2.) - Поток - на самом деле, любой объект с
fileno
методом.Popen
найдет дескриптор для этого потока, используяstream.fileno()
, а затем продолжит работу как дляint
значения. subprocess.PIPE
, указывающий, что Python должен создать канал.subprocess.STDOUT
(только дляstderr
): скажите Python использовать тот же дескриптор, что и дляstdout
. Это имеет смысл только в том случае, если вы указали (неNone
) значение дляstdout
, и даже тогда оно необходимо только в том случае, если вы установилиstdout=subprocess.PIPE
. (В противном случае вы можете просто указать тот же аргумент, который вы указали дляstdout
, например,Popen(..., stdout=stream, stderr=stream)
.)
Самые простые случаи (без каналов)
Если вы ничего не перенаправляете (оставьте все три None
значения по умолчанию или укажите явно None
), Pipe
сделать это довольно просто. Нужно просто отключить подпроцесс и дать ему запуститься. Или, если вы перенаправляете на не-PIPE
an int
или поток fileno()
— это все равно легко, поскольку ОС выполняет всю работу. Python просто нужно отключить подпроцесс, подключив его stdin, stdout и / или stderr к предоставленным файловым дескрипторам.
Все еще простой пример: один канал
Если вы перенаправляете только один поток, Pipe
все еще довольно просто. Давайте выбирать по одному потоку за раз и наблюдать.
Предположим, вы хотите предоставить некоторые stdin
, но пусть stdout
и stderr
не перенаправляются или переходят к файловому дескриптору. Как родительский процесс, ваша программа на Python просто должна использовать write()
для отправки данных по каналу. Вы можете сделать это самостоятельно, например:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc
или вы можете передать данные stdin в proc.communicate()
, который затем выполняет stdin.write
показанное выше. Выходные данные не возвращаются, поэтому communicate()
есть только одна другая реальная задача: она также закрывает канал для вас. (Если вы не вызываете proc.communicate()
вы должны вызвать proc.stdin.close()
, чтобы закрыть канал, чтобы подпроцесс знал, что данные больше не поступают.)
Предположим, вы хотите захватить, stdout
но оставить stdin
и stderr
в покое. Опять же, это просто: просто вызывайте proc.stdout.read()
(или эквивалентно), пока больше не будет вывода. Поскольку proc.stdout()
это обычный поток ввода-вывода Python, вы можете использовать в нем все обычные конструкции, например:
for line in proc.stdout:
или, опять же, вы можете использовать proc.communicate()
, который просто выполняет read()
за вас.
Если вы хотите только захватывать stderr
, это работает так же, как с stdout
.
Есть еще один трюк, прежде чем все усложнится. Предположим, вы хотите выполнить захват stdout
, а также захват stderr
но в том же канале, что и стандартный вывод:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
В этом случае subprocess
"обманывает"! Что ж, он должен это делать, так что на самом деле это не обман: он запускает подпроцесс как с его stdout, так и с его stderr, направленными в (одиночный) дескриптор канала, который возвращается к его родительскому процессу (Python). На родительской стороне снова есть только один дескриптор канала для чтения выходных данных. Все выходные данные "stderr" отображаются в proc.stdout
, и если вы вызовете proc.communicate()
, результатом stderr (второе значение в кортеже) будет None
, а не строка.
Сложные случаи: два или более канала
Все проблемы возникают, когда вы хотите использовать как минимум два канала. Фактически, в subprocess
самом коде есть этот бит:
def communicate(self, input=None):
...
# Optimization: If we are only using one pipe, or no pipe at
# all, using select() or threads is unnecessary.
if [self.stdin, self.stdout, self.stderr].count(None) >= 2:
Но, увы, здесь мы создали как минимум два, а может и три разных канала, поэтому count(None)
возвращает либо 1, либо 0. Мы должны действовать сложным способом.
В Windows это используется threading.Thread
для накопления результатов для self.stdout
и self.stderr
, и родительский поток доставляет self.stdin
входные данные (а затем закрывает канал).
В POSIX это использует poll
если доступно, в противном случае select
для накопления выходных данных и доставки входных данных stdin. Все это выполняется в (единственном) родительском процессе / потоке.
Здесь необходимы потоки или poll / select, чтобы избежать взаимоблокировки. Предположим, например, что мы перенаправили все три потока в три отдельных канала. Предположим далее, что существует небольшое ограничение на количество данных, которые могут быть загружены в канал, прежде чем процесс записи будет приостановлен, ожидая, пока процесс чтения "очистит" канал с другого конца. Давайте установим это небольшое ограничение в один байт, просто для иллюстрации. (На самом деле так все и работает, за исключением того, что ограничение намного больше одного байта.)
Если родительский процесс (Python) пытается записать несколько байтов, скажем, 'go\n'
в proc.stdin
, вводится первый байт, а затем второй заставляет процесс Python приостанавливаться, ожидая, пока подпроцесс прочитает первый байт, опустошая канал.
Между тем, предположим, что подпроцесс решает напечатать дружелюбное приветствие "Привет! Не паникуйте!". H
переходит в канал стандартного вывода, но e
заставляет его приостанавливаться, ожидая, пока его родительский файл прочитает это H
, опустошая канал стандартного вывода.
Теперь мы застряли: процесс Python находится в спящем режиме, ожидая, когда закончит говорить "go", и подпроцесс также находится в спящем режиме, ожидая, когда закончит говорить "Hello! Не паникуйте!".
subprocess.Popen
Код позволяет избежать этой проблемы с помощью потоковой передачи или выбора / опроса. Когда байты могут передаваться по каналам, они передаются. Когда они не могут, только поток (не весь процесс) должен переходить в спящий режим - или, в случае выбора / опроса, процесс Python одновременно ожидает "может записать" или "данные доступны", записывает в stdin процесса только тогда, когда есть место, и считывает его stdout и / или stderr только тогда, когда данные готовы. proc.communicate()
Код (фактически _communicate
, в котором обрабатываются сложные случаи) возвращает результат после отправки всех данных stdin (если таковые имеются) и накопления всех данных stdout и / или stderr.
Если вы хотите читать оба stdout
и stderr
по двум разным каналам (независимо от любого stdin
перенаправления), вам также нужно будет избегать взаимоблокировки. Сценарий взаимоблокировки здесь отличается — он возникает, когда подпроцесс записывает что-то длинное в stderr
пока вы извлекаете данные из stdout
, или наоборот — но он все еще существует.
Демонстрация
Я обещал продемонстрировать, что без перенаправления Python subprocess
выполняет запись в базовый стандартный вывод, а не sys.stdout
. Итак, вот некоторый код:
from cStringIO import StringIO
import os
import subprocess
import sys
def show1():
print 'start show1'
save = sys.stdout
sys.stdout = StringIO()
print 'sys.stdout being buffered'
proc = subprocess.Popen(['echo', 'hello'])
proc.wait()
in_stdout = sys.stdout.getvalue()
sys.stdout = save
print 'in buffer:', in_stdout
def show2():
print 'start show2'
save = sys.stdout
sys.stdout = open(os.devnull, 'w')
print 'after redirect sys.stdout'
proc = subprocess.Popen(['echo', 'hello'])
proc.wait()
sys.stdout = save
show1()
show2()
При запуске:
$ python out.py
start show1
hello
in buffer: sys.stdout being buffered
start show2
hello
Обратите внимание, что первая процедура завершится неудачей, если вы добавите stdout=sys.stdout
, поскольку у StringIO
объекта нет fileno
. Во втором будет опущен, hello
если вы добавите, stdout=sys.stdout
поскольку sys.stdout
был перенаправлен на os.devnull
.
(Если вы перенаправите файловый дескриптор Python-1, подпроцесс будет следовать этому перенаправлению. open(os.devnull, 'w')
Вызов создает поток, fileno()
размер которого больше 2.)
Ответ 3
Мы также можем использовать итератор файла по умолчанию для чтения стандартного вывода вместо использования iter construct с readline()
.
import subprocess
import sys
process = subprocess.Popen(
your_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
for line in process.stdout:
sys.stdout.write(line)
Ответ 4
В дополнение ко всем этим ответам, один простой подход также может быть следующим:
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
while process.stdout.readable():
line = process.stdout.readline()
if not line:
break
print(line.strip())
Перебирайте читаемый поток до тех пор, пока он доступен для чтения, и если он получает пустой результат, остановитесь.
Ключевым моментом здесь является то, что readline()
возвращает строку (с \n
в конце) до тех пор, пока есть вывод, и пустую, если она действительно находится в конце.
Надеюсь, это кому-то поможет.