文章目录
存在问题
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种思路:
- pycharm 中其实是可以设置
stderr
输出样式的(默认为红色)(弊端为sys.stderr
中非日志部分的样式不是红色,分辨不出来) - 修改 handlers 的内容,将 stream 设置为
sys.stdout
(弊端为当LOG_STDOUT
为 True 时会产生递归异常) - 将
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,
)