如何使用Popen同时将输出写入stdout和日志文件?

60

我正在使用Popen调用一个将其stdout和stderr持续写入日志文件的shell脚本。有没有办法同时连续输出日志文件(到屏幕上),或者让shell脚本同时写入日志文件和标准输出?

我基本上想在Python中做这样的事情:

cat file 2>&1 | tee -a logfile #"cat file" will be replaced with some script

这里再次将stderr/stdout管道连接到tee命令,它会将输出内容同时写入stdout和我的日志文件。

我知道如何在Python中将stdout和stderr写入日志文件。但是我卡在了如何将它们复制回屏幕上:

subprocess.Popen("cat file", shell=True, stdout=logfile, stderr=logfile)
当然,我可以像这样做,但是否有一种方法可以在不使用tee和shell文件描述符重定向的情况下完成此操作?:
subprocess.Popen("cat file 2>&1 | tee -a logfile", shell=True)

3个回答

58

您可以使用管道从程序的标准输出读取数据,并将其写入您想要的所有位置:

import sys
import subprocess

logfile = open('logfile', 'w')
proc=subprocess.Popen(['cat', 'file'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
for line in proc.stdout:
    sys.stdout.write(line)
    logfile.write(line)
proc.wait()

更新

在Python 3中,universal_newlines 参数控制管道的使用。如果为 False,则管道读取返回 bytes 对象,可能需要解码(例如,line.decode('utf-8'))以获取字符串。如果为 True,Python 将为您完成解码。

从版本 3.3 开始更改:当 universal_newlines 为 True 时,该类使用编码 locale.getpreferredencoding(False) 而不是 locale.getpreferredencoding()。有关此更改的更多信息,请参见 io.TextIOWrapper 类。


5
你可以创建一个类似文件的对象来封装这个功能,然后在调用Popen时使用它来代替stdout/stderr - Silas Ray
2
@sr2222 - 我也喜欢这个想法...不过现在我想一想...,它们是操作系统管道,而不是Python对象,那这样行得通吗? - tdelaney
3
该代码会读取stdout直到其关闭,然后等待程序退出。在等待之前进行读取是为了避免管道填满并挂起程序。读取后等待最终的程序退出和返回代码。如果不等待,你将会得到一个僵尸进程(至少在Linux上是这样的)。 - tdelaney
6
由于带有预读缓冲区的错误,你可能需要使用iter(proc.stdout.readline, '') 这个方法,并且在打印出子进程刷新后的行时,需要添加 bufsize=1。调用proc.stdout.close()可以避免文件描述符泄漏。 - jfs
2
@tdelaney:不是固定的。尝试运行这个脚本:import time; print(1); time.sleep(1); print(2)。你的版本在脚本退出之前不会打印 1。我评论中提到的 flush 指的是你无法直接控制的 子进程 中的缓冲区。如果子进程不刷新其 stdout,则输出将被延迟。可以使用 pexpect, pty 模块stdbuf, unbuffer, script 命令 来解决此问题。 - jfs
显示剩余16条评论

18

如果不使用 tee 命令并且模拟以下命令: subprocess.call("command 2>&1 | tee -a logfile", shell=True)

#!/usr/bin/env python2
from subprocess import Popen, PIPE, STDOUT

p = Popen("command", stdout=PIPE, stderr=STDOUT, bufsize=1)
with p.stdout, open('logfile', 'ab') as file:
    for line in iter(p.stdout.readline, b''):
        print line,  #NOTE: the comma prevents duplicate newlines (softspace hack)
        file.write(line)
p.wait()

为了解决可能的缓冲问题(如果输出被延迟),请参见Python: read streaming input from subprocess.communicate()中的链接。

以下是Python 3版本:

#!/usr/bin/env python3
import sys
from subprocess import Popen, PIPE, STDOUT

with Popen("command", stdout=PIPE, stderr=STDOUT, bufsize=1) as p, \
     open('logfile', 'ab') as file:
    for line in p.stdout: # b'\n'-separated lines
        sys.stdout.buffer.write(line) # pass bytes as is
        file.write(line)

2
你应该提到,在进程完成后,可以在p.returncode中找到返回代码。 - kdubs
1
@kdubs:这与问题无关。你为什么认为我“应该提到”它呢? - jfs
6
虽然我同意他没有请求这个,但似乎应该检查返回状态。我希望能够在这里找到它。这样才算是完整的回答。也许“应该”这个词用得有点强烈了。 - kdubs
1
@kdubs 我同意检查退出状态是个好主意(这就是为什么有subprocess.check_call()subprocess.check_output()函数来帮你完成)。我本可以添加if p.wait() != 0: raise subprocess.CalledProcessError(p.returncode, "command"),但那会分散注意力,而我们的重点是如何在Python中模拟tee实用程序。 - jfs
2
Python 3以上版本:执行后在屏幕上打印,非实时。 - Ujjawal Khare
显示剩余13条评论

5

逐字节向终端写入内容以实现交互式应用

该方法会立即将其收到的所有字节写入标准输出(stdout),这更加类似于tee命令的行为,特别适合于交互式应用。

main.py

#!/usr/bin/env python3
import os
import subprocess
import sys
with subprocess.Popen(sys.argv[1:], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as proc, \
        open('logfile.txt', 'bw') as logfile:
    while True:
        byte = proc.stdout.read(1)
        if byte:
            sys.stdout.buffer.write(byte)
            sys.stdout.flush()
            logfile.write(byte)
            # logfile.flush()
        else:
            break
exit_status = proc.returncode

sleep.py

#!/usr/bin/env python3
import sys
import time
for i in range(10):
    print(i)
    sys.stdout.flush()
    time.sleep(1)

首先,我们可以进行一次非交互式的健康检查:

./main.py ./sleep.py

我们可以实时看到它在标准输出中计数。

接下来,对于交互式测试,您可以运行:

./main.py bash

当您输入字符时,这些字符将立即显示在终端上,对于交互式应用程序来说非常重要。这就是当您运行以下命令时发生的情况:

bash | tee logfile.txt

此外,如果您希望立即在输出文件中显示输出,则还可以添加以下内容:
logfile.flush()

但是tee不会这样做,我担心它会影响性能。您可以使用以下命令轻松测试:

tail -f logfile.txt

相关问题:如何实时输出子进程命令的结果?

在Ubuntu 18.04和Python 3.6.7上进行了测试。


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