如何向Python子进程的标准输入(stdin)写入数据?

113
我正在尝试编写一个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”,告诉它要做什么,如果它崩溃,则尝试重新启动。因此,如果有一种方法可以捕获崩溃并仍然正常工作,那就太好了。

6
如果您想快速将字符串写入子进程标准输入中,请使用subprocess.runinput; 例如,subprocess.run(['cat'], input='foobar'.encode('utf-8')) - anishpatel
如何多次写入:https://stackoverflow.com/questions/28616018/multiple-inputs-and-outputs-in-python-subprocess-communicate - undefined
6个回答

122

最好使用communicate

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

"更好",因为有这个警告:

使用 communicate() 而不是 .stdin.write、.stdout.read 或 .stderr.read,以避免由于其他操作系统管道缓冲区填满并阻塞子进程而导致死锁。


1
不需要,但是你需要像 subprocess.PIPE 这样引用它们。这种方法还会导入 subprocess 模块中的所有内容。from subprocess import PIPE 将其引入到当前命名空间中,以便您只需使用 PIPE - jro
我使用这种方法唯一的问题是程序在处理时会冻结,我希望Python脚本可以远程启动进程并监视其标准输出。 - jonathan topf
6
communicate 方法会读取数据直到接收到 EOF。如果您想要动态地与进程交互,请使用 p.stdin.write('data') 访问管道。关于读取方面,请参阅我的先前评论。但是要注意,警告是关于这种通信方式的,因此请注意不要填满缓冲区。验证的最简单方法就是试一试... - jro
12
针对 Python 3.4 版本,你需要执行 p.communicate(input="data for input".encode())。该操作将提供输入数据并与进程进行交互。 - qed
3
当您在寻找上述问题的答案时,例如如果您需要执行“ping”命令而communicate无法使用,那么这个答案就毫无帮助。 - UpmostScarab
显示剩余5条评论

22

澄清一些要点:

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

然而,当使用subprocess.communicate并使用input喂养stdin时,您需要根据文档使用stdin=subprocess.PIPE初始化子进程。

请注意,如果要向进程的stdin发送数据,则需要使用stdin=PIPE创建Popen对象。同样,要在结果元组中得到除 None 以外的任何东西,还需要给出stdout=PIPE和/或stderr=PIPE。

此外,qed在评论中提到,对于Python 3.4,您需要对字符串进行编码,这意味着您需要传递字节而不是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 subprocess链中的stdin传递给grep,并将stdout作为PIPE传递给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中直接将两个进程连接而没有直接添加数据。

希望能对某人有所帮助。


18
自从 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,则会抛出错误。
这是一种现代、快速而简单的方法。

2
请注意,text仅最近才出现,为了与旧版本兼容,您可能需要使用input='data_to_write'.encode('utf-8') - The Godfather
@The Godfather 好观点。显然 text 关键字参数是在 3.7 版本中引入的。考虑到 run() 本身是在 3.5 版本中添加的,因此 Python 3.5 和 3.6 确实需要关注这个问题。 - L0tad

1
虽然推荐使用.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()

0

可以实时向子进程对象写入数据,而不是事先将所有输入收集到一个字符串中通过communicate()方法传递。

此示例将动物名称列表发送到Unix实用程序sort,并将输出发送到标准输出。

import sys, subprocess
p = subprocess.Popen('sort', stdin=subprocess.PIPE, stdout=sys.stdout)
for v in ('dog','cat','mouse','cow','mule','chicken','bear','robin'):
    p.stdin.write( v.encode() + b'\n' )
p.communicate()

请注意,将内容写入进程是通过 p.stdin.write(v.encode()) 完成的。我尝试使用 print(v.encode(), file=p.stdin),但是会出现错误信息 TypeError: a bytes-like object is required, not 'str'。我还没有找到如何让 print() 与此一起工作的方法。

-2

你可以将类似文件的对象提供给subprocess.call()stdin参数。

Popen对象的文档也适用于此处。

要捕获输出,你应该使用subprocess.check_output(),它接受类似的参数。从文档中可以看到:

>>> subprocess.check_output(
...     "ls non_existent_file; exit 0",
...     stderr=subprocess.STDOUT,
...     shell=True)
'ls: non_existent_file: No such file or directory\n'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接