4、Python日志模块入门:logging模块基础、Handler、Formatter、Filter的使用
做运动控制调试,最怕什么?
我告诉你,最怕程序跑飞了,你连它死在哪一步都不知道。
早期我做伺服驱动调试,全靠print大法。代码里到处是print("step1")、print("step2")。后来发现,print输出混在一起,根本分不清是哪个模块打的。更坑的是,正式上线前还得一个个删掉,删漏一个就等着被领导骂。
直到我用了Python的logging模块,才算是真正解放了。今天我就把这套东西掰开揉碎了讲给你听。
4.1 为什么不用print?
print的问题很明显:
- 没有级别区分——紧急信息和普通信息混在一起
- 没有时间戳——你根本不知道这条日志是什么时候打的
- 没有目标控制——要么全打印,要么全不打印
- 正式环境还得删代码——这是最蠢的做法
logging模块就是来解决这些问题的。它把日志分成了五个级别:DEBUG、INFO、WARNING、ERROR、CRITICAL。你可以控制只输出某个级别以上的日志。
核心概念:logging模块由四个组件构成——Logger(记录器)、Handler(处理器)、Formatter(格式化器)、Filter(过滤器)。说白了,Logger负责产生日志,Handler负责把日志送到该去的地方,Formatter决定日志长什么样,Filter决定哪些日志能过。
4.2 最简单的用法
先来个最基础的例子。你什么都不用配置,直接就能用:
import logging
logging.debug("这是debug信息")
logging.info("这是info信息")
logging.warning("这是warning信息")
logging.error("这是error信息")
logging.critical("这是critical信息")
运行一下你会发现,只有WARNING及以上级别的日志输出了。为什么?因为默认的日志级别就是WARNING。这是Python设计者的考虑——正式环境一般不需要看DEBUG和INFO信息。
我的习惯:开发阶段我会把级别设成DEBUG,上线前改成INFO或WARNING。这样调试信息不会污染正式日志。
4.3 配置日志级别和格式
光用默认配置肯定不够。你需要自己控制日志的格式和输出级别。看这个例子:
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logging.debug("这条会输出")
logging.info("这条也会输出")
basicConfig是快速配置的方法。但要注意,它只能调用一次,后面的调用会失效。我在项目中吃过这个亏——在多个模块里分别调了basicConfig,结果只有第一个生效。
4.4 Handler——日志该去哪?
Handler决定了日志的最终去向。常见的Handler有:
- StreamHandler——输出到控制台
- FileHandler——输出到文件
- RotatingFileHandler——按文件大小滚动
- TimedRotatingFileHandler——按时间滚动
我一般会同时用两个Handler:一个输出到控制台方便实时看,一个写入文件方便事后查。像这样:
import logging
logger = logging.getLogger("motion_control")
logger.setLevel(logging.DEBUG)
# 控制台Handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 文件Handler
file_handler = logging.FileHandler("motion.log")
file_handler.setLevel(logging.DEBUG)
# 给logger添加Handler
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.debug("这条只进文件")
logger.info("控制台和文件都能看到")
注意:Handler也有自己的级别。Logger的级别是总开关,Handler的级别是分开关。只有两个级别都通过了,日志才会输出。我曾经在这里栽过跟头——Logger设了DEBUG,但Handler设了WARNING,结果DEBUG信息死活不出来。
4.5 Formatter——日志长什么样?
Formatter就是给日志化妆的。你可以控制它显示时间、模块名、行号、线程ID等等。常用的格式字段:
| 字段 | 含义 | 示例 |
|---|---|---|
| %(asctime)s | 时间戳 | 2024-01-15 14:30:25 |
| %(name)s | Logger名称 | motion_control |
| %(levelname)s | 日志级别 | INFO |
| %(message)s | 日志内容 | 电机启动成功 |
| %(filename)s | 文件名 | main.py |
| %(lineno)d | 行号 | 42 |
我个人习惯的格式是:
formatter = logging.Formatter(
'%(asctime)s | %(name)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s'
)
这样一眼就能看出日志来自哪个文件的哪一行,调试起来特别方便。
4.6 Filter——日志的守门员
Filter的作用是过滤日志。比如你只想看某个模块的ERROR日志,或者只想看包含特定关键字的日志。Filter可以精确控制。
举个例子,假设你只想记录包含"电机"二字的日志:
import logging
class MotorFilter(logging.Filter):
def filter(self, record):
return "电机" in record.getMessage()
logger = logging.getLogger("motion_control")
logger.addFilter(MotorFilter())
logger.info("电机启动成功") # 这条会通过
logger.info("传感器数据异常") # 这条会被过滤掉
避坑指南:Filter是在Handler之前执行的。如果你在Logger上加了Filter,所有Handler都会受影响。如果你只想对某个Handler做过滤,就把Filter加在那个Handler上。
4.7 知识体系总览
下面这张图帮你理清logging模块的整体结构:
4.8 实战建议
最后给你几个我在运动控制项目中的实战建议:
- 按模块创建Logger——每个运动轴、每个传感器模块都创建独立的Logger,方便定位问题
- 日志文件要滚动——运动控制程序可能跑几天几夜,日志文件会非常大。用RotatingFileHandler按大小滚动,或者用TimedRotatingFileHandler按天滚动
- 关键操作打INFO——电机启停、参数变更这些关键操作,一定要打INFO日志
- 异常信息打ERROR——捕获到异常时,用logger.exception()会自动带上堆栈信息
- 调试信息打DEBUG——PID计算值、传感器原始数据这些,打DEBUG级别,正式环境关掉
总结一下:logging模块其实不复杂。记住四个组件——Logger、Handler、Formatter、Filter。Logger负责产生,Handler负责发送,Formatter负责美化,Filter负责把关。把这四个东西搞明白了,你的日志系统就稳了。