Python `tee` 子进程标准输出

10 投票
5 回答
3219 浏览
提问于 2025-04-16 20:38

有没有办法在Python中实现类似于UNIX命令行中tee的功能?我正在使用一种典型的分叉/执行模式,我希望子进程的标准输出(stdout)能够同时出现在日志文件和父进程的标准输出上,而且不需要任何缓冲。

比如在这段Python代码中,子进程的标准输出最终只出现在日志文件里,而没有出现在父进程的标准输出上。

pid = os.fork()
logFile = open(path,"w")
if pid == 0:
  os.dup2(logFile.fileno(),1)  
  os.execv(cmd)

编辑:我不想使用subprocess模块。我正在处理一些复杂的子进程操作,需要手动调用fork

5 个回答

2

哦,你啊。在我看到你例子最后一行提到的execv()之前,我已经准备好了一个不错的答案。真是让人失望。最初的想法是把每个子进程的标准输出替换成这个博客文章中的tee类实例,然后把输出分成原来的标准输出和日志文件:

http://www.shallowsky.com/blog/programming/python-tee.html

但是,由于你使用了execv(),子进程中的tee实例会被覆盖,所以这个方法行不通。

很遗憾,我找不到一个现成的解决方案来解决你的问题。最接近的办法是启动一个实际的tee程序作为子进程;如果你想要更跨平台一点,可以用Python写一个简单的替代品。

首先,要知道在编写tee替代品时:tee其实是个很简单的程序。在我见过的所有真正的C语言实现中,它并没有比这个复杂多少:

while((character = read()) != EOF) {
    /* Write to all of the output streams in here, then write to stdout. */
}

不幸的是,你不能简单地把两个流连接在一起。这会非常有用(这样一个流的输入就能自动转发到另一个流),但我们没有这样的便利,必须自己编写代码。所以,Eli和我的答案会非常相似。不同之处在于,我的答案中,Python的'tee'会通过管道在一个单独的进程中运行;这样,父线程仍然可以继续工作!

(记得也要复制博客文章中的tee类。)

import os, sys

# Open it for writing in binary mode.
logFile=open("bar", "bw")

# Verbose names, but I wanted to get the point across.
# These are file descriptors, i.e. integers.
parentSideOfPipe, childSideOfPipe = os.pipe()

# 'Tee' subprocess.
pid = os.fork()
if pid == 0:
    while True:
        char = os.read(parentSideOfPipe, 1)
        logFile.write(char)
        os.write(1, char)

# Actual command
pid = os.fork()
if pid == 0:
    os.dup2(childSideOfPipe, 1)
    os.execv(cmd)

如果这不是你想要的,我很抱歉,但这是我能找到的最佳解决方案。

祝你项目顺利!

6

下面的内容中,SOMEPATH 是指子程序的路径,这个路径的格式是适合用在 subprocess.Popen 里的(可以查看它的文档了解更多)。

import sys, subprocess


f = open('logfile.txt', 'w')
proc = subprocess.Popen(SOMEPATH, stdout=subprocess.PIPE)

while True:
    out = proc.stdout.read(1)
    if out == '' and proc.poll() != None:
        break
    if out != '':
        # CR workaround since chars are read one by one, and Windows interprets
        # both CR and LF as end of lines. Linux only has LF
        if out != '\r': f.write(out)
        sys.stdout.write(out)
        sys.stdout.flush()
6

这里有一个不使用 subprocess 模块的可行方案。虽然你可以在使用 exec* 函数的同时,利用 subprocess 来处理 tee 进程(只需使用 stdin=subprocess.PIPE,然后把描述符复制到你的 stdout)。

import os, time, sys

pr, pw = os.pipe()
pid = os.fork()

if pid == 0:
    os.close(pw)
    os.dup2(pr, sys.stdin.fileno())
    os.close(pr)
    os.execv('/usr/bin/tee', ['tee', 'log.txt'])
else:
    os.close(pr)
    os.dup2(pw, sys.stdout.fileno())
    os.close(pw)

    pid2 = os.fork()

    if pid2 == 0:
        # Replace with your custom process call
        os.execv('/usr/bin/yes', ['yes'])
    else:
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            pass

需要注意的是,tee 命令内部实际上做的和 Ben 在他的回答中提到的一样:读取输入,然后循环处理输出文件描述符并写入它们。它可能更高效,因为它的实现经过了优化,并且是用 C 语言编写的,但你会面临不同管道的开销(我不确定哪种方案更高效,但在我看来,把一个自定义的类文件对象重新分配给 stdout 是一个更优雅的解决方案)。

一些额外的资源:

撰写回答