【问题标题】:Using subprocess.Popen for Process with Large Output使用 subprocess.Popen 处理大输出
【发布时间】:2010-11-13 21:58:18
【问题描述】:

我有一些 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))

文档中的 cmets 似乎表明存在潜在问题。等待中,有:

警告:如果子进程向stdoutstderr 管道生成足够的输出,从而阻塞等待操作系统管道缓冲区接受更多数据,这将导致死锁。使用communicate() 来避免这种情况。

虽然在交流中,但我明白了:

注意读取的数据是缓存在内存中的,所以如果数据量很大或者没有限制,不要使用这种方法。

所以我不清楚如果我有大量数据,我应该使用其中任何一个。他们没有说明在这种情况下我应该使用什么方法。

我确实需要 exec 的返回值,并解析和使用 stdoutstderr

那么,在 Python 中,执行具有大量输出的外部应用程序的等效方法是什么?

【问题讨论】:

  • 通信文档中的“大”似乎比您预期的要大得多,而且肯定比一般情况大得多。例如,您可以输出 10MB 的文本,大多数系统都可以进行通信。当您只有 1GB 的 RAM 时输出 1GB 将是另一回事。

标签: python subprocess


【解决方案1】:

您正在阻止对两个文件的读取;第一个需要在第二个开始之前完成。如果应用程序向stderr 写入了很多内容,而对stdout 没有写入,那么您的进程将等待stdout 上没有出现的数据,而您正在运行的程序则坐在那里等待它的内容写信给stderr 以供阅读(它永远不会——因为你在等待stdout)。

有几种方法可以解决此问题。

最简单的就是不拦截stderr;离开stderr=None。错误将直接输出到stderr。您不能拦截它们并将它们显示为您自己的消息的一部分。对于命令行工具,这通常是可以的。对于其他应用,这可能是个问题。

另一种简单的方法是将stderr 重定向到stdout,因此您只有一个传入文件:设置stderr=STDOUT。这意味着您无法区分常规输出和错误输出。这可能会也可能不会接受,具体取决于应用程序如何写入输出。

完整而复杂的处理方法是select (http://docs.python.org/library/select.html)。这使您可以以非阻塞方式读取:只要数据出现在stdoutstderr 上,您就会获取数据。如果真的有必要,我只会推荐这个。这可能不适用于 Windows。

【讨论】:

  • 由于我正在处理的具体案例会有很多stdout和少量或没有stderr,我将先尝试Mark建议的文件重定向,但更完整的覆盖的问题非常有帮助。
【解决方案2】:

使用select独立读取stdoutstderr,输出非常大(即大量兆字节):

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)

【讨论】:

  • 到目前为止,这对我来说是克服内存缓冲区问题最有意义的。我什至尝试将子进程cmd 设置为bash -c "cat /dev/urandom | tr -dc 'a-zA-Z0-9'",效果很好。我的心理障碍围绕着这些线的含义 - [1] ready[0] 以及为什么 [2] len(proc.stdout.read(1024)) == 0 表示 EOF? [3] 为什么不检查len(proc.stderr.read(1024))? [4] 为什么不需要读取刷新?抱歉,几个问题都集中在一条评论中:/
  • @neowulf33 [1] ready 是列表的列表, ready[0] 是可以包含 stdout、stderr 或两者的列表。请参阅选择文档。 [2] "当立即遇到 EOF 时返回一个空字符串。" docs.python.org/2.7/library/stdtypes.html#file.read [3] 因为你会丢失数据! [4] 没看懂,怎么冲洗?
  • 谢谢!我的错 - 当我写“阅读冲洗”时,我肯定是半睡半醒!
  • [5] 为什么不阅读更多 1024 (1kb)? [6] [proc.stdout, proc.stderr]read[0]8192 (8kb) 有什么关系?谢谢!文档链接 - subprocess.Popenselect.select
  • @neowulf33 [5] [6] 是的,可能我选择的数字 (1024, 8192) 有点随意,它们只是足够大的缓冲区大小,AFAIK 它们没有任何特殊意义.
【解决方案3】:

很多输出是主观的,所以很难做出推荐。如果输出量真的很大,那么您可能不想通过单个 read() 调用来获取所有内容。您可能想尝试将输出写入文件,然后像这样以增量方式提取数据:

f=file('data.out','w')
p = subprocess.Popen(cmd, shell=True, stdout=f, stderr=subprocess.PIPE)
errcode = p.wait()
f.close()
if errcode:
    errmess = p.stderr.read()
    log.error('cmd failed <%s>: %s' % (errcode,errmess))
for line in file('data.out'):
    #do something

【讨论】:

  • 这也很容易死锁。如果分叉的进程写入的数据多于操作系统在以错误代码退出之前缓冲到 stderr 的数据,则此代码将永远等待它退出,而该进程将阻塞写入 stderr 等待您读取它。
  • 1) 假设大数据输出是 stderr,这会很奇怪但并非闻所未闻),2) 如果 stderr 是大数据量的来源,则解决方案相同,请将 stderr 也设为文件
  • 在这种情况下,进程可能有大量的标准输出,但不会有太多的标准错误(如果有的话),所以这对我来说是一个合理的解决方案。
【解决方案4】:

Glenn Maynard 对死锁的评论是正确的。但是,解决此问题的最佳方法是两个创建两个线程,一个用于 stdout,一个用于 stderr,它们读取各自的流直到用尽,然后对输出做任何你需要的事情。

使用临时文件的建议可能对您有用,也可能对您不起作用,具体取决于输出的大小等以及您是否需要在生成子进程的输出时对其进行处理。

正如 Heikki Toivonen 所建议的,您应该查看 communicate 方法。但是,这会将子进程的 stdout/stderr 缓冲在内存中,并且您会得到从 communicate 调用返回的那些 - 这对于某些情况并不理想。但是communication方法的出处还是值得一看的。

另一个例子是在我维护的包python-gnupg 中,其中gpg 可执行文件是通过subprocess 生成的,以执行繁重的工作,Python 包装器生成线程来读取gpg 的stdout 和stderr 并将它们作为数据由 gpg 生成。您也可以通过查看那里的来源获得一些想法。在一般情况下,gpg 生成到 stdout 和 stderr 的数据可能非常大。

【讨论】:

【解决方案5】:

我遇到了同样的问题。如果您必须处理大量输出,另一个不错的选择可能是为 stdout 和 stderr 使用文件,并按参数传递这些文件。

查看python中的tempfile模块:https://docs.python.org/2/library/tempfile.html.

这样的事情可能会奏效

out = tempfile.NamedTemporaryFile(delete=False)

那么你会这样做:

Popen(... stdout=out,...)

然后你就可以读取文件,稍后再擦除它。

【讨论】:

  • 如此简单的解决方案!
【解决方案6】:

您可以尝试沟通,看看是否能解决您的问题。如果没有,我会将输出重定向到一个临时文件。

【讨论】:

  • 如果你有大量输出,通信会明确警告不要使用,我将看看其他选项。
【解决方案7】:

这是一个简单的方法,它同时捕获常规输出和错误输出,所有这些都在 Python 中,因此stdout 中的限制不适用:

com_str = 'uname -a'
command = subprocess.Popen([com_str], stdout=subprocess.PIPE, shell=True)
(output, error) = command.communicate()
print output

Linux 3.11.0-20-generic SMP Fri May 2 21:32:55 UTC 2014 

com_str = 'id'
command = subprocess.Popen([com_str], stdout=subprocess.PIPE, shell=True)
(output, error) = command.communicate()
print output

uid=1000(myname) gid=1000(mygrp) groups=1000(cell),0(root)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-04
    • 2018-06-18
    • 2011-10-13
    • 2012-11-07
    • 1970-01-01
    • 1970-01-01
    • 2017-08-21
    相关资源
    最近更新 更多