Python子进程:提供标准输入,读取标准输出,然后提供更多标准输入

时间:2019-07-26 18:55:51

标签: python subprocess scientific-software

我正在使用一款名为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”,那么我传递“是”

到目前为止,这与我所能达到的尽可能接近:

#!/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无法生成表面,则它不会破裂-只是通过STDOUT如此说。因此,在我的python脚本中,当我遍历许多蛋白质进行分析时,我需要检查STDOUT以了解是否继续对该蛋白质进行分析。

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

无论如何,这些是现实世界中为什么我希望能够将STDIN推送到子进程,读取STDOUT并仍然能够推送更多STDIN的一些原因。

感谢您的时间!

1 个答案:

答案 0 :(得分: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


python 2 docs中的

[1]

  

readline(limit = -1)

     

从流中读取并返回一行。如果指定了限制,则最多将读取限制字节。

     

对于二进制文件,行终止符始终为b'\ n';对于文本文件,可以使用open()的newline参数来选择识别的行终止符。

来自man 2 read

[2]

#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