为什么你还在用print调试?
如果你还在代码里塞满 print(f"变量X的值是:{x}") 来排查问题,这篇文章就是为你写的。
print三宗罪:
- 上线前要一行行删掉,删完又怕下次还要加回来
- 程序崩溃了,最后几行print根本没输出到屏幕上
- 日志和正常业务输出混在一起,
grep都救不了你
Python标准库自带的 logging 模块,能让你用5行代码替换掉满屏的print,而且支持:分级输出、写入文件、自动轮转、多模块统一管理。本教程从零开始,带你一步步掌握。
第一步:5行代码告别print
最简版本——把日志同时输出到控制台和文件:
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('app.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 像print一样简单,但功能强100倍
logger.info("程序启动")
logger.warning("配置文件未找到,使用默认值")
logger.error("数据库连接失败", exc_info=True)
效果:
- 控制台实时看到日志
- 文件
app.log持久保存,程序崩溃也能追溯 - 时间戳自动带上,不用手动加
exc_info=True自动打印完整异常堆栈
第二步:理解日志等级——什么时候用什么
logging 有5个标准等级(从低到高):
| 等级 | 数值 | 使用场景 |
|---|---|---|
DEBUG |
10 | 开发调试,如”进入函数X,参数Y=42″ |
INFO |
20 | 业务流程,如”用户登录成功”、”订单创建” |
WARNING |
30 | 可恢复的问题,如”重试第3次”、”磁盘空间不足80%” |
ERROR |
40 | 功能异常,如”支付失败”、”API超时” |
CRITICAL |
50 | 系统级灾难,如”数据库挂了”、”内存耗尽” |
关键规则:设置 level=logging.INFO 后,低于INFO的DEBUG日志不会输出。生产环境设为INFO或WARNING,开发环境设为DEBUG。
第三步:日志写进文件,按天/按大小自动切割
直接写到一个文件会越来越大,生产环境必须用 RotatingHandler 实现自动轮转。
方案A:按文件大小切割(RotatingFileHandler)
import logging
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
'app.log', # 日志文件
maxBytes=10*1024*1024, # 10MB一个文件
backupCount=5, # 保留最近5个备份
encoding='utf-8'
)
handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s: %(message)s'
))
logger = logging.getLogger('myapp')
logger.setLevel(logging.INFO)
logger.addHandler(handler)
# 写够10MB后自动生成 app.log.1, app.log.2 ...
for i in range(10000):
logger.info(f"第{i}条日志:这是一条测试日志数据" * 100)
方案B:按时间切割(TimedRotatingFileHandler)
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
'app.log',
when='midnight', # 每天午夜切割
interval=1, # 每1个when单位
backupCount=30, # 保留最近30天
encoding='utf-8'
)
handler.suffix = "%Y-%m-%d" # 备份文件命名:app.log.2026-06-09
其他 when 选项: 'S'秒、'M'分钟、'H'小时、'D'天、'W0'–'W6'每周某天。
第四步:多模块项目的日志架构
当项目有多个文件时,每个模块用 logging.getLogger(name) 获取自己的logger,日志会按模块名形成层级。
# main.py —— 主入口,只在这里配置一次basicConfig
import logging
import logging.config
from crawler import fetch_data
from parser import parse_html
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
handlers=[logging.FileHandler('crawler.log', encoding='utf-8'),
logging.StreamHandler()]
)
logger = logging.getLogger(__name__)
logger.info("爬虫启动")
fetch_data()
logger.info("爬虫完成")
# —— crawler.py ——
import logging
logger = logging.getLogger(__name__) # logger名为 'crawler'
def fetch_data():
logger.info("开始抓取页面...")
# ... HTTP请求 ...
logger.warning("第3次重试中...")
return data
# —— parser.py ——
import logging
logger = logging.getLogger(__name__) # logger名为 'parser'
def parse_html(html):
logger.debug("解析HTML,长度 %d", len(html)) # level=INFO时不输出
# ...
控制台输出效果:
2026-06-09 10:00:01 [INFO] __main__: 爬虫启动
2026-06-09 10:00:02 [INFO] crawler: 开始抓取页面...
2026-06-09 10:00:05 [WARNING] crawler: 第3次重试中...
2026-06-09 10:00:06 [INFO] __main__: 爬虫完成
模块名自动出现在日志里,一眼看出问题出在哪个文件。
第五步:用dictConfig统一管理配置
当项目变大,把日志配置抽到字典或YAML里,一处修改全局生效:
import logging.config
LOGGING_CONFIG = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
},
'detailed': {
'format': '%(asctime)s [%(levelname)s] %(name)s '
'(%(filename)s:%(lineno)d): %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'INFO',
'formatter': 'standard',
},
'file': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'level': 'DEBUG',
'formatter': 'detailed',
'filename': 'logs/app.log',
'when': 'midnight',
'backupCount': 30,
'encoding': 'utf-8'
},
'error_file': {
'class': 'logging.FileHandler',
'level': 'ERROR',
'formatter': 'detailed',
'filename': 'logs/error.log',
'encoding': 'utf-8'
}
},
'root': {
'level': 'DEBUG',
'handlers': ['console', 'file', 'error_file']
}
}
logging.config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger(__name__)
logger.info("配置已加载") # → 控制台 + app.log
logger.error("磁盘写入失败!") # → 控制台 + app.log + error.log
这个配置做到了:INFO以上推控制台,所有等级写app.log并按天切割,ERROR以上额外写一份到error.log便于快速定位严重问题。
第六步:生产环境实战——Flask/Django 日志集成
Flask 日志配置
from flask import Flask
import logging
from logging.handlers import RotatingFileHandler
app = Flask(__name__)
# 配置Flask自带的logger
handler = RotatingFileHandler('flask_app.log', maxBytes=5*1024*1024,
backupCount=10, encoding='utf-8')
handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s'
))
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
@app.route('/')
def index():
app.logger.info("首页被访问,IP: %s", request.remote_addr)
return "Hello World"
# 同时配置werkzeug(Flask内置服务器)的日志级别
logging.getLogger('werkzeug').setLevel(logging.WARNING)
Django 日志配置
在 settings.py 中添加:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': '/var/log/django/app.log',
'when': 'midnight',
'backupCount': 30,
'formatter': 'verbose',
},
},
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'root': {
'handlers': ['file'],
'level': 'INFO',
},
}
第七步:用Filter实现精细化日志过滤
上面讲的是按等级过滤(level),但实际场景往往更复杂。比如:只想看某个用户的日志、只想记录耗时超过1秒的数据库查询。这时就需要自定义Filter。
import logging
import time
class SlowQueryFilter(logging.Filter):
"""只记录慢查询(耗时>1秒)"""
def filter(self, record):
# record.args 是 logger.info("... %s", value) 中的 value
duration = getattr(record, 'duration', 0)
if duration > 1.0:
record.msg = f"[慢查询 {duration:.1f}s] {record.msg}"
return True
return False # False = 丢弃这条日志
# 使用
logger = logging.getLogger('db')
slow_handler = logging.FileHandler('slow_queries.log', encoding='utf-8')
slow_handler.addFilter(SlowQueryFilter())
logger.addHandler(slow_handler)
# 模拟
def query(sql):
start = time.time()
# ... 执行SQL ...
elapsed = time.time() - start
logger.info(sql, extra={'duration': elapsed})
query("SELECT * FROM users") # 0.1s → 不记录
query("SELECT * FROM orders WHERE ...") # 2.3s → 记录到slow_queries.log
Filter的强大之处在于:你可以基于任何条件(record内容、上下文变量、时间、用户ID等)动态决定是否记录以及如何记录。
第八步:在日志中携带业务上下文
定位问题时常需要知道”当时是哪个用户在操作”。用 extra 参数把业务上下文注入日志:
import logging
# 自定义formatter,支持%(user_id)s这样的自定义字段
formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] user=%(user_id)s: %(message)s'
)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger = logging.getLogger('api')
logger.addHandler(handler)
# 方案一:每次手动传
logger.info("订单创建成功", extra={'user_id': 'u_8842'})
# 方案二(推荐):用LoggerAdapter自动注入
class UserAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
kwargs.setdefault('extra', {})
kwargs['extra']['user_id'] = self.extra.get('user_id', 'anonymous')
return msg, kwargs
adapted = UserAdapter(logger, {'user_id': 'u_8842'})
adapted.info("支付完成")
adapted.warning("余额不足")
# 输出:2026-06-09 10:00:01 [INFO] user=u_8842: 支付完成
# 2026-06-09 10:00:01 [WARNING] user=u_8842: 余额不足
使用 LoggerAdapter,一次传入用户上下文,后续所有日志自动带上用户ID,比每次手动写 extra= 干净得多。
第九步:异常日志最佳实践
捕获异常时记录日志,有四种写法——但只有一种是对的:
import logging
logger = logging.getLogger(__name__)
# ❌ 错误写法1:丢失了堆栈信息
try:
1 / 0
except ZeroDivisionError as e:
logger.error(f"出错:{e}") # 只有"division by zero",不知道哪行
# ❌ 错误写法2:日志等级用错了
try:
open('/nofile')
except FileNotFoundError:
logger.warning("文件不存在") # 应该用error,不是warning
# ❌ 错误写法3:字符串格式化在异常发生前就执行了
# logger.debug("用户 %s 的数据: %s" % (user, get_expensive_data()))
# get_expensive_data() 即使level=INFO也会执行!
# ✅ 正确写法
try:
result = risky_operation()
except Exception:
logger.exception("执行risky_operation时发生异常")
# exception() 等价于 error(msg, exc_info=True)
核心规则:
- 用
logger.exception()代替logger.error(str(e)),保留完整堆栈 - 用
%s占位符而非f-string:logger.debug("用户 %s 登录", user)优于logger.debug(f"用户 {user} 登录"),因为前者的字符串拼接只在日志实际输出时才发生 - 不要吞异常:记录完日志后,决定是
raise还是返回默认值,不要把异常悄悄吃掉
常见报错与解决
1. 日志重复输出
现象:同一条日志在控制台打印了两次甚至三次。
原因:多次调用 basicConfig() 或 addHandler() 加重复了。
解决:
# 在addHandler前检查
if not logger.handlers:
logger.addHandler(handler)
# 或者用 hasHandlers()
if not logger.hasHandlers():
logger.addHandler(handler)
2. 日志文件中文乱码
解决:所有 FileHandler 都加上 encoding='utf-8'。
handler = logging.FileHandler('app.log', encoding='utf-8')
3. basicConfig 不生效
原因:basicConfig() 只在第一次调用时生效。如果某个模块导入时就调了 logging.info()(会触发默认配置),后续的 basicConfig() 就被忽略了。
解决:在入口文件最开始(所有import之前)调用 basicConfig(),或用 logging.config.dictConfig 替代。
FAQ
Q: 日志文件会无限增大吗?怎么控制磁盘占用?
不会,只要你用 RotatingFileHandler(按大小轮转)或 TimedRotatingFileHandler(按时间轮转)。设置 maxBytes 和 backupCount 限制总占用空间。一个10MB×5备份的配置最多占60MB磁盘。生产环境建议配合logrotate或云日志服务(如阿里云SLS、腾讯云CLS)做长期归档。
Q: print 和 logging 到底能不能混用?
能,但不推荐。logging模块内部也用了 sys.stdout,混用时日志顺序可能错乱。如果你有历史遗留代码一时改不完,可以把print重定向到logging:import sys; class PrintLogger: write = lambda self, msg: logging.info(msg.strip()) if msg.strip() else None; sys.stdout = PrintLogger()。但生产环境建议彻底替换。
Q: 多进程环境下写同一个日志文件安全吗?
不推荐。Python的logging模块在单进程下是线程安全的(内部有锁),但多进程同时写同一个文件可能导致日志交错或丢失。解决方案:(1) 用SocketHandler将日志发送到独立日志进程;(2) 每个进程写自己的文件;(3) 使用支持多进程的QueueHandler+QueueListener模式(Python 3.2+)。生产环境通常用ELK(Elasticsearch+Logstash+Kibana)或Loki做集中式日志管理。
总结
日志是程序员排查问题的第一工具,也是生产环境监控的基础。核心要点:
- 5行代码起步:
basicConfig()+getLogger()就能替换掉print - 日志等级管理: DEBUG→INFO→WARNING→ERROR→CRITICAL,生产环境设INFO以上
- 文件自动轮转: RotatingFileHandler和TimedRotatingFileHandler防止磁盘撑爆
- 多模块架构: 用
__name__做logger名,统一在入口配置 - 生产级配置: dictConfig统一管理,Flask/Django都有自己的LOGGING配置入口
把print换成logging,你的代码离专业级又近了一步。如果你还想深入了解日志聚合系统(ELK/Loki)或者Python异常处理最佳实践,可以看看本站相关文章。