子进程命令的实时输出

Dil*_*rix 165 python error-handling shell logging subprocess

我正在使用python脚本作为流体动力学代码的驱动程序.当运行模拟时,我subprocess.Popen用来运行代码,从stdout和stderr收集输出到subprocess.PIPE---然后我可以打印(并保存到日志文件)输出信息,并检查是否有任何错误.问题是,我不知道代码是如何进展的.如果我直接从命令行运行它,它会给我输出关于它在什么时间迭代,什么时间,下一个时间步骤是什么等等的输出.

有没有办法既存储输出(用于记录和错误检查),还产生实时流输出?

我的代码的相关部分:

ret_val = subprocess.Popen( run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True )
output, errors = ret_val.communicate()
log_file.write(output)
print output
if( ret_val.returncode ):
    print "RUN failed\n\n%s\n\n" % (errors)
    success = False

if( errors ): log_file.write("\n\n%s\n\n" % errors)
Run Code Online (Sandbox Code Playgroud)

最初我正在run_command通过管道,tee以便副本直接进入日志文件,并且流仍然直接输出到终端 - 但这样我就不能存储任何错误(对我的知识).


编辑:

临时解决方案:

ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, shell=True )
while not ret_val.poll():
    log_file.flush()
Run Code Online (Sandbox Code Playgroud)

然后,在另一个终端,运行tail -f log.txt(st log_file = 'log.txt').

Vik*_*kez 151

有两种方法可以通过从readreadline函数创建迭代器来执行此操作:

import subprocess
import sys
with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for c in iter(lambda: process.stdout.read(1), ''):  # replace '' with b'' for Python 3
        sys.stdout.write(c)
        f.write(c)
Run Code Online (Sandbox Code Playgroud)

要么

import subprocess
import sys
with open('test.log', 'w') as f:  # replace 'w' with 'wb' for Python 3
    process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
    for line in iter(process.stdout.readline, ''):  # replace '' with b'' for Python 3
        sys.stdout.write(line)
        f.write(line)
Run Code Online (Sandbox Code Playgroud)

或者您可以创建一个reader和一个writer文件.传递writerPopen并读取reader

import io
import time
import subprocess
import sys

filename = 'test.log'
with io.open(filename, 'wb') as writer, io.open(filename, 'rb', 1) as reader:
    process = subprocess.Popen(command, stdout=writer)
    while process.poll() is None:
        sys.stdout.write(reader.read())
        time.sleep(0.5)
    # Read the remaining
    sys.stdout.write(reader.read())
Run Code Online (Sandbox Code Playgroud)

这样,您将test.log在标准输出和标准输出中写入数据.

文件方法的唯一优点是您的代码不会阻塞.因此,您可以在此期间做任何您想做的事情,并reader以非阻塞的方式随时阅读.当使用PIPE,readreadline功能将阻塞,直到任一个字符被写入到管或线被分别写入到管道.

  • 使用Python 3,你需要`iter(process.stdout.readline,b'')`(即传递给[iter]的哨兵(https://docs.python.org/3/library/functions.html#iter)需要是一个二进制字符串,因为`b''!=''`. (11认同)
  • 添加到@JohnMellor的答案,在Python 3中需要进行以下修改:`process = subprocess.Popen(command,stderr = subprocess.STDOUT,stdout = subprocess.PIPE)for iter(process.stdout.readline,b) '')sys.stdout.write(line.decode(sys.stdout.encoding))` (5认同)
  • 但输出不是实时的,是吗?根据我的经验,它只是等到进程完成执行,然后才打印到控制台。链接 -> /sf/ask/2101823181/ (5认同)
  • 呃:-)写入一个文件,从中读取,然后在循环中休眠?该过程也有可能在您读完文件之前结束。 (3认同)
  • 对于二进制流,请执行以下操作:`for line in iter(process.stdout.readline,b''):sys.stdout.buffer.write(line)` (3认同)
  • 我收到`AttributeError: '_io.BufferedWriter' object has no attribute 'buffer'`,知道出了什么问题吗? (2认同)

tor*_*rek 87

执行摘要(或"tl; dr"版本):当最多只有一个时很容易subprocess.PIPE,否则它很难.

可能是时候解释一下它是如何subprocess.Popen做到的.

(警告:这是针对Python 2.x,尽管3.x类似;而且我对Windows变体很模糊.我更了解POSIX的东西.)

Popen功能需要同时处理零到三个I/O流.这些表示stdin,stdoutstderr通常一样.

你可以提供:

  • None,表示您不想重定向流.它将像往常一样继承这些.请注意,至少在POSIX系统上,这并不意味着它将使用Python sys.stdout,而只是Python的实际标准输出; 最后看看demo.
  • 一个int价值.这是一个"原始"文件描述符(至少在POSIX中).(旁注:PIPESTDOUT实际上int,内部S中,但都是"不可能"的描述,-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()-it仍然很容易,因为操作系统可以完成所有工作.Python只需要分离子进程,将其stdin,stdout和/或stderr连接到提供的文件描述符.

仍然容易的情况:一个管道

如果您只重定向一个流,Pipe仍然很容易.我们一次选择一个流并观看.

假设你想提供一些stdin,但让stdoutstderr去未重定向,或去文件描述符.作为父进程,您的Python程序只需write()用于向管道发送数据.你可以自己做,例如:

proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.stdin.write('here, have some data\n') # etc
Run Code Online (Sandbox Code Playgroud)

或者您可以将stdin数据传递给proc.communicate(),然后执行stdin.write上面显示的操作.没有输出回来,所以communicate()只有另一个真正的工作:它也为你关闭管道.(如果不调用proc.communicate(),则必须调用proc.stdin.close()以关闭管道,以便子进程知道没有更多数据通过.)

假设你想捕捉stdout,但离开stdinstderr孤独.同样,这很简单:只需调用proc.stdout.read()(或等效)直到没有更多输出.由于proc.stdout()是普通的Python I/O流,您可以使用它上面的所有常规构造,例如:

for line in proc.stdout:
Run Code Online (Sandbox Code Playgroud)

或者,再次,你可以使用proc.communicate(),这只是read()为你做的.

如果您只想捕获stderr,它的工作方式与使用相同stdout.

在事情变得艰难之前还有一个技巧.假设你想捕捉stdout,并且还捕捉stderr,但在同一管道的标准输出:

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
Run Code Online (Sandbox Code Playgroud)

在这种情况下,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:
Run Code Online (Sandbox Code Playgroud)

但是,唉,这里我们已经制作了至少两个,也许三个不同的管道,所以count(None)返回1或0.我们必须以艰难的方式做事.

在Windows上,这用于threading.Thread累积self.stdout和的结果self.stderr,并让父线程传递self.stdin输入数据(然后关闭管道).

在POSIX上,这将使用(poll如果可用),否则select,累积输出并传递stdin输入.所有这些都在(单个)父进程/线程中运行.

这里需要线程或轮询/选择以避免死锁.例如,假设我们已将所有三个流重定向到三个单独的管道.进一步假设在写入过程暂停之前,有多少数据可以填充到管道中,等待读取过程从另一端"清理"管道.我们将这个小限制设置为单个字节,仅用于说明.(这实际上是如何工作的,除了限制远大于一个字节.)

如果父(Python)进程尝试写入几个字节 - 比如说,'go\n'to proc.stdin,第一个字节进入,然后第二个进程导致Python进程暂停,等待子进程读取第一个字节,清空管道.

同时,假设子进程决定打印一个友好的"你好!不要恐慌!" 问候.在H进入它的标准输出管道,但e导致其暂停,等待其家长阅读H,排空stdout管道.

现在我们陷入困境:Python进程处于睡眠状态,等待完成说"go",子进程也处于睡眠状态,等待完成说"你好!不要恐慌!".

subprocess.Popen代码通过线程或选择/轮询避免了这个问题.当字节可以越过管道时,它们就会消失.当它们不能时,只有一个线程(不是整个进程)必须休眠 - 或者在select/poll的情况下,Python进程同时等待"可写"或"数据可用",写入进程的stdin只有当有空间时,才会在数据准备就绪时读取它的标准输出和/或标准输出.一旦发送了所有stdin数据(如果有的话)并且已经累积了所有stdout和/或stderr数据,proc.communicate()代码(实际上_communicate处理多毛的情况)将返回.

如果你想同时读取stdoutstderr在两个不同的管道(无论任何的stdin重定向),则需要避免死锁了.这里的死锁场景是不同的 - 当stderr你从中提取数据时,子进程写了很长的东西stdout,反之亦然 - 但它仍然在那里.


演示

我答应证明,未重定向,Python subprocesses写入底层stdout,而不是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()
Run Code Online (Sandbox Code Playgroud)

运行时:

$ python out.py
start show1
hello
in buffer: sys.stdout being buffered

start show2
hello
Run Code Online (Sandbox Code Playgroud)

请注意,如果添加stdout=sys.stdout,第一个例程将失败,因为StringIO对象没有fileno.第二个将省略hello如果您添加stdout=sys.stdout以来sys.stdout已被重定向到os.devnull.

(如果重定向Python的文件描述符-1,子进程遵循该重定向.该open(os.devnull, 'w')调用产生一个fileno()大于2 的流.)

  • +1,很好的解释,但缺乏具体的代码示例.这是基于[`asyncio`的代码,以便携方式实现"硬件"(它同时处理多个管道)](http://stackoverflow.com/a/25960956/4279).您可以将它与[使用多个线程的代码(`teed_call()`)进行比较](http://stackoverflow.com/a/25755038/4279). (2认同)

小智 20

我们也可以使用默认文件迭代器来读取stdout,而不是使用带有readline()的iter构造.

import subprocess
import sys
process = subprocess.Popen(your_command, stdout=subprocess.PIPE)
for line in process.stdout:
    sys.stdout.write(line)
Run Code Online (Sandbox Code Playgroud)

  • 该解决方案不能实时显示。它等待直到该过程完成并立即显示所有输出。在Viktor Kerkez的解决方案中,如果“ your_command”逐渐显示,则只要“ your_command”不时刷新stdout(由于管道),输出就会逐渐跟随。 (8认同)
  • 为什么这不是被接受和最多投票的答案? (5认同)
  • 最优雅的答案在这里! (3认同)
  • @Nir 因为它不是实时的。 (2认同)

kab*_*hya 12

除了所有这些答案之外,一种简单的方法也可以如下:

process = subprocess.Popen(your_command, stdout=subprocess.PIPE)

while process.stdout.readable():
    line = process.stdout.readline()

    if not line:
        break

    print(line.strip())
Run Code Online (Sandbox Code Playgroud)

只要可读流,就循环遍历可读流,如果结果为空,则停止。

这里的关键是只要有输出就readline()返回一行(\n在末尾),如果它真的在末尾则为空。

希望这可以帮助某人。


Vin*_*jip 11

如果你能够使用第三方库,你可能会使用类似的东西sarge(披露:我是它的维护者).该库允许对来自子进程的输出流进行非阻塞访问 - 它在subprocess模块上分层.

  • 如果您建议使用一个工具,至少要展示一个针对这种情况的使用示例。 (3认同)

pap*_*adp 10

如果您需要的只是输出在控制台上可见,对我来说最简单的解决方案是将以下参数传递给Popen

with Popen(cmd, stdout=sys.stdout, stderr=sys.stderr) as proc:
Run Code Online (Sandbox Code Playgroud)

它将使用你的 python 脚本 stdio 文件句柄


Rot*_*eti 6

解决方案 1:实时同时记录stdoutANDstderr

一个简单的解决方案,它同时将 stdout 和 stderr实时逐行记录到日志文件中。

import subprocess as sp
from concurrent.futures import ThreadPoolExecutor


def log_popen_pipe(p, stdfile):

    with open("mylog.txt", "w") as f:

        while p.poll() is None:
            f.write(stdfile.readline())
            f.flush()

        # Write the rest from the buffer
        f.write(stdfile.read())


with sp.Popen(["ls"], stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    with ThreadPoolExecutor(2) as pool:
        r1 = pool.submit(log_popen_pipe, p, p.stdout)
        r2 = pool.submit(log_popen_pipe, p, p.stderr)
        r1.result()
        r2.result()
Run Code Online (Sandbox Code Playgroud)

解决方案 2:一个read_popen_pipes()允许您同时实时迭代两个管道 (stdout/stderr)的函数

import subprocess as sp
from queue import Queue, Empty
from concurrent.futures import ThreadPoolExecutor


def enqueue_output(file, queue):
    for line in iter(file.readline, ''):
        queue.put(line)
    file.close()


def read_popen_pipes(p):

    with ThreadPoolExecutor(2) as pool:
        q_stdout, q_stderr = Queue(), Queue()

        pool.submit(enqueue_output, p.stdout, q_stdout)
        pool.submit(enqueue_output, p.stderr, q_stderr)

        while True:

            if p.poll() is not None and q_stdout.empty() and q_stderr.empty():
                break

            out_line = err_line = ''

            try:
                out_line = q_stdout.get_nowait()
                err_line = q_stderr.get_nowait()
            except Empty:
                pass

            yield (out_line, err_line)

# The function in use:

with sp.Popen(["ls"], stdout=sp.PIPE, stderr=sp.PIPE, text=True) as p:

    for out_line, err_line in read_popen_pipes(p):
        print(out_line, end='')
        print(err_line, end='')

    p.poll()

Run Code Online (Sandbox Code Playgroud)


小智 5

与之前的答案类似,但以下解决方案适用于我在 Windows 上使用 Python3 提供实时打印和登录的通用方法(源代码

def print_and_log(command, logFile):
    with open(logFile, 'wb') as f:
        command = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)

        while True:
            output = command.stdout.readline()
            if not output and command.poll() is not None:
                f.close()
                break
            if output:
                f.write(output)
                print(str(output.strip(), 'utf-8'), flush=True)
        return command.poll()
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

129257 次

最近记录:

5 年,12 月 前