你可以用 来做到这一点subprocess
,但这不是微不足道的。如果您查看文档中的常用参数,您会看到您可以PIPE
作为stderr
参数传递,它创建一个新管道,将管道的一侧传递给子进程,并使另一侧可用作属性。stderr
*
因此,您需要维护该管道,写入屏幕和文件。通常,为此获得正确的细节非常棘手。**在您的情况下,只有一个管道,并且您计划同步维修它,所以还不错。
import subprocess
proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
stdout=file_out, stderr=subprocess.PIPE)
for line in proc.stderr:
sys.stdout.write(line)
log_file.write(line)
proc.wait()
(请注意,使用 - 基本上存在一些问题for line in proc.stderr:
,如果您正在阅读的内容因任何原因被证明不是行缓冲的,即使实际上有半行数据需要处理,您也可以坐等换行。您可以一次读取块,例如,read(128)
甚至read(1)
,以便在必要时更顺利地获取数据。如果您需要在每个字节到达后立即获取它,并且负担不起 的成本read(1)
,您将需要将管道置于非阻塞模式并异步读取。)
但是,如果您使用的是 Unix,则使用该tee
命令为您执行此操作可能会更简单。
要获得快速而肮脏的解决方案,您可以使用外壳通过它进行管道传输。像这样的东西:
subprocess.call('path_to_tool -option1 option2 2|tee log_file 1>2', shell=True,
stdout=file_out)
但我不想调试外壳管道;让我们用 Python 来做,如文档中所示:
tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
stdout=file_out, stderr=subprocess.PIPE)
tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stderr)
tool.stderr.close()
tee.communicate()
最后,在 PyPI 上围绕子进程和/或 shell 有十几个或更多更高级别的包装器——<code>sh, shell
, shell_command
, shellout
, iterpipes
, sarge
, cmd_utils
,commandwrapper
等。搜索“shell”、“subprocess”、“process”, “命令行”等,并找到一个你喜欢的,使问题变得微不足道。
如果您需要同时收集 stderr 和 stdout 怎么办?
正如 Sven Marnach 在评论中建议的那样,最简单的方法就是将一个重定向到另一个。只需像这样更改Popen
参数:
tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
然后在你使用的任何地方tool.stderr
,tool.stdout
改为使用 - 例如,对于最后一个示例:
tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stdout)
tool.stdout.close()
tee.communicate()
但这有一些权衡。最明显的是,将两个流混合在一起意味着您不能将 stdout 记录到 file_out 并将 stderr 记录到 log_file,或者将 stdout 复制到您的 stdout 并将 stderr 复制到您的 stderr。但这也意味着排序可能是不确定的——如果子进程总是在向 stdout 写入任何内容之前向 stderr 写入两行,那么一旦混合流,您最终可能会在这两行之间得到一堆 stdout。这意味着它们必须共享 stdout 的缓冲模式,所以如果您依赖 linux/glibc 保证 stderr 是行缓冲的事实(除非子进程显式更改它),那可能不再正确。
如果您需要分别处理这两个过程,则会变得更加困难。之前,我说过,只要您只有一根管道并且可以同步维护它,就可以轻松地在运行中维护管道。如果你有两个管道,那显然不再正确。想象一下,您正在等待tool.stdout.read()
,新数据来自tool.stderr
. 如果数据过多,可能会导致管道溢出和子进程阻塞。但是即使没有发生这种情况,您显然也无法读取和记录 stderr 数据,直到从 stdout 中输入某些内容。
如果您使用管道直通tee
解决方案,则可以避免最初的问题……但只能通过创建一个同样糟糕的新项目来解决。您有两个tee
实例,当您呼叫communicate
一个时,另一个则坐在那里永远等待。
因此,无论哪种方式,您都需要某种异步机制。你可以用线程、select
反应器、类似的东西等来做到这一点gevent
。
这是一个快速而肮脏的例子:
proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def tee_pipe(pipe, f1, f2):
for line in pipe:
f1.write(line)
f2.write(line)
t1 = threading.Thread(target=tee_pipe, args=(proc.stdout, file_out, sys.stdout))
t2 = threading.Thread(target=tee_pipe, args=(proc.stderr, log_file, sys.stderr))
t3 = threading.Thread(proc.wait)
t1.start(); t2.start(); t3.start()
t1.join(); t2.join(); t3.join()
但是,在某些极端情况下这是行不通的。(问题是 SIGCHLD 和 SIGPIPE/EPIPE/EOF 到达的顺序。我认为这些都不会影响我们,因为我们没有发送任何输入……但不要不假思索就相信我通过和/或测试。)subprocess.communicate
3.3+ 中的功能正确地获取了所有繁琐的细节。但是你可能会发现使用你可以在 PyPI 和 ActiveState 上找到的异步子进程包装器实现之一,甚至是来自像 Twisted 这样成熟的异步框架的子进程的东西要简单得多。
* 文档并没有真正解释什么是管道,就好像他们希望你是一个老 Unix C 手……但是一些例子,特别是在用模块替换旧函数subprocess
部分,展示了它们是如何使用的,这很简单。
** 困难的部分是正确排序两个或多个管道。如果您在一个管道上等待,另一个可能会溢出并阻塞,从而阻止您对另一个管道的等待完成。解决这个问题的唯一简单方法是创建一个线程来服务每个管道。(在大多数 *nix 平台上,您可以使用 a select
or poll
reactor 代替,但要实现跨平台非常困难。)模块的源代码,尤其是communicate
它的助手,展示了如何做到这一点。(我链接到 3.3,因为在早期版本中,它本身会弄错一些重要的事情……)这就是为什么在需要多个管道communicate
时尽可能使用的原因。communicate
在您的情况下,您不能使用communicate
,但幸运的是您不需要多个管道。