Python 捕获子进程的标准输出并逐行处理

3

我阅读了许多与此相关的问题并学到了很多,但我仍然无法解决我的问题。我正在构建一个运行c++可执行文件并实时显示该可执行文件stdout的wxPython应用程序。在尝试使其工作时,我遇到了几个奇怪的结果。以下是我的当前设置/问题:

//test.cc (compiled as test.out with gcc 4.5.2)
#include <stdio.h>
int main()
{
  FILE* fh = fopen("output.txt", "w");
  for (int i = 0; i < 10000; i++)
  {
      printf("Outputting: %d\n", i);
      fprintf(fh, "Outputting: %d\n", i);
  }
  fclose(fh);
  return 0;
}

#wxPythonScript.py (running on 2.7 interpreter)
def run(self):
  self.externalBinary = subprocess.Popen(['./test.out'], shell=False, stdout=subprocess.PIPE)
  while not self.wantAbort:
      line = self.externalBinary.stdout.readline()
      wx.PostEvent(self.notifyWindow, Result_Event(line, Result_Event.EVT_STDOUT_ID))
    print('Subprocess still running')
  print('Subprocess aborted smoothly')

如果我运行以上代码,子进程需要很长时间才能完成,即使它只是写出数据并退出。然而,如果我运行以下代码,它会非常快地完成:

#wxPythonScript.py (running on 2.7 interpreter)
def run(self):
  outFile = open('output.txt', 'r+')
  self.externalBinary = subprocess.Popen(['./test.out'], shell=False, stdout=outFile)
  while not self.wantAbort:
      #line = self.externalBinary.stdout.readline()
      #wx.PostEvent(self.notifyWindow, Result_Event(line, Result_Event.EVT_STDOUT_ID))
    print('Subprocess still running')
  print('Subprocess aborted smoothly')

当我将子进程的标准输出重定向到管道时,它会变慢/挂起,但如果将其写入文件或根本不重定向,则没有问题。为什么会这样呢?


可能是逐行读取子进程的标准输出的重复问题。 - S.Lott
但是我没有得到我期望的结果。这个问题只是关于缓冲性能吗?就这些吗?如果是的话,请更新问题以表明它可以工作,但您不喜欢性能。 - S.Lott
另一个我遇到的问题是getline会卡住,即使子进程据说仍在输出数据,getline只会读取其中一部分然后就停在那里。正如我在问题中所说,我遇到了几个奇怪的问题。我正在尝试一次只提出一个问题并缩小范围。 - anderspitman
@eryksun: "稍作修改的J.F. Sebastian的答案"是指您可能对这个问题有一个完全不同的答案?也许您可以详细说明一下这个小修改? - S.Lott
@eryksun:我直接复制了J.F. Sebastian的代码,在Python 2.6上它显示为1个“尚未输出”,然后什么都没有发生。如果我将try/except块放在一个循环中,那么它就会不断地输出“尚未输出”。但是文件已经正确编写了。你能否在Python 2.6上尝试你的解决方案,如果可行,请将你的代码发布为答案?谢谢 - anderspitman
显示剩余5条评论
1个回答

6

我只在Windows上测试过,但它适用于2.6.6、2.7.2和3.2.1版本:

from __future__ import print_function
from subprocess import PIPE, Popen
from threading  import Thread
import sys

try:
    from Queue import Queue, Empty
except ImportError:
    from queue import Queue, Empty  # python 3.x

ON_POSIX = 'posix' in sys.builtin_module_names

def enqueue_output(out, queue):
    for line in iter(out.readline, b''):
        line = line.decode(sys.stdout.encoding)
        queue.put(line)
    out.close()

def main():
    p = Popen(['c/main.exe'], stdout=PIPE, bufsize=1, close_fds=ON_POSIX)
    q = Queue()
    t = Thread(target=enqueue_output, args=(p.stdout, q))
    t.daemon = True # thread dies with the program
    t.start()

    #initially the queue is empty and stdout is open
    #stdout is closed when enqueue_output finishes
    #then continue printing until the queue is empty 

    while not p.stdout.closed or not q.empty():
        try:
            line = q.get_nowait()
        except Empty:
            continue
        else:
            print(line, end='')
    return 0

if __name__ == '__main__':
    sys.exit(main())

输出:

Outputting: 0
Outputting: 1
Outputting: 2
...
Outputting: 9997
Outputting: 9998
Outputting: 9999

编辑:

readline() 会一直阻塞,直到程序的标准输出缓冲区刷新完成。如果数据流是间歇性的,这可能需要很长时间。如果你能够编辑源代码,则可以手动调用 fflush(stdout) 来刷新缓冲区,或者你可以在程序开始时使用 setvbuf 禁用缓冲。例如:

#include <stdio.h>

int main() {

    setvbuf(stdout, NULL, _IONBF, 0);

    FILE* fh = fopen("output.txt", "w");
    int i;

    for (i = 0; i < 10; i++) {
        printf("Outputting: %d\n", i);
        fprintf(fh, "Outputting: %d\n", i);
        sleep(1);
    }

    fclose(fh);
    return 0;
}

同时,也可以考虑使用unbuffer或者stdbuf来修改现有程序的输出流。


不错,我觉得我们已经接近成功了。如果我按原样运行所有内容,它可以工作。然而,我需要调用的实际C程序输出数据非常缓慢(大约每秒1行)。如果我在C循环中添加sleep(1);,它就不会输出任何东西,除非我还添加fflush(stdout);。我有一种感觉,这将为比我更聪明的人提供一个线索,以找出问题所在。另外,让我们假设我无法访问C程序的源代码,以便简单地添加fflush命令。 - anderspitman
当我在line = line.decode(sys.stdout.encoding)之后添加了out.flush(),它对我起作用了。我认为我遇到的一个根本性问题是我已经有了一个单独的线程,以便我的GUI不会锁定,但我需要另一个线程在其中处理stdout。有没有办法在一个线程中处理读取stdout?我仍然不太明白发生了什么,这几乎比它不起作用更让我困扰。但它确实起作用了,所以感谢您的帮助。另外,close_fds是什么意思?Python文档让我更加困惑了。 - anderspitman
算了,我错了。除非我直接在C代码中刷新,否则它仍然无法工作。有什么想法吗?这似乎应该非常简单。输出被放入stdout缓冲区。为什么我不能只是从缓冲区中取出它并让每个人都高兴呢?你觉得未缓冲模式中有bug吗? - anderspitman
1
好的,我的问题很简单:stdout被缓冲了,就这样。https://dev59.com/B3NA5IYBdhLWcg3wrPyq。看起来我可以从子进程中刷新它或者使用类似pexpect的东西。S. Lott你是正确的,这绝对是重复多次。我道歉。话虽如此,我不明白为什么普通终端能够实时读取程序输出而无需等待stdout刷新或程序退出。如果我找不到答案,这可能值得问一下。 - anderspitman
如果你把"throw setvbuf up as an answer" 作为答案,我会接受它。 - anderspitman
@erykson:老兄,你真是个天才。stdbuf 真的解决了我的问题!你可能已经厌倦了编辑那个答案,但我认为使用它是对我的问题最直接的回答:现在我能够监视输出了,这也是最初的目标。虽然不是最通用的解决方案,但对我来说很有效。非常感谢! - anderspitman

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