如何向Python子进程的stdin写入数据?

117 投票
7 回答
197287 浏览
提问于 2025-04-17 08:13

我正在尝试写一个Python脚本,目的是启动一个子进程,并向这个子进程的标准输入写入内容。同时,我还想知道如果这个子进程崩溃了,我该怎么处理。

我想启动的程序叫做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,告诉它做某件事,如果崩溃了就再试一次。如果有办法捕捉到崩溃并且还能继续运行,那就太好了。

7 个回答

21

自从 Python 3.5 版本开始,出现了一个叫做 subprocess.run() 的函数,它提供了一种方便的方法来初始化和与 Popen() 对象进行交互。这个 run() 函数可以接受一个可选的 input 参数,通过这个参数,你可以像使用 Popen.communicate() 一样,把数据传递给 stdin,不过这次是一次性完成的。

如果我们把 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,就会抛出一个错误。

这就是一种“现代”的、快速简单的方法来完成这个任务。

23

为了澄清一些要点:

正如 jro 提到的,正确的方法是使用 subprocess.communicate

不过,当你用 subprocess.communicateinputstdin 输入数据时,必须在启动子进程时加上 stdin=subprocess.PIPE,这点可以在文档中找到。

注意,如果你想向进程的 stdin 发送数据,创建 Popen 对象时需要设置 stdin=PIPE。同样,如果你想在结果元组中得到非 None 的内容,也需要设置 stdout=PIPE 和/或 stderr=PIPE

另外,qed 在评论中提到,对于 Python 3.4,你需要对字符串进行编码,也就是说要传递字节(Bytes)而不是字符串(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。所以我们可以在 Python 的子进程链中将数据作为变量传递给 grep 的 stdin,然后将 wc 进程的 stdout 作为 PIPE 传递给它的 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 中直接添加数据。

希望这对某些人有所帮助。

126

可能更好用的是 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,这样可以避免因为其他操作系统的管道缓冲区满了而导致的死锁问题,这会阻塞子进程的运行。

撰写回答