执行摘要(或“ 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
和STDOUT
实际上int
是内部的,但是是“不可能的”描述符-1和-2。)
- 流-实际上是具有
fileno
方法的任何对象。 Popen
将使用来找到该流的描述符stream.fileno()
,然后按照int
值进行操作。
subprocess.PIPE
,指示Python应该创建一个管道。
subprocess.STDOUT
(stderr
仅适用):告诉Python使用与相同的描述符stdout
。仅当您提供的(非None
)值时才有意义stdout
,即使如此,也只有在设置时才需要它stdout=subprocess.PIPE
。(否则,您可以只提供您提供的相同参数stdout
,例如Popen(..., stdout=stream, stderr=stream)
。)
最简单的情况(无管道)
如果不进行任何重定向(将所有三个都保留为默认None
值或提供明确的None
),Pipe
则非常简单。它只需要剥离子流程并使其运行。或者,如果您重定向到一个非PIPE
-an int
或流是fileno()
-它仍然很容易,因为OS做所有的工作。Python只需要剥离子进程,即可将其stdin,stdout和/或stderr连接到提供的文件描述符。
仍然很容易的情况:一根烟斗
如果仅重定向一个流,那么Pipe
事情仍然很简单。让我们一次选择一个流并观看。
假设你想提供一些stdin
,但让stdout
和stderr
去未重定向,或去文件描述符。作为父进程,您的Python程序只需要用于通过write()
管道发送数据。您可以自己执行此操作,例如:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc
或者您可以将stdin数据传递到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
但与stdout在同一管道上:
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!Do n't Panic!”。问候。在H
进入它的标准输出管道,但e
导致其暂停,等待其家长阅读H
,排空stdout管道。
现在我们陷入困境:Python进程处于睡眠状态,等待说完“ go”,而子进程也处于睡眠状态,等待说完“ Hello!Don Panic!”。
该subprocess.Popen
代码避免了线程化或选择/轮询的问题。当字节可以通过管道时,它们就会通过。如果不能,则只有一个线程(而不是整个进程)必须进入睡眠状态;或者,在选择/轮询的情况下,Python进程同时等待“可以写入”或“可用数据”,然后写入该进程的stdin仅在有空间时,并且仅在数据准备就绪时读取其stdout和/或stderr。一旦发送了所有标准输入数据(如果有的话)并且所有标准输出和/或标准错误数据都已存储,则该proc.communicate()
代码(实际上_communicate
是处理多毛案件的地方)返回。
如果你想同时读取stdout
并stderr
在两个不同的管道(无论任何的stdin
重定向),则需要避免死锁了。此处的死锁情况有所不同-发生在子进程stderr
从中提取数据时写入了很长时间stdout
,反之亦然,但是这种情况仍然存在。
演示
我答应演示未经重定向的python subprocess
写入底层标准输出,而不是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
如果已添加,第二个将省略,stdout=sys.stdout
因为它sys.stdout
已被重定向到os.devnull
。
(如果重定向Python的file-descriptor-1,则子进程将遵循该重定向。该open(os.devnull, 'w')
调用将产生一个fileno()
大于2 的流。)
Popen.poll
在上一个“堆栈溢出”问题中使用。