第7章 7.4 错误监控与日志
上几章我们折腾了不少「跑得更快」的事情:分包让用户只下载需要的代码、懒加载让页面按需渲染、图片压缩让流量省一半。但有一个问题你可能没想过:如果用户那边崩了呢?
你想的是「我本地测得好好的」,结果用户手机直接弹个白屏,连报错信息都没有。这种「静默失败」比报错更可怕——你根本不知道哪里出了问题。
这一章我们要解决的问题很简单:程序出错了,怎么第一时间知道?怎么知道用户操作了什么导致的?
学完这章,你就能写出一个带完整日志系统的 Python 小工具,既能在开发时帮自己调试,也能在上线后帮自己「破案」。
🎯 基础 25 分钟:核心概念(小白视角)
什么是日志?先说个生活类比
日志 = 程序的「黑匣子」。
飞机失事后,调查员会看黑匣子记录的最后几秒操作。程序崩了,日志就是你的黑匣子——它会帮你记住:「崩之前用户点了哪个按钮」「传了什么参数」「执行到哪一行代码」。
第一个问题:为什么不用 print?
新手喜欢用 print() 调试,写起来方便。但 \n\n
\n\n
\n\nprint 有几个致命缺点:
| print 的问题 | 日志的好处 |
|---|---|
| 只能输出到屏幕 | 可以同时写文件、发邮件、发钉钉 |
| 没有时间戳 | 自动记录精确时间 |
| 没有级别(调试/警告/错误混在一起) | 分轻重缓急,一眼看出严重程度 |
| 生产环境不知道该不该删 | 留代码,调级别就行 |
说白了:print 是日记本,日志是监控摄像头。
Python 的 logging 模块(内置,不用安装)
Python 自带了一个日志工具 logging,用它就像用一个「专业记录员」:
import logging
# 创建一个记录器(给记录员起个名字)
logger = logging.getLogger("我的小工具")
# 设置记录级别:DEBUG 是最详细,INFO 是普通消息,ERROR 是错误
logger.setLevel(logging.DEBUG)
# 让记录员同时输出到屏幕和文件
console_handler = logging.StreamHandler()
file_handler = logging.FileHandler("app.log", encoding="utf-8")
# 设定输出格式
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# 记录员正式上岗
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# 试试记录几条
logger.debug("这是调试信息:我进了一个函数")
logger.info("这是普通信息:用户登录成功")
logger.warning("这是警告:用户密码太简单")
logger.error("这是错误:数据库连接失败了")
运行后你会看到屏幕输出,同时同目录下多了一个 app.log 文件,里面也有同样的内容。
每行代码在干嘛:
- getLogger("我的小工具") = 给记录员分配工牌
- setLevel = 设定记录员要记录哪些级别的内容
- FileHandler = 让记录员同时写文件
- Formatter = 规定记录的格式(时间-谁-什么级别-内容)
第二个概念:try...except(程序的「安全网」)
生活类比:你把易碎品寄快递,会包好几层气泡垫。try...except 就是你代码里的气泡垫——它不能防止摔,但能让摔了之后不至于全碎。
def 读取配置文件(文件名):
try:
with open(文件名, "r", encoding="utf-8") as f:
内容 = f.read()
print(f"✅ 成功读取 {文件名}")
return 内容
except FileNotFoundError:
print(f"⚠️ 文件不存在:{文件名}")
return None
except PermissionError:
print(f"⚠️ 没权限读取:{文件名}")
return None
except Exception as e:
print(f"❌ 未知错误:{type(e).__name__} - {e}")
return None
# 测试一下
读取配置文件("config.json") # 假设文件不存在
读取配置文件("app.log") # 这个文件应该存在
执行流程:如果 open() 那行报错了,Python 会立刻跳到对应的 except,不会让整个程序崩溃。
第三个概念:traceback(错误「堆栈」)
当程序报错时,Python 会显示一串长长的错误信息,叫「traceback」。很多人一看这么长就慌了,其实看懂它很简单:
Traceback (most recent call last):
File "test.py", line 5, in <module>
result = 10 / 0
ZeroDivisionError: division by zero
从上往下读:
1. 最后一行才是「真正报错的原因」:division by zero(除以零)
2. 往上几行是「报错的位置」:test.py 第 5 行
3. 最上面是「调用链」:谁调了谁
第四个概念:自定义日志格式(让日志更好看)
默认格式有点单调,我们可以加点料:
import logging
from datetime import datetime
class ColoredFormatter(logging.Formatter):
"""带颜色的日志格式"""
COLORS = {
'DEBUG': '\033[36m', # 青色
'INFO': '\033[32m', # 绿色
'WARNING': '\033[33m', # 黄色
'ERROR': '\033[31m', # 红色
'CRITICAL': '\033[35m', # 紫色
}
RESET = '\033[0m'
def format(self, record):
color = self.COLORS.get(record.levelname, self.RESET)
record.levelname = f"{color}{record.levelname}{self.RESET}"
return super().format(record)
logger = logging.getLogger("带颜色的日志")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(ColoredFormatter(
"%(asctime)s │ %(levelname)s │ %(message)s",
datefmt="%H:%M:%S"
))
logger.addHandler(handler)
logger.debug("我进来了")
logger.info("处理中...")
logger.warning("内存有点高")
logger.error("出问题了")
现在终端里运行,你会看到不同级别的日志有不同的颜色。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):一个会「记日记」的计算器
目标:写一个计算器,每次操作都记录日志,出错了也不崩。
import logging
# 配置日志
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S"
)
logger = logging.getLogger("计算器")
def 加法(a, b):
logger.info(f"执行加法:{a} + {b}")
return a + b
def 减法(a, b):
logger.info(f"执行减法:{a} - {b}")
return a - b
def 乘法(a, b):
logger.info(f"执行乘法:{a} × {b}")
return a * b
def 除法(a, b):
logger.info(f"执行除法:{a} ÷ {b}")
if b == 0:
logger.error("除数不能为0!")
return None
return a / b
def 计算器菜单():
print("=" * 30)
print(" 小明牌计算器 v1.0")
print("=" * 30)
历史 = []
while True:
print("\n1. 加法 2. 减法 3. 乘法 4. 除法 0. 退出")
选择 = input("请选择:")
if 选择 == "0":
logger.info("用户退出程序")
print("拜拜!")
break
if 选择 not in ("1", "2", "3", "4"):
logger.warning(f"无效选择:{选择}")
print("⚠️ 选错了,重新选")
continue
try:
a = float(input("第一个数:"))
b = float(input("第二个数:"))
except ValueError:
logger.error("输入了非数字")
print("❌ 请输入数字!")
continue
if 选择 == "1":
结果 = 加法(a, b)
elif 选择 == "2":
结果 = 减法(a, b)
elif 选择 == "3":
结果 = 乘法(a, b)
else:
结果 = 除法(a, b)
if 结果 is not None:
历史.append(f"{a} {'+-×÷'[int(选择)-1]} {b} = {结果}")
print(f"✅ 结果:{结果}")
print("\n📝 计算历史:")
for i, 记录 in enumerate(历史, 1):
print(f" {i}. {记录}")
if __name__ == "__main__":
计算器菜单()
预期输出(部分):
2025-07-15 14:23:01 [INFO] 执行加法:10.0 + 3.0
✅ 结果:13.0
2025-07-15 14:23:05 [INFO] 执行除法:10.0 ÷ 0.0
2025-07-15 14:23:05 [ERROR] 除数不能为0!
一句话解释:程序每次操作都自动记日志,除以0这种错误被捕获了,不会闪退。
项目 2(15 分钟):日志分析小工具
目标:读取一个网站访问日志(CSV 格式),找出所有错误请求,统计每个错误出现了多少次。
先创建一个模拟的日志文件 access_log.csv:
时间,状态码,URL,用户IP
2025-07-15 10:00:01,200,/api/home,192.168.1.100
2025-07-15 10:00:02,404,/api/notexist,192.168.1.101
2025-07-15 10:00:03,500,/api/order,192.168.1.102
2025-07-15 10:00:04,200,/api/home,192.168.1.100
2025-07-15 10:00:05,403,/api/admin,192.168.1.103
2025-07-15 10:00:06,500,/api/order,192.168.1.104
2025-07-15 10:00:07,404,/api/product/123,192.168.1.105
2025-07-15 10:00:08,200,/api/home,192.168.1.100
然后写分析工具:
import csv
from collections import Counter
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("日志分析")
def 读取日志文件(文件名):
"""读取 CSV 格式的日志文件"""
记录列表 = []
try:
with open(文件名, "r", encoding="utf-8") as f:
读取器 = csv.DictReader(f)
for 行 in 读取器:
记录列表.append(行)
logger.info(f"成功读取 {len(记录列表)} 条记录")
return 记录列表
except FileNotFoundError:
logger.error(f"文件不存在:{文件名}")
return []
except Exception as e:
logger.error(f"读取失败:{e}")
return []
def 统计错误(记录列表):
"""统计各种错误"""
错误记录 = []
所有状态码 = []
状态码含义 = {
"400": "请求语法错误",
"401": "未授权",
"403": "禁止访问",
"404": "页面不存在",
"500": "服务器内部错误",
"502": "网关错误",
"503": "服务不可用"
}
for 记录 in 记录列表:
状态码 = 记录["状态码"]
所有状态码.append(状态码)
# 只记录 4xx 和 5xx 错误
if 状态码.startswith(("4", "5")):
错误记录.append({
"时间": 记录["时间"],
"状态码": 状态码,
"含义": 状态码含义.get(状态码, "未知错误"),
"URL": 记录["URL"],
"IP": 记录["用户IP"]
})
logger.warning(f"发现错误:{记录['时间']} {状态码} {记录['URL']}")
return 错误记录, 所有状态码
def 打印报告(错误记录, 所有状态码):
"""打印分析报告"""
print("\n" + "=" * 50)
print(" 📊 日志分析报告")
print("=" * 50)
# 总体统计
print(f"\n📈 总体概览")
print(f" 总请求数:{len(所有状态码)}")
状态统计 = Counter(所有状态码)
print(f" 成功(2xx):{sum(v for k,v in status_count.items() if k.startswith('2'))}")
print(f" 错误(4xx+5xx):{sum(v for k,v in status_count.items() if k.startswith(('4','5')))}")
# 错误详情
if 错误记录:
print(f"\n❌ 错误详情(共 {len(错误记录)} 条)")
print("-" * 50)
错误类型统计 = Counter(r["状态码"] for r in 错误记录)
print("\n🔢 错误类型统计:")
for 码, 次数 in 错误类型统计.most_common():
含义 = 错误记录[0]["含义"] if 错误记录 else ""
print(f" {码} ({含义}):{次数}次")
print("\n📋 最近 5 条错误:")
for 记录 in 错误记录[-5:]:
print(f" [{记录['时间']}] {记录['状态码']} {记录['URL']} ← {记录['IP']}")
else:
print("\n✅ 没有发现错误,干得漂亮!")
print("\n" + "=" * 50)
if __name__ == "__main__":
记录列表 = 读取日志文件("access_log.csv")
if 记录列表:
错误记录, 所有状态码 = 统计错误(记录列表)
打印报告(错误记录, 所有状态码)
预期输出:
2025-07-15 14:30:01 - INFO - 成功读取 8 条记录
2025-07-15 14:30:01 - WARNING - 发现错误:2025-07-15 10:00:02 404 /api/notexist
2025-07-15 14:30:01 - WARNING - 发现错误:2025-07-15 10:00:03 500 /api/order
...
==================================================
📊 日志分析报告
==================================================
📈 总体概览
请求数:8
功(2xx):3
误(4xx+5xx):5
❌ 错误详情(共 5 条)
🔢 错误类型统计:
04:2次
00:2次
03:1次
📋 最近 5 条错误:
2025-07-15 10:00:02] 404 /api/notexist ← 192.168.1.101
2025-07-15 10:00:03] 500 /api/order ← 192.168.1.102
2025-07-15 10:00:05] 403 /api/admin ← 192.168.1.103
..
一句话解释:读取 CSV → 挑出所有非 2xx 的请求 → 按类型分组统计 → 打印好看报告。
项目 3(15 分钟):带日志的「待办清单」持久化版
目标:做一个命令行待办清单,数据保存在文件里,每次操作都记录日志,支持增删查,还能自动记录「什么时候谁干了什么」。
import json
import logging
from datetime import datetime
# 配置日志:同时写文件和控制台
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
handlers=[
logging.FileHandler("todo.log", encoding="utf-8"),
logging.StreamHandler()
]
)
logger = logging.getLogger("待办清单")
数据文件 = "todos.json"
def 加载数据():
"""从文件加载待办清单"""
try:
with open(数据文件, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
logger.info("数据文件不存在,创建新的")
return []
except json.JSONDecodeError:
logger.error("数据文件损坏,开始新清单")
return []
def 保存数据(待办列表):
"""保存待办清单到文件"""
try:
with open(数据文件, "w", encoding="utf-8") as f:
json.dump(待办列表, f, ensure_ascii=False, indent=2)
logger.debug(f"已保存 {len(待办列表)} 条待办")
except Exception as e:
logger.error(f"保存失败:{e}")
def 显示菜单():
print("\n📝 我的待办清单")
print("-" * 30)
print("1. 查看所有待办")
print("2. 添加待办")
print("3. 标记完成")
print("4. 删除待办")
print("5. 搜索待办")
print("0. 退出")
print("-" * 30)
def 查看待办(待办列表):
if not 待办列表:
print("📭 清单是空的,快添加一个吧!")
return
print(f"\n📋 共 {len(待办列表)} 项待办:")
for i, 项 in enumerate(待办列表, 1):
状态 = "✅" if 项["完成"] else "⬜"
优先级标记 = {
"高": "🔴",
"中": "🟡",
"低": "🟢"
}.get(项.get("优先级", "中"), "🟢")
print(f" {i}. {状态} {项['内容']} {优先级标记}")
if 项.get("截止日期"):
print(f" 📅 截止:{项['截止日期']}")
def 添加待办(待办列表):
内容 = input("📨 输入待办内容:").strip()
if not 内容:
print("⚠️ 内容不能为空")
return 待办列表
优先级 = input("🔖 优先级(高/中/低,默认中):").strip() or "中"
截止日期 = input("📅 截止日期(可选,格式 2025-12-31):").strip()
新待办 = {
"内容": 内容,
"完成": False,
"创建时间": datetime.now().strftime("%Y-%m-%d %H:%M"),
"优先级": 优先级,
"截止日期": 截止日期 or None
}
待办列表.append(新待办)
logger.info(f"添加待办:{内容}(优先级{优先级})")
print(f"✅ 已添加「{内容}」")
return 待办列表
def 标记完成(待办列表):
查看待办(待办列表)
if not 待办列表:
return 待办列表
try:
序号 = int(input("\n请输入要标记完成的序号:"))
if 1 <= 序号 <= len(待办列表):
待办列表[序号-1]["完成"] = True
待办列表[序号-1]["完成时间"] = datetime.now().strftime("%Y-%m-%d %H:%M")
logger.info(f"标记完成:{待办列表[序号-1]['内容']}")
print(f"✅ 「{待办列表[序号-1]['内容']}」已完成!")
else:
print("⚠️ 序号超出范围")
except ValueError:
print("⚠️ 请输入数字")
return 待办列表
def 删除待办(待办列表):
查看待办(待办列表)
if not 待办列表:
return 待办列表
try:
序号 = int(input("\n请输入要删除的序号:"))
if 1 <= 序号 <= len(待办列表):
被删除 = 待办列表.pop(序号-1)
logger.warning(f"删除待办:{被删除['内容']}")
print(f"🗑️ 已删除「{被删除['内容']}」")
else:
print("⚠️ 序号超出范围")
except ValueError:
print("⚠️ 请输入数字")
return 待办列表
def 搜索待办(待办列表):
关键词 = input("🔍 输入搜索关键词:").strip()
if not 关键词:
print("⚠️ 关键词不能为空")
return
结果 = [项 for 项 in 待办列表 if 关键词 in 项["内容"]]
logger.info(f"搜索「{关键词}」,找到 {len(结果)} 条")
if 结果:
print(f"\n🔎 找到 {len(结果)} 条相关待办:")
for i, 项 in enumerate(结果, 1):
状态 = "✅" if 项["完成"] else "⬜"
print(f" {i}. {状态} {项['内容']}")
else:
print("😕 没找到匹配的待办")
def main():
logger.info("=" * 20)
logger.info("待办清单程序启动")
print("👋 欢迎使用待办清单!(输入 q 随时退出)")
待办列表 = 加载数据()
logger.info(f"加载了 {len(待办列表)} 条待办")
while True:
显示菜单()
选择 = input("请选择:").strip()
if 选择 == "0" or 选择.lower() == "q":
logger.info("用户退出程序")
print("👋 下次见!")
break
if 选择 == "1":
查看待办(待办列表)
elif 选择 == "2":
待办列表 = 添加待办(待办列表)
保存数据(待办列表)
elif 选择 == "3":
待办列表 = 标记完成(待办列表)
保存数据(待办列表)
elif 选择 == "4":
待办列表 = 删除待办(待办列表)
保存数据(待办列表)
elif 选择 == "5":
搜索待办(待办列表)
else:
logger.warning(f"无效选择:{选择}")
print("⚠️ 无效选择,请重新输入")
if __name__ == "__main__":
main()
预期输出:
👋 欢迎使用待办清单!(输入 q 随时退出)
📝 我的待办清单
------------------------------
1. 查看所有待办
...
请选择:2
📨 输入待办内容:买牛奶
🔖 优先级(高/中/低,默认中):
📅 截止日期(可选,格式 2025-12-31):
✅ 已添加「买牛奶」
请选择:1
📋 共 1 项待办:
1. ⬜ 买牛奶 🟢
同时 todo.log 文件里会记录:
2025-07-15 14:00:01 [INFO] ====================
2025-07-15 14:00:01 [INFO] 待办清单程序启动
2025-07-15 14:00:01 [INFO] 加载了 0 条待办
2025-07-15 14:00:03 [INFO] 添加待办:买牛奶(优先级中)
2025-07-15 14:00:03 [DEBUG] 已保存 1 条待办
一句话解释:数据存 JSON 文件,每次增删改都自动记日志,随时能查到「什么时候干了什么」。
💪 进阶 20 分钟:常见坑 + 调试技巧
坑 1:日志重复输出
❌ 错误:
import logging
logger = logging.getLogger("我的")
logger.setLevel(logging.DEBUG)
logger.debug("一条消息") # 输出两次?因为 root logger 也会输出
✅ 正确:给 logger 设置 propagate=False,防止消息向上传播:
logger = logging.getLogger("我的")
logger.setLevel(logging.DEBUG)
logger.propagate = False # 阻止向上传给 root logger
handler = logging.StreamHandler()
logger.addHandler(handler)
logger.debug("只会输出一次")
坑 2:except 抓了但啥也不干
❌ 错误:
try:
result = int(user_input)
except ValueError:
pass # 静默失败,完全不知道出错了
✅ 正确:至少打个日志:
try:
result = int(user_input)
except ValueError as e:
logger.error(f"转换失败:{user_input},错误:{e}")
坑 3:文件没关闭
❌ 错误:
f = open("test.txt", "w")
f.write("hello")
# 如果这行报错了,文件没关
✅ 正确:用 with 自动关:
with open("test.txt", "w") as f:
f.write("hello")
# 缩进结束自动关闭
坑 4:traceback 不会看
❌ 错误:
Traceback (most recent call last):
File "test.py", line 8, in <module>
result = 计算()
File "test.py", line 4, in 计算
return 10 / 0
ZeroDivisionError: division by zero
新手的反应:「好长,算了不管了」。正确做法:盯着最后一行 ZeroDivisionError 看原因,往上找自己的代码(看文件名和行号)。
坑 5:生产环境开 DEBUG 级别
❌ 错误:
logging.basicConfig(level=logging.DEBUG) # 生产环境太详细
✅ 正确:根据环境切换级别:
import os
环境 = os.getenv("APP_ENV", "development")
if 环境 == "production":
logging.basicConfig(level=logging.WARNING) # 只记警告和错误
else:
logging.basicConfig(level=logging.DEBUG) # 开发环境记所有
调试技巧:pdb 打断点
除了 print,你可以用 Python 内置调试器 pdb:
import pdb
def 有bug的函数():
a = 10
b = 0
pdb.set_trace() # 程序会停在这里,进入交互式调试
result = a / b
return result
有bug的函数()
运行后你会看到 pdb> 提示符,可以输入命令:
- p 变量名 — 查看变量值
- n — 执行下一行
- s — 进入函数内部
- c — 继续执行直到断点
- q — 退出调试
✏️ 练习题
练习 1(2 分钟):改个日志级别
把项目 1 里「除数不能为0」的日志从 logger.error 改成 logger.warning,运行看看输出有什么区别。
输入:在除法函数里输入 10 和 0
预期输出:日志级别从 ERROR 变成 WARNING
练习 2(2 分钟):加一个判断
在项目 2 的日志分析工具里,加一个判断:如果「500 错误」超过 3 次,额外打印「⚠️ 警告:服务器可能有严重问题!」
提示:用 Counter 统计后判断 错误类型统计.get("500", 0) > 3
练习 3(3 分钟):处理新数据
把 access_log.csv 复制一份,改成包含更多 404 错误的日志,运行项目 2 的分析工具,看看统计对不对。
输入:一个新的 CSV 文件
预期输出:能正确统计新文件里的错误
练习 4(5 分钟):串起来
把项目 2(日志分析)和项目 3(待办清单)结合起来:当日志分析发现 500 错误时,自动往待办清单添加一条「检查 /api/xxx 接口」的待办。
提示:调用待办清单的添加函数
练习 5(3 分钟):看 traceback 找 bug
下面这段代码运行后会报错,仔细读 traceback,告诉我:
1. 错误类型是什么?
2. 错误发生在哪一行?
3. 怎么修复?
def 读取用户(文件名):
with open(文件名, "r") as f:
数据 = json.load(f)
return 数据["name"]
名字 = 读取用户("user.json")
print(f"用户名:{名字}")
假设 user.json 内容是 {"username": "xiaoming"}(注意是 username 不是 name)
作业:做一个「错误日志监控系统」
需求描述:写一个 Python 脚本,监控一个目录下的所有 .log 文件,统计每个文件的:
1. 总行数
2. ERROR 出现次数
3. WARNING 出现次数
4. 最近 5 条 ERROR 日志
功能点:
1. 读取目录下所有 .log 文件
2. 正则表达式提取日志级别
3. 生成汇总报告
加分项:
1. 发现 ERROR 超过 10 次时,打印红色警告
2. 把报告保存成 HTML 文件(带颜色)
验收标准:
- 能运行 ./log_monitor.py ./logs 这样的命令
- 输出清晰的汇总报告
- 代码有注释
📚 总结
本文学了 3 件事:
1. 日志系统:用 logging 模块给程序装上「监控摄像头」
2. 异常处理:try...except 是代码的安全网,让程序不崩溃
3. 持久化:把数据存文件,程序重启也不会丢
延伸资源:
- 官方文档(英):https://docs.python.org/3/library/logging.html
- 官方文档(中):https://docs.python.org/zh-cn/3/library/logging.html
- 《Python Crash Course》第 9 章:文件与异常
互动钩子:
你的项目上线后出过一次「静默崩溃」吗?当时怎么发现的?评论区聊聊,老粉优先回复!
下章预告:
学会了错误监控和日志,下一章我们要做一个「电商完整版」实战——把你这 7 章学的所有东西串起来,做一个能跑的多端电商 App。敬请期待!

评论(0)