Вопрос-Ответ

How do I write to a Python subprocess' stdin?

Как мне записать в подпроцесс Python ' stdin?

Я пытаюсь написать скрипт на Python, который запускает подпроцесс и выполняет запись в stdin подпроцесса. Я также хотел бы иметь возможность определять действие, которое необходимо предпринять в случае сбоя подпроцесса.

Процесс, который я пытаюсь запустить, называется программойnuke, которая имеет свою собственную встроенную версию Python, которой я хотел бы иметь возможность отправлять команды, а затем указывать ей завершить работу после выполнения команд. До сих пор я выяснял, что если я запускаю Python в командной строке, например, а затем запускаю nuke как подпроцесс, то я могу вводить команды в nuke, но я хотел бы иметь возможность поместить все это в скрипт, чтобы основная программа Python могла запускаться nuke, а затем записывать в свой стандартный ввод (и, следовательно, во встроенную версию Python) и указывать ему делать шикарные вещи, поэтому я написал скрипт, который начинается nuke следующим образом:

subprocess.call(["C:/Program Files/Nuke6.3v5/Nuke6.3", "-t", "E:/NukeTest/test.nk"])

Тогда ничего не происходит, потому что nuke ожидает ввода пользователем. Как бы я теперь записал в стандартный ввод?

Я делаю это, потому что я запускаю плагин с nuke, который приводит к периодическому сбою при рендеринге нескольких кадров. Итак, я бы хотел, чтобы этот скрипт мог запускаться nuke, скажите ему, чтобы он что-то сделал, а затем, если произойдет сбой, попробуйте еще раз. Итак, если есть способ зафиксировать сбой и при этом все будет в порядке, то это было бы здорово.

Переведено автоматически
Ответ 1

Возможно, было бы лучше использовать communicate:

from subprocess import Popen, PIPE, STDOUT
p = Popen(['myapp'], stdout=PIPE, stdin=PIPE, stderr=PIPE, text=True)
stdout_data = p.communicate(input='data_to_write')[0]

"Лучше" из-за этого предупреждения:


Используйте communicate() вместо .stdin.write, .stdout.read или .stderr.read, чтобы избежать взаимоблокировок из-за заполнения любого из других буферов канала ОС и блокирования дочернего процесса.


Ответ 2

Чтобы прояснить некоторые моменты:

Как jro упоминал, правильный способ - использовать subprocess.communicate.

Тем не менее, при подаче stdin using subprocess.communicate с помощью input вам необходимо инициировать подпроцесс с помощью stdin=subprocess.PIPE согласно docs.


Обратите внимание, что если вы хотите отправить данные в stdin процесса, вам нужно создать объект Popen с помощью stdin=PIPE . Аналогично, чтобы получить что-либо, кроме None, в результирующем кортеже, вам также нужно указать stdout=PIPE и / или stderr=PIPE .


Также qed упомянул в комментариях, что для Python 3.4 вам нужно закодировать строку, что означает, что вам нужно передавать байты в input, а не в string . Это не совсем верно. Согласно документации, если потоки были открыты в текстовом режиме, вводом должна быть строка (источник - та же страница).


Если потоки были открыты в текстовом режиме, ввод должен быть строкой. В противном случае это должны быть байты.


Итак, если потоки не были открыты явно в текстовом режиме, то должно сработать что-то вроде приведенного ниже:

import subprocess
command = ['myapp', '--arg1', 'value_for_arg1']
p = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = p.communicate(input='some data'.encode())[0]

Я намеренно оставил stderr значение выше в качестве STDOUT примера.

При этом иногда вам может потребоваться вывод другого процесса, а не создавать его с нуля. Допустим, вы хотите запустить эквивалент echo -n 'CATCH\nme' | grep -i catch | wc -m. Обычно это должно возвращать символы числа в 'CATCH' плюс символ новой строки, что приводит к 6. Смысл echo здесь в том, чтобы передать CATCH\nme данные в grep. Таким образом, мы можем передать данные в grep с помощью stdin в цепочке подпроцессов Python в качестве переменной, а затем передать стандартный вывод в виде КАНАЛА в wc процесс' stdin (тем временем, избавьтесь от лишнего символа перевода строки):

import subprocess

what_to_catch = 'catch'
what_to_feed = 'CATCH\nme'

# We create the first subprocess, note that we need stdin=PIPE and stdout=PIPE
p1 = subprocess.Popen(['grep', '-i', what_to_catch], stdin=subprocess.PIPE, stdout=subprocess.PIPE)

# We immediately run the first subprocess and get the result
# Note that we encode the data, otherwise we'd get a TypeError
p1_out = p1.communicate(input=what_to_feed.encode())[0]

# Well the result includes an '\n' at the end,
# if we want to get rid of it in a VERY hacky way
p1_out = p1_out.decode().strip().encode()

# We create the second subprocess, note that we need stdin=PIPE
p2 = subprocess.Popen(['wc', '-m'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)

# We run the second subprocess feeding it with the first subprocess' output.
# We decode the output to convert to a string
# We still have a '\n', so we strip that out
output = p2.communicate(input=p1_out)[0].decode().strip()

Это несколько отличается от ответа здесь, где вы передаете два процесса напрямую, не добавляя данные непосредственно в Python.

Надеюсь, это кому-нибудь поможет.

Ответ 3

Начиная с subprocess 3.5, существует subprocess.run() функция, которая предоставляет удобный способ инициализации и взаимодействия с Popen() объектами. run() принимает необязательный input аргумент, через который вы можете передавать данные stdin (как вы бы использовали Popen.communicate(), но все за один раз).

Адаптация примера jro для использования run() будет выглядеть следующим образом:

import subprocess
p = subprocess.run(['myapp'], input='data_to_write', capture_output=True, text=True)

После выполнения p будет CompletedProcess объект. Устанавливая capture_output значение True, мы делаем доступным p.stdout атрибут, который дает нам доступ к выходным данным, если мы заботимся об этом. text=True сообщает ему, что он должен работать с обычными строками, а не с байтами. Если вы хотите, вы также можете добавить аргумент check=True, чтобы он выдавал ошибку, если состояние выхода (доступное независимо через p.returncode) не равно 0.

Это "современный" / быстрый и простой способ сделать это.

Ответ 4

хотя .communicate() это рекомендуемый способ, он решает не все варианты использования. Для тех, кто хочет записать поток строк и прочитать его преобразование обратно из подпроцесса, вот пример кода.

import sys
from pathlib import Path
import itertools
import subprocess
import threading

def copy_to_stdin(proc, src_file: Path, mt_file: Path):
"""Example task: Write data to subproc stdin.
Note: run this on another thread to avoid deadlocks
This function reads two parallel files (src_file and mt_file), and write them as TSV record to the stdin of the sub process.
:param proc: subprocess object to write to
:param src_file: path to source file
:param mt_file: path to MT file
"""


with src_file.open() as src_lines, mt_file.open() as mt_lines:
for src_line, mt_line in itertools.zip_longest(src_lines, mt_lines):
if src_line is None or mt_line is None:
log.error(f'Input files have different number of lines')
raise ValueError('Input files have different number of lines')
line = src_line.rstrip('\n') + '\t' + mt_line.rstrip('\n') + '\n'
proc.stdin.write(line)
proc.stdin.flush()
proc.stdin.close() # close stdin to signal end of input


cmd_line = ['yourcmd', 'arg1'] # fill your args
src_file, mt_file = ... # your files

proc = subprocess.Popen(cmd_line, shell=False,
stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=sys.stderr, text=True, encoding='utf8', errors='replace')
try:
copy_thread = threading.Thread(target=copy_to_stdin, args=(proc, src_file, mt_file))
copy_thread.start()
# demonstration of reading data from stdout.
for line in proc.stdout:
line = line.rstrip()
print(line)

copy_thread.join()
returncode = proc.wait()
if returncode != 0:
raise RuntimeError(f'Process exited with code {returncode}')
finally:
if proc.returncode is None:
log.warning(f'Killing process {proc.pid}')
proc.kill()
python subprocess