实时子进程通过stdout和PIPE的Popen

26

我正在尝试从subprocess.Popen调用中获取stdout,虽然我可以通过以下方式轻松实现:

cmd = subprocess.Popen('ls -l', shell=True, stdout=PIPE)
for line in cmd.stdout.readlines():
    print line

我希望能够实时获取stdout。使用上述方法,PIPE在等待抓取所有的stdout,然后才返回结果。
因此,对于日志记录而言,这不符合我的要求(例如,在发生事件时“查看”发生了什么)。
是否有一种方法可以逐行获取正在运行的stdout?或者这是subprocess的限制(必须等待PIPE关闭)。
编辑: 如果我将readlines()替换为readline(),那么我只会得到stdout的最后一行(并不理想)。
In [75]: cmd = Popen('ls -l', shell=True, stdout=PIPE)
In [76]: for i in cmd.stdout.readline(): print i
....: 
t
o
t
a
l

1
0
4

请问如何在Python中运行一个exe文件时将命令窗口输出到Windows? - Roger Pate
相关:【Python: 从 subprocess.communicate() 中读取流式输入】(https://dev59.com/iXE85IYBdhLWcg3wkkU8) - jfs
使用 readline() 函数,你可以获取第一行,而不是最后一行。你需要重复调用 readline() 函数来获取每一行。 - Albert
8个回答

23

您的解释器正在缓冲。在打印语句后添加一个调用sys.stdout.flush()。


2
@alfredodeza:这不可能起作用。.readlines() 不会返回,直到读取了 所有输出(直到发生 EOF),因此添加 sys.stdout.flush() 不会改变任何东西。这与您的要求相矛盾:“是否有一种方法可以在 [子进程] 运行时逐行获取 stdout?” - jfs

19

实际上,真正的解决方案是将子进程的标准输出直接重定向到您的进程的标准输出。

事实上,使用您的解决方案,您只能同时打印stdout,而不能同时打印stderr。

import sys
from subprocess import Popen
Popen("./slow_cmd_output.sh", stdout=sys.stdout, stderr=sys.stderr).communicate()

communicate() 是为了让调用在子进程结束前阻塞,否则它会直接跳到下一个行并且你的程序可能会在子进程之前终止(尽管重定向到你的 stdout 仍将工作,即使在你的 Python 脚本关闭之后,我已经测试过)。

这样,例如,你可以实时地重定向标准输出和错误输出。

例如,在我的情况下,我使用了这个脚本 slow_cmd_output.sh 进行了测试:

#!/bin/bash

for i in 1 2 3 4 5 6; do sleep 5 && echo "${i}th output" && echo "err output num ${i}" >&2; done

2
注意:我知道这个帖子很旧了,但我看到了这个帖子,答案并没有让我满意。找到答案后,我想我也可以发一下 :) - Undo
这个有什么需要注意的地方吗?@Undo - Moshe
这给我一个“UnsupportedOperation”错误。 - cefect

11
为了实时获取输出,subprocess不适用,因为它无法击败其他进程的缓冲策略。这就是我总是建议的原因,每当需要这样的“实时”输出抓取(在堆栈溢出上非常常见!)时,应该使用pexpect代替(除了Windows之外,在Windows上使用wexpect)。

Expect的行缓冲是我最近非常关心的一个话题;你介意看一下我的最新问题吗? - Tobu
@Tobu,好的,我看了一下并回答了(再次推荐使用pexpect)。 - Alex Martelli
此外,在Windows系统中也可以使用winpexpect模块 - jfs
您可以在使用subprocess模块时,也可以提供伪终端(pseudo-tty)。详情请参考:https://dev59.com/OWct5IYBdhLWcg3wAY7q#12471855 - jfs

3
作为一个问题,我花了数天时间寻找答案,现在我希望能够留下这个解决方案供后来的人使用。虽然 subprocess 不能与其他进程的缓冲区策略相抗衡,但在调用另一个 Python 脚本时,您可以通过 subprocess.Popen 告诉它启动一个无缓冲的 python。
command = ["python", "-u", "python_file.py"]
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

for line in iter(p.stdout.readline, ''):
    line = line.replace('\r', '').replace('\n', '')
    print line
    sys.stdout.flush()

我也见过一些情况,其中使用 bufsize=1universal_newlines=True 作为 popen 参数有助于暴露隐藏的 stdout


1
你可以使用print line,(注意逗号)来避免剥离换行符(无需replace('\n','')rstrip(b'\r\n'))。 - jfs

3

我看了那个链接,但我不清楚如何在Python中强制缓冲,你能解释一下吗? - alfredodeza
你可以在命令上强制使用缓冲。tail -f默认使用行缓冲。对于grep、sed等命令,你需要向它们传递适当的选项。还要注意新的stdbuf命令,它可以将行缓冲应用于使用stdio的任何命令。 - pixelbeat

1
cmd = subprocess.Popen(["ls", "-l"], stdout=subprocess.PIPE)
for line in cmd.stdout:
    print line.rstrip("\n")

2
罗杰,这个过程仍在等待结束。我可以确认这一点的方法是运行一个更长的进程,比如系统更新,并将信息传递给日志。在10秒的运行过程中,所有行都同时打印/写入日志。 - alfredodeza
你遇到了缓冲区大小的问题;使用我上面的代码,并将命令更改为["find", "/"],你会在该进程结束之前看到输出。 - Roger Pate
我在Python3中使用print(line.rstrip("\n")),但出现了TypeError: a bytes-like object is required, not 'str'的错误,你有什么想法吗? - ah bon

0

readlines 的调用正在等待进程退出。将其替换为循环,使用 cmd.stdout.readline() (注意是单数形式),一切都应该没问题。


这只返回了最后一行,而不是所有行:for i in cmd.stdout.readline(): print i ....: t o t a l 1 0 4 - alfredodeza
是的,我错了。正确答案是上面由Robert Pate提供的那一个。 - Jakob Borg
有一个来自Robert Pate的答案(至少搜索Robert找不到)。虽然你的答案是不正确的,但你应该删除它... - Betlista

0
如前所述,问题在于stdio库对printf语句的缓冲处理,当进程没有连接终端时会出现此问题。在Windows平台上有一种解决方法,其他平台可能也有类似的解决方案。
在Windows上,您可以在进程创建时强制创建一个新控制台。好处是这可以保持隐藏,因此您永远不会看到它(这是通过子进程模块中的shell=True完成的)。
cmd = subprocess.Popen('ls -l', shell=True, stdout=PIPE, creationflags=_winapi.CREATE_NEW_CONSOLE, bufsize=1, universal_newlines=True)
for line in cmd.stdout.readlines():
    print line

或者

一个稍微更完整的解决方案是,你可以显式地设置STARTUPINFO参数,这可以防止启动一个新的不必要的cmd.exe shell进程,而shell=True则会执行此操作。

class PopenBackground(subprocess.Popen):
    def __init__(self, *args, **kwargs):

        si = kwargs.get('startupinfo', subprocess.STARTUPINFO())
        si.dwFlags |= _winapi.STARTF_USESHOWWINDOW
        si.wShowWindow = _winapi.SW_HIDE

        kwargs['startupinfo'] = si
        kwargs['creationflags'] = kwargs.get('creationflags', 0) | _winapi.CREATE_NEW_CONSOLE
        kwargs['bufsize'] = 1
        kwargs['universal_newlines'] = True

        super(PopenBackground, self).__init__(*args, **kwargs)

process = PopenBackground(['ls', '-l'], stdout=subprocess.PIPE)
    for line in cmd.stdout.readlines():
        print line

只要使用 .readlines() 并且它不会在 EOF 之前返回,子进程的缓冲策略就无关紧要。建议使用 for line in iter(cmd.stdout.readline, b''):。请参阅我在上面链接问题的回答。 - jfs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接