Scrapy 日志初始化分析,实现自定义日志handler和日志着色

存在问题

Scrapy 在 Pycharm 中运行时,日志在终端输出的样式为红色,分析原因为:pycharm 会将 sys.stderr 的内容渲染为红色;

需要知道 scrapy 中的日志如何进行初始化的。


scrapy.utils.log

该模块位于 scrapy/utils/log.py,负责对scrapy框架的日志进行配置

configure_logging

configure_logging 函数负责配置 log,主要做三件事:

  • 将 twisted 的日志使用 python 标准日志库 logging 进行日志输出

    # 转发 twisted 的日志到标准库中
    observer = twisted_log.PythonLoggingObserver("twisted")
    observer.start()
    
  • 将 DEBUG 级别的日志分配给 Scrapy 的 logger,将 ERROR 级别的日志分配给 Twisted 的 logger

  • 将标准输出变成日志输出(LOG_STDOUT 配置)

    if settings.getbool("LOG_STDOUT"):
        sys.stdout = StreamLogger(logging.getLogger("stdout"))  # type: ignore[assignment]
    	# 将标准输出改为 logger
    

该函数在 install_root_handler 为 True (默认)的情况下会调用 install_scrapy_root_handler


install_scrapy_root_handler

install_scrapy_root_handler 该函数会给 logging.root 添加一个 handler(由 _get_handler 从 settings 中读取配置)

# scrapy/utils/log.py#143
# settings 为从 settings.py 读取的配置信息 
def _get_handler(settings: Settings) -> logging.Handler:
    """Return a log handler object according to settings"""
    filename = settings.get("LOG_FILE")  # 输出的日志文件名称
    handler: logging.Handler
    if filename:
        mode = "a" if settings.getbool("LOG_FILE_APPEND") else "w"  # 日志文件模式
        encoding = settings.get("LOG_ENCODING")  # 日志文件编码
        handler = logging.FileHandler(filename, mode=mode, encoding=encoding)
    elif settings.getbool("LOG_ENABLED"):  # 是否启用日志
        handler = logging.StreamHandler()  # 标记1
    else:
        handler = logging.NullHandler()

    formatter = logging.Formatter(  # 设置日志格式
        fmt=settings.get("LOG_FORMAT"), datefmt=settings.get("LOG_DATEFORMAT")
    )
    handler.setFormatter(formatter)
    handler.setLevel(settings.get("LOG_LEVEL"))  # 设置日志最低级别
    if settings.getbool("LOG_SHORT_NAMES"):  # 是否缩写
        handler.addFilter(TopLevelFormatter(["scrapy"]))
    return handler

# scrapy/utils/log.py#125
# 设置 scrapy 的 handler
def install_scrapy_root_handler(settings: Settings) -> None:
    global _scrapy_root_handler

    if (
        _scrapy_root_handler is not None
        and _scrapy_root_handler in logging.root.handlers
    ):
        logging.root.removeHandler(_scrapy_root_handler)
    logging.root.setLevel(logging.NOTSET)
    _scrapy_root_handler = _get_handler(settings)
    logging.root.addHandler(_scrapy_root_handler)
  • 标记1:scrapy 使用的是 logging.StreamHandler,该 StreamHandler 默认使用的就是 sys.stderr

分析

configure_logging 会在 scrapy.CrawlerProcess 中调用:

# scrapy/crawler.py 328
class CrawlerProcess(CrawlerRunner):

    def __init__(
        self,
        settings: Union[Dict[str, Any], Settings, None] = None,
        install_root_handler: bool = True,
    ):
        ...
        configure_logging(self.settings, install_root_handler)
        ...

其中: install_root_handler 默认值为 True

CrawlerProcess 只有在 cmdline 中被调用:

# scrapy/cmdline.py #159
def execute(argv=None, settings=None):
	...
    cmd.crawler_process = CrawlerProcess(settings)
	...

这部分在之前的命令分析文章中提到,是命令执行的关键函数,框架的启动必定会调用该函数;

那么 scrapy_root_handler 必定会被添加到 logging.root.handlers 中,所有日志都会经过该 handler 进行输出


实现

既然已经知道 scrapy 的日志输出是通过将 _scrapy_root_handler 添加到 logging.root.handlers 中实现的,那可以考虑如何解决在 pycharm 中显示为 红色的问题了;

有3种思路:

  1. pycharm 中其实是可以设置 stderr 输出样式的(默认为红色)(弊端为 sys.stderr 中非日志部分的样式不是红色,分辨不出来)
  2. 修改 handlers 的内容,将 stream 设置为 sys.stdout(弊端为当 LOG_STDOUT 为 True 时会产生递归异常)
  3. install_root_handler 设置为 False(这样就会导致 settings.py 的配置信息无效,需要自己读取设置才行)

第1种:修改Pycharm配置

Pycharm中依次点击:File -> Settings -> Editor -> Color Schema -> Console Colors -> Console -> Error outpt

然后就能设置对应的颜色了


第2种:返回自定义handler

可以通过hack的方式修改:

settings.py 加入:

import scrapy.utils.log as log

# 拿到原函数
_get_handler = copy.copy(log._get_handler)

# 自己修改过后的
def get_handler_custom(settings: Settings):
    handler = _get_handler(settings)
    # 如果是 StreamHandler,就修改为 sys.stdout
    if isinstance(handler, logging.StreamHandler):
        handler.setStream(sys.stdout)
    return handler

# 覆盖原来的函数
log._get_handler = get_handler_custom

注意不能设置 LOG_STDOUT 为 True,会递归爆栈的

分析:

  • configure_logging 中有这么一行代码

    if settings.getbool("LOG_STDOUT"):
        sys.stdout = StreamLogger(logging.getLogger("stdout"))  # type: ignore[assignment]
    

    是直接将标准输出定为一个名称为 stdout 的 StreamLogger 了

    StreamLogger 有这样一个方法,将 buf 用 日志输出了

    def write(self, buf: str) -> None:
        for line in buf.rstrip().splitlines():
            self.logger.log(self.log_level, line.rstrip())
    
  • logging.StreamHandler的代码:

    此时先将 sys.stdout 设置为 StreamLogger

    然后我们上面的代码将 stream 设置为 sys.stdout

    def flush(self):
        self.acquire()
        try:
            if self.stream and hasattr(self.stream, "flush"):
                self.stream.flush()
            finally:
                self.release()
    

    这样产生递归了,没有出口,就爆栈了

可以自己返回自定义的 handler,但是日志配置信息还是得自己读取,settings参数已经有了,直接模拟源码读取即可


第3种:日志着色

我当前写的项目中,是项目外部启动(不使用 Scrapy 命令,而是 CrawlerProcess 启动)

那么我直接传入 install_root_handler 为 False 即可

def run_spider(spider_name: str, _settings):
    crawler_process = CrawlerProcess(settings, install_root_handler=False)
    crawl_defer = crawler_process.crawl(spider_name)
    if getattr(crawl_defer, "result", None) is not None and issubclass(
            crawl_defer.result.type, Exception
    ):
        exitcode = 1
    else:
        crawler_process.start()

        if (
                crawler_process.bootstrap_failed
                or hasattr(crawler_process, "has_exception")
                and crawler_process.has_exception
        ):
            exitcode = 1
        else:
            exitcode = 0
    sys.exit(exitcode)

为了输出日志有颜色,我使用了 coloredlogs 这个库来输出:

  • 在 settings.py 中加入:

    import coloredlogs, logging
    coloredlogs.install(level=logging.DEBUG, stream=sys.stdout)
    

如果想要读取配置信息,自己根据 settings 进行配置即可,详见先前分析命令执行的文章

下面是一个简单的例子:

def configure_logging(settings: Settings):
    enabled = settings.getbool("LOG_ENABLED")  # 是否启用日志
    if not enabled:
        return

    filename = settings.get("LOG_FILE")  # 输出的日志文件名称

    log_format = settings.get("LOG_FORMAT")
    log_dateformat = settings.get("LOG_DATEFORMAT")

    level = settings.get("LOG_LEVEL")

    # 输出到文件就不需要着色
    if filename:
        file_mode = "a" if settings.getbool("LOG_FILE_APPEND") else "w"  # 日志文件模式
        encoding = settings.get("LOG_ENCODING")  # 日志文件编码

        handler = logging.FileHandler(
            filename=filename,
            mode=file_mode,
            encoding=encoding
        )

        formatter = logging.Formatter(  # 设置日志格式
            fmt=log_dateformat, datefmt=log_dateformat
        )
        handler.setFormatter(formatter)
        if settings.getbool("LOG_SHORT_NAMES"):  # 是否缩写
            handler.addFilter(log.TopLevelFormatter(["scrapy"]))
        logging.root.handlers.append(handler)

    else:
        coloredlogs.install(
            level=level,
            stream=sys.stdout,
            datefmt=log_dateformat,
            fmt=log_format,
        )
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值