使用subprocess.Popen处理大量输出的进程
我有一些Python代码,它可以运行一个外部应用程序。当这个应用程序的输出很少时,一切都正常,但如果输出很多,就会卡住。我的代码是这样的:
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
errcode = p.wait()
retval = p.stdout.read()
errmess = p.stderr.read()
if errcode:
log.error('cmd failed <%s>: %s' % (errcode,errmess))
文档中有一些评论似乎提到了可能的问题。在“wait”部分,有这么一句:
警告:如果子进程生成的输出太多,导致等待操作系统的管道缓冲区接受更多数据时发生死锁。这种情况下请使用
communicate()
来避免。
而在“communicate”部分,我看到:
注意:读取的数据会缓存在内存中,所以如果数据量很大或没有限制,请不要使用这种方法。
所以我不太清楚在面对大量数据时应该用哪种方法。这两者都没有说明我在这种情况下该用什么。
我确实需要从执行中获取返回值,并且会解析和使用stdout
和stderr
的内容。
那么在Python中,有什么等效的方法可以用来执行一个输出量大的外部应用程序呢?
7 个回答
Glenn Maynard在他的评论中提到的死锁问题是对的。不过,解决这个问题的最好方法是创建两个线程,一个用来处理标准输出(stdout),另一个用来处理错误输出(stderr),这两个线程会分别读取这些输出,直到没有数据为止,然后你可以根据需要处理这些输出。
使用临时文件的建议可能对你有用,也可能没用,这取决于输出的大小等因素,以及你是否需要实时处理子进程生成的输出。
正如Heikki Toivonen所建议的,你应该看看communicate
这个方法。不过,这个方法会把子进程的标准输出和错误输出都存储在内存中,然后你从communicate
调用中得到这些数据——在某些情况下,这并不是最理想的选择。不过,了解communicate
方法的源代码是很有价值的。
另一个例子是在我维护的一个包中,python-gnupg,在这个包里,gpg
可执行文件是通过subprocess
启动的,用来完成繁重的工作,而Python的包装器则会启动线程来读取gpg的标准输出和错误输出,并在gpg生成数据时进行处理。你也许可以通过查看那里的源代码获得一些灵感,因为gpg生成的标准输出和错误输出在一般情况下可能会非常大。
在处理非常大的输出(比如几兆字节的数据)时,如何独立读取 stdout
(标准输出)和 stderr
(错误输出),可以使用 select
这个工具。
import subprocess, select
proc = subprocess.Popen(cmd, bufsize=8192, shell=False, \
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
with open(outpath, "wb") as outf:
dataend = False
while (proc.returncode is None) or (not dataend):
proc.poll()
dataend = False
ready = select.select([proc.stdout, proc.stderr], [], [], 1.0)
if proc.stderr in ready[0]:
data = proc.stderr.read(1024)
if len(data) > 0:
handle_stderr_data(data)
if proc.stdout in ready[0]:
data = proc.stdout.read(1024)
if len(data) == 0: # Read of zero bytes means EOF
dataend = True
else:
outf.write(data)
你正在从两个文件中读取数据,而第一个文件的读取必须在第二个文件开始之前完成。如果你的程序往 stderr
(错误输出)写了很多东西,而没有往 stdout
(标准输出)写任何东西,那么你的程序就会一直在等 stdout
的数据,但实际上这些数据根本不会到来。同时,你运行的程序也在等它写入 stderr
的内容被读取,但由于你在等 stdout
,所以这些内容永远不会被读取。
有几种方法可以解决这个问题。
最简单的方法是不要拦截 stderr
,直接把它设为 stderr=None
。这样错误信息会直接输出到 stderr
,你无法将它们拦截并作为自己消息的一部分显示出来。对于命令行工具来说,这通常是可以的,但对于其他应用程序来说,可能会有问题。
另一个简单的方法是把 stderr
重定向到 stdout
,这样你就只有一个输入文件了:设置 stderr=STDOUT
。这意味着你无法区分正常输出和错误输出。是否可以接受这点,取决于应用程序是如何输出内容的。
处理这个问题的完整而复杂的方法是使用 select
(http://docs.python.org/library/select.html)。这个方法可以让你以非阻塞的方式读取数据:只要 stdout
或 stderr
有数据出现,你就能读取到。只有在真的必要时,我才会推荐使用这个方法。而且,这个方法在 Windows 系统上可能不太好用。