在进程运行时不断打印子进程输出


201

为了从我的Python脚本启动程序,我使用以下方法:

def execute(command):
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    output = process.communicate()[0]
    exitCode = process.returncode

    if (exitCode == 0):
        return output
    else:
        raise ProcessException(command, exitCode, output)

因此,当我启动像这样的过程时Process.execute("mvn clean install"),我的程序将等待直到该过程完成为止,然后我才能获得程序的完整输出。如果我正在运行需要一段时间才能完成的过程,这将很烦人。

我可以让我的程序通过在循环完成之前轮询过程输出来逐行写过程输出吗?

** [编辑]对不起,发布此问题之前,我搜索得并不好。线程实际上是关键。在此处找到了一个示例,该示例演示了如何执行此操作:** 从线程中获取Python Subprocess.Popen


我认为是线程而不是子进程
Ant

9
不,您不需要线程。整个管道构想是可行的,因为您可以在进程运行时对其进行读取/写入。
tokland

Answers:


264

您可以在命令输出行之后立即使用iter处理行lines = iter(fd.readline, "")。这是一个显示典型用例的完整示例(感谢@jfs的帮助):

from __future__ import print_function # Only Python 2.x
import subprocess

def execute(cmd):
    popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True)
    for stdout_line in iter(popen.stdout.readline, ""):
        yield stdout_line 
    popen.stdout.close()
    return_code = popen.wait()
    if return_code:
        raise subprocess.CalledProcessError(return_code, cmd)

# Example
for path in execute(["locate", "a"]):
    print(path, end="")

24
我已经尝试过此代码(使用需要花费大量时间才能运行的程序),并且可以确认它在收到行时就输出它们,而不是等待执行完成。这是imo的上乘答案。
安德鲁·马丁

11
注意:在Python 3中,您可以使用for line in popen.stdout: print(line.decode(), end='')。同时支持Python 2和3,使用字节的文字:b''否则lines_iterator永远在Python 3的两端
JFS

3
这种方法的问题在于,如果该过程暂停了一段时间而未向stdout写入任何内容,则没有更多的输入可读取。您将需要一个循环来检查该过程是否已完成。我试着Python 2.7版使用此subprocess32
喀拉

7
它应该工作。擦亮它,您可以添加bufsize=1(它可以提高Python的性能2),靠近该popen.stdout管明确(无需等待垃圾回收照顾它),提高subprocess.CalledProcessError(比如check_call()check_output()做)。该print语句在Python 2和3上是不同的:您可以使用softspace hack print line,(注意:逗号)来避免像代码一样将所有换行符加倍并universal_newlines=True在Python 3上传递,以获得文本而不是字节(与答案有关)
jfs

6
@binzhang没错,默认情况下,stdout在Python脚本(对于许多Unix工具)上也被缓冲。尝试execute(["python", "-u", "child_thread.py"])。更多信息:stackoverflow.com/questions/14258500/…–
tokland

84

好的,我设法通过使用此问题的片段来解决了没有线程的问题(不建议使用线程会更好的任何建议),方法是使用该问题的代码段在运行时拦截子进程的stdout

def execute(command):
    process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

    # Poll process for new output until finished
    while True:
        nextline = process.stdout.readline()
        if nextline == '' and process.poll() is not None:
            break
        sys.stdout.write(nextline)
        sys.stdout.flush()

    output = process.communicate()[0]
    exitCode = process.returncode

    if (exitCode == 0):
        return output
    else:
        raise ProcessException(command, exitCode, output)

3
合并ifischer的和tokland的代码工作得很好(我不得不改变print line,sys.stdout.write(nextline); sys.stdout.flush()否则,就打印出每两行话说回来,这是使用的IPython的笔记本的接口,所以也许别的事情正在发生。 -不管,显式调用flush()的作品。
eacousineau 2012年

3
先生,你是我的救星!真的很奇怪,这种事情不是库本身内置的..原因是,如果我编写cliapp,我想立即显示循环中正在处理的所有内容.. s'rsly ..
holms 2013年

3
可这个解决方案进行修改,以不断地打印两个输出和错误?如果我切换stderr=subprocess.STDOUT到该循环stderr=subprocess.PIPE然后process.stderr.readline()从循环中调用,我似乎会遇到subprocess模块文档中警告过的非常僵局。
davidrmcharles 2013年

7
@DavidCharles我想您正在寻找的是stdout=subprocess.PIPE,stderr=subprocess.STDOUT捕获stderr的工具,并且我相信(但我尚未测试)它也捕获了stdin。
安德鲁·马丁

感谢您等待退出代码。不知道如何解决
Vitaly Isaev

67

在Python中刷新子进程的stdout缓冲区后立即逐行打印子进程的输出:

from subprocess import Popen, PIPE, CalledProcessError

with Popen(cmd, stdout=PIPE, bufsize=1, universal_newlines=True) as p:
    for line in p.stdout:
        print(line, end='') # process line here

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

注意:您不需要p.poll()-循环在到达eof时结束。而且您不需要iter(p.stdout.readline, '')-预读错误已在Python 3中修复。

另请参见Python:从subprocess.communicate()读取流输入


3
这个解决方案对我有用。上面给出的公认的解决方案只是为我打印空白行。
代号

3
我确实必须添加sys.stdout.flush()才能立即获得打印结果。
代号

3
@Codename:您不需要sys.stdout.flush()在父级中-如果stdout没有重定向到文件/管道,则stdout是行缓冲的,因此打印会line自动刷新缓冲区。您也不需要sys.stdout.flush()在子级中- -u而是通过命令行选项。
jfs 2015年

1
@Codename:如果要使用,请>运行python -u your-script.py > some-file。注意:-u我上面提到的选项(无需使用sys.stdout.flush())。
jfs 2015年

1
@mvidelgauz无需调用p.wait()-在退出该with块时调用。使用p.returncode
jfs

8

当您只想打印输出时,实际上有一种非常简单的方法来执行此操作:

import subprocess
import sys

def execute(command):
    subprocess.check_call(command, stdout=sys.stdout, stderr=subprocess.STDOUT)

在这里,我们只是将子流程指向我们自己的标准输出,并使用现有的成功或异常api。


1
这个解决方案比@tokland的Python 3.6解决方案更简单,更干净。我注意到shell = True参数不是必需的。
善意

好收获,善意。已删除shell=True
安德鲁环

非常艰苦,只需很少的代码即可完美工作。也许您也应该将子进程stderr重定向到sys.stderr?
Manu

Manu当然可以。我没有在这里,因为问题的尝试是将stderr重定向到stdout。
安德鲁环

您能解释一下sys.stdout和subprocess.STDOUT之间的区别吗?
罗恩·塞鲁亚

7

@tokland

尝试了代码并针对3.4进行了更正,Windows dir.cmd是一个简单的dir命令,另存为cmd文件

import subprocess
c = "dir.cmd"

def execute(command):
    popen = subprocess.Popen(command, stdout=subprocess.PIPE,bufsize=1)
    lines_iterator = iter(popen.stdout.readline, b"")
    while popen.poll() is None:
        for line in lines_iterator:
            nline = line.rstrip()
            print(nline.decode("latin"), end = "\r\n",flush =True) # yield line

execute(c)

3
您可以简化代码iter()并且end='\r\n'是不必要的。Python默认使用通用换行模式,即在打印过程中将其'\n'转换为通用换行模式'\r\n''latin'可能是错误的编码,您可以universal_newlines=True用来获取Python 3中的文本输出(使用语言环境的首选编码进行解码)。不要停下来.poll(),可能会缓冲未读的数据。如果Python脚本在控制台中运行,则其输出是行缓冲的;您可以使用-uoption 强制进行行缓冲-您flush=True在这里不需要。
jfs 2015年

4

如果有人想同时使用线程读取stdout和读取消息stderr,这是我想出的:

import threading
import subprocess
import Queue

class AsyncLineReader(threading.Thread):
    def __init__(self, fd, outputQueue):
        threading.Thread.__init__(self)

        assert isinstance(outputQueue, Queue.Queue)
        assert callable(fd.readline)

        self.fd = fd
        self.outputQueue = outputQueue

    def run(self):
        map(self.outputQueue.put, iter(self.fd.readline, ''))

    def eof(self):
        return not self.is_alive() and self.outputQueue.empty()

    @classmethod
    def getForFd(cls, fd, start=True):
        queue = Queue.Queue()
        reader = cls(fd, queue)

        if start:
            reader.start()

        return reader, queue


process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdoutReader, stdoutQueue) = AsyncLineReader.getForFd(process.stdout)
(stderrReader, stderrQueue) = AsyncLineReader.getForFd(process.stderr)

# Keep checking queues until there is no more output.
while not stdoutReader.eof() or not stderrReader.eof():
   # Process all available lines from the stdout Queue.
   while not stdoutQueue.empty():
       line = stdoutQueue.get()
       print 'Received stdout: ' + repr(line)

       # Do stuff with stdout line.

   # Process all available lines from the stderr Queue.
   while not stderrQueue.empty():
       line = stderrQueue.get()
       print 'Received stderr: ' + repr(line)

       # Do stuff with stderr line.

   # Sleep for a short time to avoid excessive CPU use while waiting for data.
   sleep(0.05)

print "Waiting for async readers to finish..."
stdoutReader.join()
stderrReader.join()

# Close subprocess' file descriptors.
process.stdout.close()
process.stderr.close()

print "Waiting for process to exit..."
returnCode = process.wait()

if returnCode != 0:
   raise subprocess.CalledProcessError(returnCode, command)

我只是想分享一下,因为我最终遇到了这个问题,试图做类似的事情,但是没有一个答案解决了我的问题。希望它可以帮助某人!

请注意,在我的用例中,外部进程会杀死我们的进程Popen()


1
对于python2,我不得不使用几乎完全像这样的东西。虽然应该在python2中提供类似的内容,但绝对不是很好。
斯图尔特·阿克森

3

对于任何试图从Python脚本获取标准输出的问题的人,请注意Python会缓冲其标准输出,因此可能需要一段时间才能看到该标准输出。

可以通过在目标脚本中的每个stdout写完后添加以下内容来纠正此问题:

sys.stdout.flush()

1
但是首先将Python作为Python的子进程运行是很疯狂的。您的脚本应该只是import另一个脚本;看看multiprocessing或者threading,如果你需要并行执行。
Tripleee '18

3
@triplee在几种情况下,将Python作为Python的子进程运行是合适的。我有很多python批处理脚本,希望每天都可以顺序运行。这些可以由启动执行的主Python脚本编排,并在子脚本失败时通过电子邮件发送给我。每个脚本都相互沙盒化-没有命名冲突。我没有并行化,因此多处理和线程无关。
user1379351

您还可以使用与运行主Python程序不同的python可执行文件来启动其他python程序,例如subprocess.run("/path/to/python/executable", "pythonProgramToRun.py")
Kyle Bridenstine

3

在Python> = 3.5中使用subprocess.run对我有效:

import subprocess

cmd = 'echo foo; sleep 1; echo foo; sleep 2; echo foo'
subprocess.run(cmd, shell=True)

(在执行期间获取输出也可以在没有的情况下使用shell=Truehttps://docs.python.org/3/library/subprocess.html#subprocess.run


2
这不是“执行期间”。该subprocess.run()调用仅在子进程完成运行后返回。
Tripleee '19

1
您能解释一下它不是“执行期间”吗?类似的东西似乎>>> import subprocess; subprocess.run('top')也会打印“执行期间”(并且顶部永远不会结束)。也许我没有把握一些细微的区别?
user7017793 '19

如果您将输出重定向回Python等,stdout=subprocess.PIPE则只能在top完成后读取它。在子流程执行期间,您的Python程序被阻止。
Tripleee

1
是的,那很有意义。该run方法仍然有效,如果你只关心看到,因为它是生成的输出。如果您想对python中的输出进行异步处理,那是对的,它是行不通的。
user7017793 '19

3

为了回答最初的问题,IMO最好的方法是直接将子进程stdout直接重定向到您的程序stdout(可选,可以对进行相同的操作stderr,如下例所示)

p = Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
p.communicate()

3
不指定任何内容,stdout并且stderr用更少的代码即可完成相同的操作。尽管我认为显式胜于隐式。
Tripleee '19

1

此PoC不断读取过程的输出,可以在需要时进行访问。仅保留最后一个结果,所有其他输出都将被丢弃,从而防止PIPE耗尽内存:

import subprocess
import time
import threading
import Queue


class FlushPipe(object):
    def __init__(self):
        self.command = ['python', './print_date.py']
        self.process = None
        self.process_output = Queue.LifoQueue(0)
        self.capture_output = threading.Thread(target=self.output_reader)

    def output_reader(self):
        for line in iter(self.process.stdout.readline, b''):
            self.process_output.put_nowait(line)

    def start_process(self):
        self.process = subprocess.Popen(self.command,
                                        stdout=subprocess.PIPE)
        self.capture_output.start()

    def get_output_for_processing(self):
        line = self.process_output.get()
        print ">>>" + line


if __name__ == "__main__":
    flush_pipe = FlushPipe()
    flush_pipe.start_process()

    now = time.time()
    while time.time() - now < 10:
        flush_pipe.get_output_for_processing()
        time.sleep(2.5)

    flush_pipe.capture_output.join(timeout=0.001)
    flush_pipe.process.kill()

print_date.py

#!/usr/bin/env python
import time

if __name__ == "__main__":
    while True:
        print str(time.time())
        time.sleep(0.01)

输出:您可以清楚地看到,从〜2.5s间隔只有输出,两者之间没有任何输出。

>>>1520535158.51
>>>1520535161.01
>>>1520535163.51
>>>1520535166.01

0

这至少在Python3.4中有效

import subprocess

process = subprocess.Popen(cmd_list, stdout=subprocess.PIPE)
for line in process.stdout:
    print(line.decode().strip())

1
这有一个问题,它阻塞循环,直到进程完成运行为止。
Tripleee

0

这里没有答案能满足我的所有需求。

  1. 没有用于stdout的线程(也没有队列等)
  2. 非阻塞,因为我需要检查其他情况
  3. 根据需要使用PIPE来执行多项操作,例如流输出,写入日志文件并返回输出的字符串副本。

一些背景知识:我正在使用ThreadPoolExecutor来管理线程池,每个线程都启动一个子进程并运行它们的并发性。(在Python2.7中,但这也应在较新的3.x中运行)。我不想仅将线程用于输出收集,因为我希望尽可能多的线程可用于其他事情(20个进程的池将仅使用40个线程来运行; 1个用于进程线程,而1个用于stdout ...还有更多,如果您想要stderr,我猜)

我在这里剥离了很多异常,因此这是基于可在生产环境中使用的代码的。希望我不会在复制粘贴时毁了它。另外,非常欢迎反馈!

import time
import fcntl
import subprocess
import time

proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

# Make stdout non-blocking when using read/readline
proc_stdout = proc.stdout
fl = fcntl.fcntl(proc_stdout, fcntl.F_GETFL)
fcntl.fcntl(proc_stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK)

def handle_stdout(proc_stream, my_buffer, echo_streams=True, log_file=None):
    """A little inline function to handle the stdout business. """
    # fcntl makes readline non-blocking so it raises an IOError when empty
    try:
        for s in iter(proc_stream.readline, ''):   # replace '' with b'' for Python 3
            my_buffer.append(s)

            if echo_streams:
                sys.stdout.write(s)

            if log_file:
                log_file.write(s)
    except IOError:
        pass

# The main loop while subprocess is running
stdout_parts = []
while proc.poll() is None:
    handle_stdout(proc_stdout, stdout_parts)

    # ...Check for other things here...
    # For example, check a multiprocessor.Value('b') to proc.kill()

    time.sleep(0.01)

# Not sure if this is needed, but run it again just to be sure we got it all?
handle_stdout(proc_stdout, stdout_parts)

stdout_str = "".join(stdout_parts)  # Just to demo

我确定这里要增加开销,但是这对我来说不是问题。从功能上来说,它可以满足我的需求。我唯一没有解决的问题就是为什么这对于日志消息非常有效,但是我看到一些print消息稍后出现,并且一次全部出现。


-2

在Python 3.6中,我使用了以下命令:

import subprocess

cmd = "command"
output = subprocess.call(cmd, shell=True)
print(process)

1
这不是对这个特定问题的答案。OP试图避免的是,确切地等待子流程完成后再获取其输出。旧的遗留函数subprocess.call()有一些疣,这些疣由较新的函数修复。在Python 3.6中,通常会使用subprocess.run()它;为了方便起见,较旧的包装器函数subprocess.check_output()仍然可用-它返回过程的实际输出(此代码将仅返回退出代码,但即使这样,也将打印未定义的内容)。
Tripleee
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.