如何将sys.stdout复制到日志文件中?

168

编辑:由于看起来要么没有解决方案,要么我正在做一些如此非标准的事情以至于没有人知道 - 我将修订我的问题,还要问:在 Python 应用程序进行大量系统调用时,实现记录日志的最佳方法是什么?

我的应用程序有两种模式。在交互模式下,我希望所有输出都能显示在屏幕上并写入日志文件,包括任何系统调用的输出。在守护进程模式下,所有输出都写入日志中。使用 os.dup2() 守护进程模式非常好用。但是在交互模式下,我找不到“tee”所有输出到日志的方法,而无需修改每个系统调用。


换句话说,我想要一个类似命令行工具 'tee' 的功能,用于捕获 Python 应用程序生成的所有输出,包括系统调用输出

澄清一下:

为了重定向所有输出,我做了以下操作,效果非常好:

# open our log file
so = se = open("%s.log" % self.name, 'w', 0)

# re-open stdout without buffering
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

# redirect stdout and stderr to the log file opened above
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())

这样做的好处是不需要在代码中特别指定打印调用。此外,代码还运行了一些shell命令,因此不必单独处理它们的输出也很方便。

简单来说,我想做同样的事情,只是要“复制”而不是重定向。

一开始我认为只需简单地反转dup2就可以了,为什么不行?以下是我的测试:

import os, sys

### my broken solution:
so = se = open("a.log", 'w', 0)
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

os.dup2(sys.stdout.fileno(), so.fileno())
os.dup2(sys.stderr.fileno(), se.fileno())
###

print("foo bar")

os.spawnve("P_WAIT", "/bin/ls", ["/bin/ls"], {})
os.execve("/bin/ls", ["/bin/ls"], os.environ)

文件"a.log"应该与屏幕上显示的内容完全相同。


如果您查看man页面(http://www.manpagez.com/man/2/dup2/),dup2的第二个参数始终关闭(如果已经打开)。因此,在您的“错误解决方案”中,它正在关闭so和se,然后重新分配它们的文件描述符给sys.stdout。 - Jacob Gabrielson
1
关于你的编辑:这并不罕见,我以前也做过类似的事情(在其他编程语言中)。虽然Unix允许为同一个文件句柄设置多个“别名”,但它不会将文件句柄“分割”(复制到其他多个文件句柄)。所以你必须自己实现“tee”(或者直接使用“tee”,请参考我的简单回答)。 - Jacob Gabrielson
我认为JohnT的答案比实际被接受的更好。你可能想要更改已接受的答案。 - Phong
我正在做一些非常非标准的事情 - 你真的是这样,人们只是将他们的日志发送到stderr并从命令行处理。 - khachik
18个回答

151

我以前也遇到过这个问题,发现这段代码非常有用:

class Tee(object):
    def __init__(self, name, mode):
        self.file = open(name, mode)
        self.stdout = sys.stdout
        sys.stdout = self
    def __del__(self):
        sys.stdout = self.stdout
        self.file.close()
    def write(self, data):
        self.file.write(data)
        self.stdout.write(data)
    def flush(self):
        self.file.flush()

来源: http://mail.python.org/pipermail/python-list/2007-May/438106.html


9
对于内部处理sys.stdout重新分配的做法给予+1赞,这样你可以通过删除Tee对象来结束日志记录。 - Ben Blank
15
我会在那里加上一个刷新。例如:'self.file.flush()' - Luke Stanley
4
请在答案中注意这个后续链接中的修订版本。 - martineau
5
那样做行不通。直到程序执行结束, __del__ 才会被调用。参考 https://dev59.com/DW025IYBdhLWcg3wRT1K - Nux
6
这个解决方案不会记录除了print()sys.stdout.write()之外的其他调用产生的输出。由Python包装的C代码或Fortran生成的输出也不会被重定向到文件,因为它们直接写入stdout fd(1),而不是IOTextwrapper sys.stdout。不过,Jacob Gabrielson的方法可以解决这个问题,但请查看他回答中的评论以获取提案的修改建议。 - matthieu
显示剩余11条评论

81
print语句将调用分配给sys.stdout的任何对象的write()方法。

我会创建一个小类来同时写入两个位置...

import sys

class Logger(object):
    def __init__(self):
        self.terminal = sys.stdout
        self.log = open("log.dat", "a")

    def write(self, message):
        self.terminal.write(message)
        self.log.write(message)  

sys.stdout = Logger()

现在print语句既会输出到屏幕上,也会添加到您的日志文件中:

# prints "1 2" to <stdout> AND log.dat
print "%d %d" % (1,2)

这显然是一个快速而草率的方法。一些注意事项:

  • 你可能应该将日志文件名作为参数传入。
  • 如果您在程序执行期间不会记录日志,那么您应该将 sys.stdout 恢复为 <stdout>
  • 您可能希望能够同时写入多个日志文件,或处理不同的日志级别等问题。

这些都直截了当,我相信读者们可以轻易完成。关键的见解在于,print 只是调用分配给 sys.stdout 的“类文件对象”。


正是我要发布的内容,差不多就是这样。当你解决write没有self参数的问题时,请+1。另外,将要写入的文件传递进来会更好一些。甚至,将stdout传递进来也可能是更好的设计。 - Devin Jeanpierre
7
我选择了这个答案太快了。它对于“打印”非常有效,但是对于外部命令输出来说效果不是很好。 - drue
2
Logger类还应定义一个flush()方法,例如“def flush():self.terminal.flush(); self.log.flush()” - blokeley
6
你说“print语句会调用分配给sys.stdout的任何对象的write()方法”。那么其他函数将数据发送到stdout而不使用print呢?例如,如果我使用subprocess.call创建一个进程,则其输出将显示在控制台上而不是log.dat文件中...是否有方法来解决这个问题? - jpo38
@blokeley 这就是我需要的 :) - z3ntu
显示剩余4条评论

67

既然您喜欢从您的代码中生成外部进程,您可以直接使用tee。我不知道有任何Unix系统调用可以完全取代tee的功能。

# Note this version was written circa Python 2.6, see below for
# an updated 3.3+-compatible version.
import subprocess, os, sys

# Unbuffer output (this ensures the output is in the correct order)
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

tee = subprocess.Popen(["tee", "log.txt"], stdin=subprocess.PIPE)
os.dup2(tee.stdin.fileno(), sys.stdout.fileno())
os.dup2(tee.stdin.fileno(), sys.stderr.fileno())

print "\nstdout"
print >>sys.stderr, "stderr"
os.spawnve("P_WAIT", "/bin/ls", ["/bin/ls"], {})
os.execve("/bin/ls", ["/bin/ls"], os.environ)
你也可以使用multiprocessing包(或者在使用Python 2.5或更早版本时使用processing)来模拟tee。(更新:下面是适用于Python 3.3及以上版本的代码示例:)
import subprocess, os, sys

tee = subprocess.Popen(["tee", "log.txt"], stdin=subprocess.PIPE)
# Cause tee's stdin to get a copy of our stdin/stdout (as well as that
# of any child processes we spawn)
os.dup2(tee.stdin.fileno(), sys.stdout.fileno())
os.dup2(tee.stdin.fileno(), sys.stderr.fileno())

# The flush flag is needed to guarantee these lines are written before
# the two spawned /bin/ls processes emit any output
print("\nstdout", flush=True)
print("stderr", file=sys.stderr, flush=True)

# These child processes' stdin/stdout are 
os.spawnve("P_WAIT", "/bin/ls", ["/bin/ls"], {})
os.execve("/bin/ls", ["/bin/ls"], os.environ)

36
好的,这个答案是可行的,所以我会接受它。不过,它让我感觉很不舒服。 - drue
2
我刚刚发布了一个纯Python实现的tee(py2/3兼容),可以在任何平台上运行,并且还可以在不同的日志配置中使用。 https://dev59.com/gHRB5IYBdhLWcg3wc3A0#3423392 - sorin
8
如果 Python 可以在我的计算机上运行,而解决方案却不能,则这不是一个“Pythonic”的解决方案。因此被人们投票否定。 - anatoly techtonik
2
根据这篇帖子,自Python 3.3起(参见PEP 3116),该行代码sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)不再起作用。 - Ken Myers
2
不错的建议, 我会加上: (... , preexec_fn=lambda: signal.signal(signal.SIGINT, signal.SIG_IGN)一旦用户按下“Ctrl+C”,“tee”进程首先被杀死,关闭stdout\stderr FDs,然后主线程中没有任何输出。 这解决了问题 :) - Aviad
显示剩余7条评论

65
您真正需要的是标准库中的 logging 模块。创建一个记录器并附加两个处理器,一个将写入文件,另一个将写入 stdout 或 stderr。
详情请参见在多个目标上记录日志

11
日志模块不会将异常和其他重要的输出记录到标准输出(stdout),这在分析构建服务器上的日志时非常有用(例如)。 - anatoly techtonik
3
logging 模块无法重定向系统调用的输出,比如 os.write(1, b'stdout') - jfs

16

下面是另一种方案,比其他方案更通用,它支持将输出(写入sys.stdout)拆分为任意数量的文件对象。没有要求必须包含__stdout__本身。

import sys

class multifile(object):
    def __init__(self, files):
        self._files = files
    def __getattr__(self, attr, *args):
        return self._wrap(attr, *args)
    def _wrap(self, attr, *args):
        def g(*a, **kw):
            for f in self._files:
                res = getattr(f, attr, *args)(*a, **kw)
            return res
        return g

# for a tee-like behavior, use like this:
sys.stdout = multifile([ sys.stdout, open('myfile.txt', 'w') ])

# all these forms work:
print 'abc'
print >>sys.stdout, 'line2'
sys.stdout.write('line3\n')

注意:这仅是一个概念验证。实现在这里不完整,因为它仅包装文件对象的方法(例如write ),省略了成员/属性/setattr等。但是,就目前而言,对于大多数人来说足够好。

我喜欢它的原因不仅在于其通用性,而且在于它的干净程度,因为它没有直接调用 write , flush , os.dup2 等。


3
我会将__init__函数的输入参数改为*files而非files,但除此之外,是的,就是这样。其他解决方案都没有单独隔离“tee”功能而不尝试解决其他问题。如果您想在输出的每个内容上加前缀,可以在一个前缀写入器类中包装此类。(如果您只想对一个流添加前缀,可以包装一个流并将其交给此类。)这种方法还有一个优点,即multifile([])创建一个忽略所有内容的文件(如open('/dev/null'))。 - Ben
为什么这里要使用_wrap?你不能将其中的代码复制到__getattr__中,让它们能够达到同样的效果吗? - timotree
@Ben 实际上,multifile([]) 创建的文件在调用其方法时会引发 UnboundLocalError。(res 未被分配就返回了) - timotree
由于它是多文件,您还可以添加sys.stderr。它也适用于Windows。唯一的缺点是:在Python 2.x中,在异常中打印Traceback会丢失并且无法记录。 - Eric H.
毫无疑问,sys.stdout在sys.stderr之前。例如:sys.stdout = multifile([ sys.stdout, open('myfile.txt', 'w') ]) sys.stderr = multifile([ sys.stderr, open('myfile.txt', 'w') ]) sys.stderr.write( "Tests 1...\n" ) sys.stdout.write( "Tests 2...\n" ) Tests 2在Tests 1之前。 - Iaoceot

15

我知道这个问题已经被反复回答过了,但是我从John T's的答案中提取了主要答案,并对其进行了修改,使其包含了建议的清空操作,并遵循了其链接的修订版本。我还按照cladmi's的答案中提到的,在使用with语句时添加了enter和exit。此外,文档提到使用os.fsync()来清空文件,所以我也添加了它。我不知道你是否真的需要它,但是它在那里。

import sys, os

class Logger(object):
    "Lumberjack class - duplicates sys.stdout to a log file and it's okay"
    #source: https://dev59.com/gHRB5IYBdhLWcg3wc3A0
    def __init__(self, filename="Red.Wood", mode="a", buff=0):
        self.stdout = sys.stdout
        self.file = open(filename, mode, buff)
        sys.stdout = self

    def __del__(self):
        self.close()

    def __enter__(self):
        pass

    def __exit__(self, *args):
        self.close()

    def write(self, message):
        self.stdout.write(message)
        self.file.write(message)

    def flush(self):
        self.stdout.flush()
        self.file.flush()
        os.fsync(self.file.fileno())

    def close(self):
        if self.stdout != None:
            sys.stdout = self.stdout
            self.stdout = None

        if self.file != None:
            self.file.close()
            self.file = None

您可以随后使用它。
with Logger('My_best_girlie_by_my.side'):
    print("we'd sing sing sing")

或者
Log=Logger('Sleeps_all.night')
print('works all day')
Log.close()

非常感谢 @Status,您解决了我的问题(https://dev59.com/VZrga4cB1Zd3GeqPiy31)。我会放置一个链接到您的解决方案。 - Mohammad ElNesr
1
@MohammadElNesr 我刚意识到当代码与 with 语句一起使用时存在一个问题。我已经修复了它,现在可以正确地在 with 块的末尾关闭。 - Status
3
对我来说这很有效,只需要将模式改为mode="ab",在write函数中使用self.file.write(message.encode("utf-8"))即可。 - ennetws

13

为了补充John T的回答:https://dev59.com/gHRB5IYBdhLWcg3wc3A0#616686

我添加了__enter____exit__方法,以便将其作为上下文管理器使用with关键字,代码如下:

class Tee(object):
    def __init__(self, name, mode):
        self.file = open(name, mode)
        self.stdout = sys.stdout
        sys.stdout = self

    def __del__(self):
        sys.stdout = self.stdout
        self.file.close()

    def write(self, data):
        self.file.write(data)
        self.stdout.write(data)

    def __enter__(self):
        pass

    def __exit__(self, _type, _value, _traceback):
        pass

它可以被用作

with Tee('outfile.log', 'w'):
    print('I am written to both stdout and outfile.log')

5
我会将 __del__ 功能移植到 __exit__ 中。 - vontrapp
2
实际上,我认为使用__del__是一个不好的主意。它应该被移动到一个“close”函数中,在__exit__中调用。 - cladmi

13

正如其他地方所描述的那样,也许最好的解决方案是直接使用logging模块:

import logging

logging.basicConfig(level=logging.DEBUG, filename='mylog.log')
logging.info('this should to write to the log file')
然而,有时候(很少)你确实希望重定向标准输出。我遇到这种情况是在扩展Django的runserver命令时,它使用print语句:我不想篡改Django源码但需要将print语句输出到文件中。
以下是一种使用logging模块将标准输出和错误重定向离开shell的方法:
import logging, sys

class LogFile(object):
    """File-like object to log text using the `logging` module."""

    def __init__(self, name=None):
        self.logger = logging.getLogger(name)

    def write(self, msg, level=logging.INFO):
        self.logger.log(level, msg)

    def flush(self):
        for handler in self.logger.handlers:
            handler.flush()

logging.basicConfig(level=logging.DEBUG, filename='mylog.log')

# Redirect stdout and stderr
sys.stdout = LogFile('stdout')
sys.stderr = LogFile('stderr')

print 'this should to write to the log file'

如果你真的无法直接使用logging模块,那么你才应该使用这个LogFile实现。


1
如先前所述,使用这种解决方案,您将不会看到异常和其他问题,而这正是在这种情况下最重要的。 - ashrasmun

11

我在Python中编写了一个tee()实现,适用于大多数情况,并且在Windows上也可以使用。

https://github.com/pycontribs/tendo

此外,如果需要,你可以与Python的logging模块一起使用它。


1
哇,你的软件包真棒,特别是如果你知道 Windows 控制台文化有多繁琐,但你没有放弃让它工作! - n611x007

8
这是一个使用Python日志模块的示例程序。自从2.3版本以来,该日志模块一直存在。在这个示例中,日志记录可通过命令行选项进行配置。在安静模式下,它只会记录到文件中,在正常模式下,它会同时记录到文件和控制台。
import os
import sys
import logging
from optparse import OptionParser

def initialize_logging(options):
    """ Log information based upon users options"""

    logger = logging.getLogger('project')
    formatter = logging.Formatter('%(asctime)s %(levelname)s\t%(message)s')
    level = logging.__dict__.get(options.loglevel.upper(),logging.DEBUG)
    logger.setLevel(level)

    # Output logging information to screen
    if not options.quiet:
        hdlr = logging.StreamHandler(sys.stderr)
        hdlr.setFormatter(formatter)
        logger.addHandler(hdlr)

    # Output logging information to file
    logfile = os.path.join(options.logdir, "project.log")
    if options.clean and os.path.isfile(logfile):
        os.remove(logfile)
    hdlr2 = logging.FileHandler(logfile)
    hdlr2.setFormatter(formatter)
    logger.addHandler(hdlr2)

    return logger

def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    # Setup command line options
    parser = OptionParser("usage: %prog [options]")
    parser.add_option("-l", "--logdir", dest="logdir", default=".", help="log DIRECTORY (default ./)")
    parser.add_option("-v", "--loglevel", dest="loglevel", default="debug", help="logging level (debug, info, error)")
    parser.add_option("-q", "--quiet", action="store_true", dest="quiet", help="do not log to console")
    parser.add_option("-c", "--clean", dest="clean", action="store_true", default=False, help="remove old log file")

    # Process command line options
    (options, args) = parser.parse_args(argv)

    # Setup logger format and output locations
    logger = initialize_logging(options)

    # Examples
    logger.error("This is an error message.")
    logger.info("This is an info message.")
    logger.debug("This is a debug message.")

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

好的答案。我看到了一些非常复杂的方法来复制日志记录到控制台,但使用stderr创建StreamHandler是我一直在寻找的答案 :) - meatvest
代码写得很好,但并没有回答问题——它将日志输出到文件和stderr,而原始问题是要求将stderr复制到日志文件中。 - emem

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