【问题标题】:Python subprocess: Giving stdin, reading stdout, then giving more stdinPython 子进程:提供标准输入,读取标准输出,然后提供更多标准输入
【发布时间】:2019-12-05 03:06:42
【问题描述】:

我正在使用一款名为 Chimera 的科学软件。本题下游的部分代码,要求我使用Python 2.7。

我想调用一个进程,给那个进程一些输入,读取它的输出,基于它给它更多的输入,等等。

我使用Popen 打开进程,使用process.stdin.write 传递标准输入,但是当进程仍在运行时,我一直在尝试获取输出。 process.communicate() 停止进程,process.stdout.readline() 似乎让我陷入无限循环。


这是我想做的一个简化示例:

假设我有一个名为 exampleInput.sh 的 bash 脚本。

#!/bin/bash
# exampleInput.sh

# Read a number from the input
read -p 'Enter a number: ' num

# Multiply the number by 5
ans1=$( expr $num \* 5 )

# Give the user the multiplied number
echo $ans1

# Ask the user whether they want to keep going
read -p 'Based on the previous output, would you like to continue? ' doContinue

if [ $doContinue == "yes" ]
then
    echo "Okay, moving on..."
    # [...] more code here [...]
else
    exit 0
fi

通过命令行与之交互,我会运行脚本,输入“5”,然后,如果它返回“25”,我会输入“yes”,如果不是,我会输入“no ”。

我想运行一个 python 脚本,我在其中传递 exampleInput.sh "5",如果它返回 "25",那么我传递 "yes"

到目前为止,这是我能得到的最接近的:

#!/home/user/miniconda3/bin/python2
# talk_with_example_input.py
import subprocess
process = subprocess.Popen(["./exampleInput.sh"], 
                        stdin = subprocess.PIPE,
                        stdout = subprocess.PIPE)
process.stdin.write("5")

answer = process.communicate()[0]

if answer == "25":
    process.stdin.write("yes")
    ## I'd like to print the STDOUT here, but the process is already terminated

但这当然失败了,因为在 `process.communicate()' 之后,我的进程不再运行了。


(以防万一/仅供参考):实际问题

Chimera 通常是一个基于 gui 的应用程序,用于检查蛋白质结构。如果您运行chimera --nogui,它将打开一个提示并接受输入。

在运行下一个命令之前,我经常需要知道 chimera 的输出。例如,我经常会尝试生成蛋白质表面,如果 Chimera 无法生成表面,它不会破裂——它只是通过 STDOUT 这么说的。因此,在我的 python 脚本中,当我循环分析许多蛋白质时,我需要检查 STDOUT 以了解是否继续对该蛋白质进行分析。

在其他用例中,我会先通过 Chimera 运行大量命令来清理蛋白质,然后我会想运行大量单独的命令来获取不同的数据,并使用这些数据来决定是否运行其他命令。我可以获取数据,关闭子进程,然后运行另一个进程,但这需要每次都重新运行所有这些清理命令。

无论如何,这些都是我希望能够将 STDIN 推送到子进程、读取 STDOUT 并且仍然能够推送更多 STDIN 的一些实际原因。

感谢您的宝贵时间!

【问题讨论】:

  • 查看this article(特别是“与孩子的标准输入和标准输出直接交互”部分)。看起来您需要对标准输入和标准输出进行一系列 .close().flush() 调用。
  • 没有什么期待?
  • 这正是pexpect 的用途。我强烈建议尝试直接使用 subprocess 进行处理,因为 pexpect 会抽象出大部分流处理细节。

标签: python subprocess scientific-software


【解决方案1】:

您不需要在示例中使用process.communicate

使用process.stdin.writeprocess.stdout.read 简单地读写。还要确保发送换行符,否则read 将不会返回。当您从标准输入读取时,您还必须处理来自echo 的换行符。

注意process.stdout.read 会一直阻塞到EOF

# talk_with_example_input.py
import subprocess

process = subprocess.Popen(["./exampleInput.sh"], 
                        stdin = subprocess.PIPE,
                        stdout = subprocess.PIPE)

process.stdin.write("5\n")
stdout = process.stdout.readline()
print(stdout)

if stdout == "25\n":
    process.stdin.write("yes\n")
    print(process.stdout.readline())
$ python2 test.py
25

Okay, moving on...



更新

以这种方式与程序通信时,您必须特别注意应用程序实际编写的内容。最好是在十六进制编辑器中分析输出:

$ chimera --nogui 2>&1 | hexdump -C

请注意readline [1] 只读取到下一个换行符 (\n)。在您的情况下,您必须调用 readline 至少四次才能获得第一个输出块。

如果您只想在子进程停止打印之前读取所有内容,则必须逐字节读取并实现超时。遗憾的是,readreadline 都没有提供这样的超时机制。这可能是因为底层的 read 系统调用 [2] (Linux) 也没有提供。

在 Linux 上,我们可以使用 poll / select 编写单线程 read_with_timeout()。示例见 [3]

from select import epoll, EPOLLIN

def read_with_timeout(fd, timeout__s):
    """Reads from fd until there is no new data for at least timeout__s seconds.

    This only works on linux > 2.5.44.
    """
    buf = []
    e = epoll()
    e.register(fd, EPOLLIN)
    while True:
        ret = e.poll(timeout__s)
        if not ret or ret[0][1] is not EPOLLIN:
            break
        buf.append(
            fd.read(1)
        )
    return ''.join(buf)

如果您需要一种可靠的方式在 Windows 和 Linux 下读取非阻塞,this answer might be helpful


[1] 来自python 2 docs

readline(limit=-1)

从流中读取并返回一行。如果指定了limit,最多会读取limit个字节。

对于二进制文件,行终止符总是 b'\n';对于文本文件,open() 的换行参数可用于选择识别的行终止符。

[2] 来自man 2 read:

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

[3] 示例

$ tree
.
├── prog.py
└── prog.sh

prog.sh

#!/usr/bin/env bash

for i in $(seq 3); do
  echo "${RANDOM}"
  sleep 1
done

sleep 3
echo "${RANDOM}"

prog.py

# talk_with_example_input.py
import subprocess
from select import epoll, EPOLLIN

def read_with_timeout(fd, timeout__s):
    """Reads from f until there is no new data for at least timeout__s seconds.

    This only works on linux > 2.5.44.
    """
    buf = []
    e = epoll()
    e.register(fd, EPOLLIN)
    while True:
        ret = e.poll(timeout__s)
        if not ret or ret[0][1] is not EPOLLIN:
            break
        buf.append(
            fd.read(1)
        )
    return ''.join(buf)

process = subprocess.Popen(
    ["./prog.sh"],
    stdin = subprocess.PIPE,
    stdout = subprocess.PIPE
)

print(read_with_timeout(process.stdout, 1.5))
print('-----')
print(read_with_timeout(process.stdout, 3))
$ python2 prog.py 
6194
14508
11293

-----
10506


【讨论】:

  • 这确实解决了我的例子,谢谢!不幸的是,由于某种原因,它并没有延续到我的现实问题。使用此代码,与实际输出相比,我的 proc.stdout.readline() 输出被截断了很多。不确定如何为实际案例制作 MWE。我将在下面放一个截图(顶部:python 脚本,bot:首先,从终端输出,然后通过 python 脚本输出)链接:imgur.com/hyNX8Jc
  • 最新的更新正是我所需要的。 python 脚本输出现在与我的命令行输出匹配。这种实现比上面 cmets 中建议的 pexpect 解决方案更简单。感谢您在这方面的不懈努力,Ente!