关于python:在进程运行时不断打印Subprocess输出

Constantly print Subprocess output while process is running

要从我的python脚本启动程序,我使用以下方法:

1
2
3
4
5
6
7
8
9
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


当命令输出行时,可以使用ITER来处理它们:lines = iter(fd.readline,"")。下面是一个完整的例子,展示了一个典型的用例(感谢@jfs的帮助):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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="")


好吧,我通过使用这个问题中的一个片段,在子进程运行时截取stdout,成功地解决了没有线程的问题(感谢您对为什么使用线程更好的任何建议)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)


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

1
2
3
4
5
6
7
8
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, '')--预读bug是在python 3中修复的。

另请参见python:read streaming input from subprocess.communication()。


托克兰

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 ="

"
,flush =True) # yield line

execute(c)


对于任何试图从python脚本获取stdout这个问题的答案的人,请注意python缓冲其stdout,因此可能需要一段时间才能看到stdout。

这可以通过在目标脚本中的每个stdout写入之后添加以下内容来纠正:

1
sys.stdout.flush()


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

1
2
3
4
import subprocess

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

(在执行过程中获取输出也可以在没有shell=True的情况下工作)https://docs.python.org/3/library/subprocess.html subprocess.run


如果有人想在使用线程的同时读取stdoutstderr,我就想到了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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)

我只想和大家分享这个问题,因为我最终在这个问题上试图做一些类似的事情,但是没有一个答案解决了我的问题。希望它能帮助别人!

注意,在我的用例中,一个外部进程终止了我们所使用的cx1〔5〕进程。


这个POC不断地从一个进程中读取输出,并且可以在需要时访问。只保留最后一个结果,所有其他输出都将被丢弃,从而防止管道从内存中增长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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()

PrimtTyDAT.Py

1
2
3
4
5
6
7
#!/usr/bin/env python
import time

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

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

1
2
3
4
>>>1520535158.51
>>>1520535161.01
>>>1520535163.51
>>>1520535166.01

这里的答案都不能满足我所有的需要。

  • 没有用于stdout的线程(也没有队列等)
  • 不阻塞,因为我需要检查其他事情
  • 根据需要使用pipe执行多项操作,例如流输出、写入日志文件并返回输出的字符串副本。
  • 一点背景知识:我使用一个线程池执行器来管理一个线程池,每个线程都启动一个子进程并并发地运行它们。(在python2.7中,但这也适用于更新的3.x)。我不想将线程仅用于输出收集,因为我希望尽可能多的线程可用于其他用途(20个进程的池将使用40个线程仅用于运行;1个用于进程线程,1个用于stdout…如果需要stderr,我猜还有更多)

    我剥离了很多异常,所以这是基于在生产中工作的代码。希望我没有在复制和粘贴中破坏它。另外,非常欢迎反馈!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    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消息稍后就会出现,而且一次都会出现。


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

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


    这至少在python3.4中有效。

    1
    2
    3
    4
    5
    import subprocess

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


    在Python3.6中,我使用了:

    1
    2
    3
    4
    5
    import subprocess

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