scrapy日志系统

Posted Python成长路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了scrapy日志系统相关的知识,希望对你有一定的参考价值。

前言

如何修改scrapy的日志?我想大部分人想到的都是修改配置文件的几个参数。参数如下:

  • LOG_ENABLED:  是否启用log
  • LOG_ENCODING: log编码,仅针对保存文件时
  • LOG_FILE: 日志文件名
  • LOG_FORMAT:日志格式化字符串
  • LOG_DATEFORMAT:日志时间格式化字符串
  • LOG_LEVEL:日志级别
  • LOG_STDOUT:是否重定向stdout
  • LOG_SHORT_NAMES:过滤掉大部分日志,没啥用
  • LOG_FORMATTER:格式化日志的类,默认为scrapy.logformatter.LogFormatter

但是当你设置了LOG_FILE时,日志虽然保存到了文件,但在终端上不显示了。这其实也很合理,因为你可以实时打印文件将日志打印在终端,比如tail -f 日志文件名。不过这是在Linux上可以这样操作,在Windows的时候,我们更倾向于界面的显示,但是又不能不保存日志待以后检查问题。就不能既打印在终端又保存在文件吗?

scrapy同时保存日志到终端和文件

scrapy的日志其实使用的就是python自带的logging模块,先看看使用logging如何打印终端和保存在文件

import os
import sys
import time
import logging


class Logger:
    @staticmethod
    def get_logger(name=None):
        logger = logging.getLogger(f'wechat.{name}')
        logger.setLevel(level=logging.INFO)
        if not logger.handlers:
            stream_handler = logging.StreamHandler(sys.stdout)
            stream_handler.setLevel(level=logging.INFO)
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s')
            stream_handler.setFormatter(formatter)
            logger.addHandler(stream_handler)

            # FileHandler
            os.makedirs('log', exist_ok=True)
            today = time.strftime('%Y-%m-%d',time.localtime())
            file_handler = logging.FileHandler(f'log/{today}.log', encoding='utf-8')
            file_handler.setLevel(level=logging.INFO)
            formatter = logging.Formatter('%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s')
            file_handler.setFormatter(formatter)
            logger.addHandler(file_handler)

        return logger

if not logger.handlers:这一句的判断是因为,logging会重复打印多个日志。

既然scrapy使用的也是logging模块,那我能不能给他加一个打印在屏幕的handle,先看看spider中logger属性的代码: https://docs.scrapy.org/en/latest/_modules/scrapy/spiders.html#Spider:

@property
def logger(self):
    logger = logging.getLogger(self.name)
    return logging.LoggerAdapter(logger, {'spider': self})

def log(self, message, level=logging.DEBUG, **kw):
    self.logger.log(level, message, **kw)

那我能不能重写logger方法,来将日志也打印在终端,

@property
def logger(self):
        logger = logging.getLogger(self.name)
        stream_handler = logging.StreamHandler()
        stream_handler.setLevel(level=logging.INFO)
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s')
        stream_handler.setFormatter(formatter)
        logger.addHandler(stream_handler)
        return logging.LoggerAdapter(logger, {'spider': self})

发现还是没打印在终端,为啥呢?因为这个logger只控制你写的spider里面的日志,如果你spider没有使用self.logger就不会打印。我们在spider加一个打印信息就会发现终端有输出结果了。其实我们一般也只关心spider里面的日志内容,所以这个方法足够满足一般情况了。

修改源代码

如何将scrapy所有的日志内容都打印在终端,要想做到这个,肯定是需要修改scrapy的源代码了。我们先看看scrapy是在哪里操作日志的,可以打开scrapy的源码全局搜索LOG_FILE或者FileHandler, 可以看到scrapy控制logging的代码都放在scrapy.utils.log这个文件里面, 也可以在官网查看:https://docs.scrapy.org/en/latest/_modules/scrapy/utils/log.html,处理handle的主要是这两个方法:


def install_scrapy_root_handler(settings):
    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)

def _get_handler(settings):
    """ Return a log handler object according to settings """
    filename = settings.get('LOG_FILE')
    if filename:
        encoding = settings.get('LOG_ENCODING')
        handler = logging.FileHandler(filename, encoding=encoding)
    elif settings.getbool('LOG_ENABLED'):
        handler = logging.StreamHandler()
    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

_get_handler方法根据settings文件中的配置参数来生成相应的handle,install_scrapy_root_handler方法用来添加handle到logger。只需要稍微修改这两个函数就可以实现功能,需要直接修改python的包路径里面的代码,修改前记得备份原文件:

def install_scrapy_root_handler(settings):
    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)
    handles = _get_handler(settings)
    _scrapy_root_handler = handles[0]
    for handler in handles:
        logging.root.addHandler(handler)

def _get_handler(settings):
    """ Return a log handler object according to settings """
    filename = settings.get('LOG_FILE')
    handlers = []
    if filename:
        encoding = settings.get('LOG_ENCODING')
        file_handler = logging.FileHandler(filename, encoding=encoding)
        stream_hander = logging.StreamHandler()
        handlers.append(file_handler)
        handlers.append(stream_hander)
    elif settings.getbool('LOG_ENABLED'):
        stream_hander = logging.StreamHandler()
        handlers.append(stream_hander)
    else:
        null_handler = logging.NullHandler()
        handlers.append(null_handler)
    for handler in handlers:
        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 handlers

不修改源代码

修改源码虽然可以一劳永逸,但是一般是不推荐的,怎么在不修改源码的基础上做改动呢?

其实可以从上面的代码看出来,log文件的只是操作了logging.root,那么我们直接在自己写的spider里操作logging.root行不行呢?

直接把这段代码加在文件的开头:

import logging

stream_handler = logging.StreamHandler()
stream_handler.setLevel(level=logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s')
stream_handler.setFormatter(formatter)
logging.root.addHandler(stream_handler)

这样同样可以达到效果,当然实际使用的话,需要读取配置文件中LOG_FILE的值,如果存在再添加handle,如果不存在就不添加了。

其实还有一种方法。从CrawlerProcess着手,因为在scrapy源码中全局搜索scrapy.utils.log发现只有crawler.py这个文件导入了这个类里面的这两个方法,CrawlerProcess.crawl这个方法传入的第一个参数类型是crawler.Crawler这个类的对象,而在这个类中同样处理了日志的handle。所以我们重写这个类,然后传递给CrawlerProcess.crawl就可以实现同样的效果, 不过这个会更麻烦一点,还是推荐前两种吧。


以上是关于scrapy日志系统的主要内容,如果未能解决你的问题,请参考以下文章

scrapy日志系统

Windows10 下安装scrapy 日志

scrapy爬虫系列之三--爬取图片保存到本地及日志的基本用法

argparse 代码片段只打印部分日志

常用python日期日志获取内容循环的代码片段

Scrapy Spider没有返回所有元素