将Python的'print'输出重定向到Logger

58
我有一个使用“Print”打印到stdout的Python脚本。最近,我通过Python Logger添加了日志记录,并希望如果启用了日志记录,则这些print语句会转到记录器中。我不想修改或删除这些print语句。
我可以通过执行'log.info("some info msg")'来记录日志。我想能够像这样做:
if logging_enabled:
  sys.stdout=log.info
print("test")

如果启用了日志记录,"test" 应该被记录,就像我执行 log.info("test") 一样。如果未启用日志记录,则 "test" 应该只打印到屏幕上。
这种情况是否可能?我知道可以以类似的方式将 stdout 重定向到文件中(参见:将打印重定向到日志文件)。
7个回答

47

你有两个选项:

  1. 打开一个日志文件,将sys.stdout替换为它,而不是一个函数:

    log = open("myprog.log", "a")
    sys.stdout = log
    
    >>> print("Hello")
    >>> # nothing is printed because it goes to the log file instead.
    
  2. 用你的日志函数替换print:

  3. # If you're using python 2.x, uncomment the next line
    #from __future__ import print_function
    print = log.info
    
    >>> print("Hello!")
    >>> # nothing is printed because log.info is called instead of print
    

9
第一个示例中,是否可能同时将内容写入文件和屏幕标准输出? - Hrvoje T
@HrvojeT:请参阅如何将sys.stdout复制到日志文件? - user
4
log.info接收的参数与print接收的参数意义不相同。例如,file=参数允许你将输出打印到任何文件中。如果你完全使用logger替换print函数,则会失去此功能。只有标准输出(stdout)/标准错误(stderr)的打印应该被截取。 - Stephan Leclercq
2
警告!如果print包含任何不适用于log.info的参数,例如:print("Hello", file = sys.stderr),则此设置将失败。 - hafiz031

28

当然,您可以同时将内容输出到标准输出和追加到日志文件中,像这样:

# Uncomment the line below for python 2.x
#from __future__ import print_function

import logging

logging.basicConfig(level=logging.INFO, format='%(message)s')
logger = logging.getLogger()
logger.addHandler(logging.FileHandler('test.log', 'a'))
print = logger.info

print('yo!')

2
警告!!如果print包含任何不适用于logger.info的参数,例如:print(“yo!”,file = sys.stderr),则此设置将失败。 - hafiz031

22

另一种方法是将记录器封装在一个对象中,将对write的调用转换为记录器的log方法。

Ferry Boender就是这样做的,在GPL许可下提供一篇文章他的网站上。下面的代码基于此,但解决了原始代码中的两个问题:

  1. 该类未实现程序退出时调用的flush方法。
  2. 该类未缓冲换行符,而io.TextIOWrapper对象应该缓冲换行符,导致换行符出现在奇怪的位置。
import logging
import sys


class StreamToLogger(object):
    """
    Fake file-like stream object that redirects writes to a logger instance.
    """
    def __init__(self, logger, log_level=logging.INFO):
        self.logger = logger
        self.log_level = log_level
        self.linebuf = ''

    def write(self, buf):
        temp_linebuf = self.linebuf + buf
        self.linebuf = ''
        for line in temp_linebuf.splitlines(True):
            # From the io.TextIOWrapper docs:
            #   On output, if newline is None, any '\n' characters written
            #   are translated to the system default line separator.
            # By default sys.stdout.write() expects '\n' newlines and then
            # translates them so this is still cross platform.
            if line[-1] == '\n':
                self.logger.log(self.log_level, line.rstrip())
            else:
                self.linebuf += line

    def flush(self):
        if self.linebuf != '':
            self.logger.log(self.log_level, self.linebuf.rstrip())
        self.linebuf = ''


logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s:%(levelname)s:%(name)s:%(message)s',
    filename="out.log",
    filemode='a'
)

stdout_logger = logging.getLogger('STDOUT')
sl = StreamToLogger(stdout_logger, logging.INFO)
sys.stdout = sl

stderr_logger = logging.getLogger('STDERR')
sl = StreamToLogger(stderr_logger, logging.ERROR)
sys.stderr = sl


这可以让你轻松地将所有输出路由到所选的日志记录器。如果需要,你可以像本文其他人提到的那样保存sys.stdout和/或sys.stderr,以便在需要恢复时进行替换。

1
你如何使用它?你是将其放置在期望为每个可能发送输出到stdout的模块生成stdout的模块中吗?还是任何发送到stdout的输出都会被发送到文件,只要我在__main__函数中进行了上述配置? - alpha_989
1
程序退出时崩溃:StreamToLogger类还必须实现flush()方法。 - Stephan Leclercq
有人知道 self.linebuf = '' 的目的吗?我看不到它被用在任何地方。 - Massey101

17

一个更简单的选择,

import logging, sys
logging.basicConfig(filename='path/to/logfile', level=logging.DEBUG)
logger = logging.getLogger()
sys.stderr.write = logger.error
sys.stdout.write = logger.info

12
只有在不使用 logging.StreamHandler() 将日志也输出到屏幕时,这种解决方案才有效。因为如果你这样做,就会导致消息进入无限循环:流处理器尝试将其写入 sys.stdout.write,其中它被重定向到记录器,然后再次被发送到流处理器。 - Decrayer
2
你可以使用 sys.__stderr__ 作为 StreamHandler,它永远不会改变。 - Mitar
1
它说:sys.stdout.write = details.info AttributeError: 'file'对象属性'write'是只读的。 - Linh
1
@Linh 我相信你正在使用Python2.7。根据这个链接,这是2.7版本的一个错误。我也在我的Python2.7中复制了你的错误。我个人建议你切换到Python3.x。 - user3002273
1
这段代码对我来说很好用,因为在设置之前我可以获取到原始的sys.stdout的引用。 - Michael Pfaff
显示剩余4条评论

4

定义日志器之后,可以使用它将print的多个参数重定向到日志器。

print = lambda *tup : logger.info(str(" ".join([str(x) for x in tup]))) 

3
你真的应该换一种方式来实现: 通过调整你的日志配置来使用print语句或其他东西,具体取决于设置。不要覆盖print行为,因为将来可能会引入一些设置(例如,你自己或其他人使用你的模块)可能会将其输出到stdout,这样你就会遇到问题。
有一个处理程序,可以将你的日志消息重定向到适当的流(文件,stdout或任何其他文件类)。它被称为StreamHandler,并且随着logging模块捆绑在一起。
所以基本上我认为你应该做你说你不想做的事情:用实际的日志记录替换print语句。

我将在未来的脚本中使用日志记录,但对于我正在做的目的,现在更新所有内容以适应日志记录并不值得花费时间。不过我会研究一下StreamHandler。 - Rauffle
@Rauffle:随你便。我强烈建议使用C0deH4cker提到的第二种解决方案,否则你可能会遇到我在答案中提到的问题。 - Tadeck
1
我必须使用某些第三方库,这些库将输出发送到stdout。我的应用程序中使用了logging,因此应用程序本身没有任何打印语句。您提到StreamHandler可以将日志消息发送到filestdoutStreamHandler是否可以将stdout的输出发送到文件? - alpha_989
1
如果你正在处理一个通过print()不当地进行日志记录的第三方库,那么这就特别有趣了。我希望将每个print调用都变成logging.info。然后,我可以通过正确的过滤器获取时间戳、线程ID等信息。 - aggieNick02

1

以下代码片段在我的 PySpark 代码中完美运行。如果有人需要了解的话,可以参考以下:

import os
import sys
import logging
import logging.handlers

log = logging.getLogger(__name_)

handler = logging.FileHandler("spam.log")
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
log.addHandler(handler)
sys.stderr.write = log.error 
sys.stdout.write = log.info 

(将每个错误记录在同一目录下的“spam.log”中,不会在控制台/标准输出上显示任何内容)

(将每个信息记录在同一目录下的“spam.log”中,不会在控制台/标准输出上显示任何内容)

要在文件和控制台上打印输出错误/信息,请删除上述两行。

愉快编码 干杯!!!


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