执行摘要(或“tl;dr”版本):最多只有一个 subprocess.PIPE 时很容易,否则很难。
也许是时候解释一下subprocess.Popen是如何工作的了。
(警告:这是针对 Python 2.x 的,虽然 3.x 类似;而且我对 Windows 变体很模糊。我对 POSIX 的理解要好得多。)
Popen 函数需要同时处理零到三个 I/O 流。像往常一样,这些标记为 stdin、stdout 和 stderr。
您可以提供:
-
None,表示不想重定向流。它将像往常一样继承这些。请注意,至少在 POSIX 系统上,这并不意味着它将使用 Python 的 sys.stdout,而只是使用 Python 的 actual 标准输出;见最后的演示。
-
int 值。这是一个“原始”文件描述符(至少在 POSIX 中)。 (旁注:PIPE 和 STDOUT 在内部实际上是 ints,但它们是“不可能的”描述符,-1 和 -2。)
- 流——实际上,任何具有
fileno 方法的对象。 Popen 将使用stream.fileno() 找到该流的描述符,然后按照int 值继续。
-
subprocess.PIPE,表示 Python 应该创建一个管道。
-
subprocess.STDOUT(仅适用于 stderr):告诉 Python 使用与 stdout 相同的描述符。这仅在您为stdout 提供(非None)值时才有意义,即便如此,只有在您设置stdout=subprocess.PIPE 时才需要。 (否则,您可以提供与 stdout 相同的参数,例如 Popen(..., stdout=stream, stderr=stream)。)
最简单的情况(没有管道)
如果您什么都不重定向(将所有三个保留为默认的 None 值或提供明确的 None),Pipe 很容易。它只需要剥离子进程并让它运行。或者,如果您重定向到非PIPE——int 或流的fileno()——仍然很容易,因为操作系统会完成所有工作。 Python 只需要剥离子进程,将其标准输入、标准输出和/或标准错误连接到提供的文件描述符。
仍然很简单的案例:一根管道
如果你只重定向一个流,Pipe 仍然很容易。让我们一次选择一个流并观看。
假设您想提供一些stdin,但让stdout 和stderr 不重定向,或者转到文件描述符。作为父进程,您的 Python 程序只需要使用write() 将数据发送到管道中。您可以自己执行此操作,例如:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc
或者您可以将标准输入数据传递给proc.communicate(),然后它会执行上面显示的stdin.write。没有输出返回,所以communicate() 只有另一项真正的工作:它还会为您关闭管道。 (如果您不调用proc.communicate(),则必须调用proc.stdin.close() 来关闭管道,以便子进程知道没有更多数据通过。)
假设您想捕获stdout,但不理会stdin 和stderr。同样,这很简单:只需调用proc.stdout.read()(或等效项),直到没有更多输出。由于proc.stdout() 是一个普通的 Python I/O 流,您可以在其上使用所有普通的构造,例如:
for line in proc.stdout:
或者,同样,您可以使用proc.communicate(),它只是为您执行read()。
如果您只想捕获stderr,它的工作原理与stdout 相同。
在事情变得艰难之前,还有一个技巧。假设您要捕获stdout,还要捕获stderr,但在与标准输出相同的管道上:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
在这种情况下,subprocess“作弊”!好吧,它必须这样做,所以它并不是真正的作弊:它启动子进程时,它的 stdout 和它的 stderr 都指向(单个)管道描述符,该管道描述符反馈给它的父(Python)进程。在父端,再次只有一个管道描述符用于读取输出。所有“stderr”输出都显示在proc.stdout 中,如果您调用proc.communicate(),则stderr 结果(元组中的第二个值)将为None,而不是字符串。
困难的情况:两个或多个管道
当您想使用至少两个管道时,所有问题都会出现。其实subprocess代码本身就有这个位:
def communicate(self, input=None):
...
# Optimization: If we are only using one pipe, or no pipe at
# all, using select() or threads is unnecessary.
if [self.stdin, self.stdout, self.stderr].count(None) >= 2:
但是,可惜的是,我们已经制作了至少两个,也许是三个不同的管道,所以count(None) 返回 1 或 0。我们必须努力做事。
在 Windows 上,这使用 threading.Thread 来累积 self.stdout 和 self.stderr 的结果,并让父线程传递 self.stdin 输入数据(然后关闭管道)。
在 POSIX 上,如果可用,则使用 poll,否则使用 select,以累积输出并提供标准输入。所有这些都在(单个)父进程/线程中运行。
这里需要线程或轮询/选择以避免死锁。例如,假设我们已将所有三个流重定向到三个单独的管道。进一步假设在写入过程暂停之前可以将多少数据填充到管道中的限制很小,等待读取过程从另一端“清除”管道。让我们将这个小限制设置为单个字节,只是为了说明。 (实际上这就是事情的运作方式,只是限制远大于一个字节。)
如果父 (Python) 进程尝试写入多个字节——例如,'go\n' 到 proc.stdin,则第一个字节进入,然后第二个字节导致 Python 进程挂起,等待子进程读取第一个字节字节,清空管道。
同时,假设子进程决定打印一个友好的“Hello!Don't Panic!”问候。 H 进入其 stdout 管道,但 e 导致它挂起,等待其父级读取 H,清空 stdout 管道。
现在我们陷入困境:Python 进程处于睡眠状态,等待说完“go”,而子进程也处于睡眠状态,等待说完“Hello!Don't Panic!”。
subprocess.Popen 代码通过线程或选择/轮询避免了这个问题。当字节可以通过管道时,它们就会通过。当它们不能时,只有一个线程(而不是整个进程)必须休眠——或者,在选择/轮询的情况下,Python 进程同时等待“可以写入”或“数据可用”,写入进程的标准输入仅当有空间时,并且仅在数据准备好时才读取其标准输出和/或标准错误。一旦发送了所有标准输入数据(如果有)并且所有标准输出和/或标准错误数据都已累积,proc.communicate() 代码(实际上是处理多毛情况的_communicate)返回。
如果您想在两个不同的管道上同时读取stdout 和stderr(不管任何stdin 重定向),您也需要避免死锁。这里的死锁场景不同——当你从stdout拉数据时子进程向stderr写了很长的东西,反之亦然——但它仍然存在。
演示
我承诺证明,未重定向的 Python subprocesses 写入底层标准输出,而不是 sys.stdout。所以,这里有一些代码:
from cStringIO import StringIO
import os
import subprocess
import sys
def show1():
print 'start show1'
save = sys.stdout
sys.stdout = StringIO()
print 'sys.stdout being buffered'
proc = subprocess.Popen(['echo', 'hello'])
proc.wait()
in_stdout = sys.stdout.getvalue()
sys.stdout = save
print 'in buffer:', in_stdout
def show2():
print 'start show2'
save = sys.stdout
sys.stdout = open(os.devnull, 'w')
print 'after redirect sys.stdout'
proc = subprocess.Popen(['echo', 'hello'])
proc.wait()
sys.stdout = save
show1()
show2()
运行时:
$ python out.py
start show1
hello
in buffer: sys.stdout being buffered
start show2
hello
请注意,如果添加stdout=sys.stdout,第一个例程将失败,因为StringIO 对象没有fileno。如果您添加stdout=sys.stdout,第二个将省略hello,因为sys.stdout 已被重定向到os.devnull。
(如果您重定向 Python 的 file-descriptor-1,子进程将遵循该重定向。open(os.devnull, 'w') 调用会生成一个 fileno() 大于 2 的流。)