实时读取子进程的标准输出

20

考虑以下代码片段:

from subprocess import Popen, PIPE, CalledProcessError


def execute(cmd):
    with Popen(cmd, shell=True, stdout=PIPE, bufsize=1, universal_newlines=True) as p:
        for line in p.stdout:
            print(line, end='')

    if p.returncode != 0:
        raise CalledProcessError(p.returncode, p.args)

base_cmd = [
    "cmd", "/c", "d:\\virtual_envs\\py362_32\\Scripts\\activate",
    "&&"
]
cmd1 = " ".join(base_cmd + ['python -c "import sys; print(sys.version)"'])
cmd2 = " ".join(base_cmd + ["python -m http.server"])

如果我运行execute(cmd1),输出将被打印而不会出现任何问题。

然而,如果我运行execute(cmd2),就什么都不会打印出来,为什么会这样,如何修复它以便我能够实时看到http.server的输出。

for line in p.stdout是如何在内部评估的?它是否是一种无限循环,直到达到stdouteof或其他东西?

这个主题已经在SO中多次讨论过,但我没有找到一个Windows解决方案。上面的片段来自于这个答案,我正在从virtualenv中运行http.server(python3.6.2-32位win7)。

5个回答

7
如果您希望持续从正在运行的子进程中读取内容,则必须使该进程的输出无缓冲。 鉴于您的子进程是 Python 程序,可以通过向解释器传递 -u 来实现此目的:

python -u -m http.server

这是在 Windows 上的显示效果。

enter image description here


对我来说工作正常,尽管我没有使用Windows框,因此删除了你的cmd /c ...内容。 - fpbhb
流和缓冲在Windows和其他操作系统上工作方式基本相同。请参见我更新的答案中的截图。你能复现吗? - fpbhb
1
@fpbhb,您可以删除" ".join,因为在列表中执行单个元素时它不起作用。 - Chen A.

7

由于缓冲,您无法看到实时输出的这段代码:

for line in p.stdout:
    print(line, end='')

但如果你使用 p.stdout.readline(),它应该可以工作:

while True:
  line = p.stdout.readline()
  if not line: break
  print(line, end='')

请查看相应的Python错误讨论以获取详情。 更新:在stackoverflow上可以找到几乎相同的问题及各种解决方案

4
我认为主要问题在于http.server在某种程度上将输出记录到了stderr,这里我有一个使用asyncio的示例,可以从stdoutstderr读取数据。

我的第一次尝试是使用asyncio,这是一个很好的API,在Python 3.4中就已经存在了。后来我发现了一个更简单的解决方案,所以你可以选择,两者都应该可以工作。

使用asyncio作为解决方案

在后台,asyncio使用IOCP——Windows API来进行异步处理。

# inspired by https://pymotw.com/3/asyncio/subprocesses.html

import asyncio
import sys
import time

if sys.platform == 'win32':
    loop = asyncio.ProactorEventLoop()
    asyncio.set_event_loop(loop)

async def run_webserver():
    buffer = bytearray()

    # start the webserver without buffering (-u) and stderr and stdin as the arguments
    print('launching process')
    proc = await asyncio.create_subprocess_exec(
        sys.executable, '-u', '-mhttp.server',
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )

    print('process started {}'.format(proc.pid))
    while 1:
        # wait either for stderr or stdout and loop over the results
        for line in asyncio.as_completed([proc.stderr.readline(), proc.stdout.readline()]):
            print('read {!r}'.format(await line))

event_loop = asyncio.get_event_loop()
try:
    event_loop.run_until_complete(run_df())
finally:
    event_loop.close()

重定向stdout输出

根据你的示例,这是一个非常简单的解决方案。它只是将stderr重定向到stdout,并且只有stdout被读取。

from subprocess import Popen, PIPE, CalledProcessError, run, STDOUT import os

def execute(cmd):
    with Popen(cmd, stdout=PIPE, stderr=STDOUT, bufsize=1) as p:
        while 1:
            print('waiting for a line')
            print(p.stdout.readline())

cmd2 = ["python", "-u", "-m", "http.server"]

execute(cmd2)

3
你可以在操作系统层面实现无缓冲行为。
在Linux中,你可以使用stdbuf包装你的现有命令行:
stdbuf -i0 -o0 -e0 YOURCOMMAND

在Windows系统中,您可以使用winpty来包装您现有的命令行:

winpty.exe -Xallow-non-tty -Xplain YOURCOMMAND

我不知道有针对此的操作系统中立工具。


3

p.stdout是一个缓冲区(阻塞)。 当你从一个的缓冲区读取时,你会被阻塞,直到有东西被写入该缓冲区。 一旦有了数据,您就可以获取数据并执行内部部分。

想想linux上的tail -f如何工作:它等待直到文件被写入内容,当文件被写入时将新数据输出到屏幕上。 当没有数据时会发生什么? 等待。 因此,当您的程序达到这行时,它会等待数据并处理它。

您的代码能够正常运行,但作为模型运行时却不能,这可能与此有关。 http.server模块可能会缓存输出。 尝试将-u参数添加到Python中以以非缓冲方式运行该进程:

-u :无缓冲二进制标准输出和错误输出;还有PYTHONUNBUFFERED=x 详情请参阅man页面有关'-u'的内部缓冲区的详细信息

此外,您可能需要尝试将循环更改为for line in iter(lambda: p.stdout.read(1), ''):,因为这会在处理之前每次只读取1个字节。


更新:完整的循环代码是:

for line in iter(lambda: p.stdout.read(1), ''):
    sys.stdout.write(line)
    sys.stdout.flush()

另外,您可以将命令作为字符串传递。尝试将其作为列表传递,每个元素都在自己的插槽中:

cmd = ['python', '-m', 'http.server', ..]

@BPL 我已经更新了我的答案,包括循环内容和另一个建议,你可以尝试一下。 - Chen A.

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