执行摘要(或“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 的实际标准输出;见最后的演示。
- 一个
int
值。这是一个“原始”文件描述符(至少在 POSIX 中)。(旁注:PIPE
andSTDOUT
实际上int
在内部是 s,但是是“不可能的”描述符,-1 和 -2。)
- 流——实际上,任何具有
fileno
方法的对象。 Popen
将使用 找到该流的描述符,stream.fileno()
然后继续作为int
值。
subprocess.PIPE
,表示 Python 应该创建一个管道。
subprocess.STDOUT
(stderr
仅用于):告诉 Python 使用与 for 相同的描述符stdout
。None
这仅在您为 提供(非)值时才有意义,即使那样,只有在您设置stdout
时才需要stdout=subprocess.PIPE
它。(否则,您可以只提供您为 提供的相同参数stdout
,例如,Popen(..., stdout=stream, stderr=stream)
。)
最简单的情况(没有管道)
如果您什么都不重定向(将所有三个保留为默认None
值或提供显式None
),Pipe
这很容易。它只需要剥离子进程并让它运行。或者,如果您重定向到非PIPE
-anint
或流的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
离开。同样,这很容易:只需调用(或等效的)直到没有更多输出。由于它是一个普通的 Python I/O 流,因此您可以在其上使用所有普通构造,例如:stdin
stderr
proc.stdout.read()
proc.stdout()
for line in proc.stdout:
或者,你可以再次使用proc.communicate()
,它只是read()
为你做的。
如果您只想捕获stderr
,它的工作原理与 with 相同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'
to proc.stdin
,则第一个字节进入,然后第二个字节导致 Python 进程挂起,等待子进程读取第一个字节,从而清空管道。
同时,假设子进程决定打印一个友好的“Hello!Don't Panic!”。问候。H
进入其标准输出管道,但导致e
它暂停,等待其父级读取H
,清空标准输出管道。
现在我们陷入了困境:Python 进程处于休眠状态,等待说完“go”,而子进程也处于休眠状态,等待说完“Hello!Don't Panic!”。
该subprocess.Popen
代码通过线程或选择/轮询避免了这个问题。当字节可以通过管道时,它们就会通过。当它们不能时,只有一个线程(而不是整个进程)必须休眠——或者,在选择/轮询的情况下,Python 进程同时等待“可以写入”或“数据可用”,写入进程的标准输入仅当有空间时,并且仅在数据准备好时才读取其标准输出和/或标准错误。一旦发送了所有标准输入数据(如果有)并且所有标准输出和/或标准错误数据都已累积,代码(实际上是处理毛茸茸的情况的地方)返回proc.communicate()
。_communicate
如果你想同时读取stdout
两个stderr
不同的管道(不管任何stdin
重定向),你也需要避免死锁。这里的死锁场景是不同的——它发生在stderr
当你从 中提取数据时子进程写了很长的东西stdout
,反之亦然——但它仍然存在。
演示
我承诺证明,未重定向的 Python subprocess
es 写入底层标准输出,而不是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
。第二个将省略hello
if you add stdout=sys.stdout
since sys.stdout
has been redirected to os.devnull
。
(如果您重定向 Python 的 file-descriptor-1,子进程将遵循该重定向。该open(os.devnull, 'w')
调用会产生一个fileno()
大于 2 的流。)