应提取「异常指纹」去重:用traceback.format_exception标准化堆栈生成规范字符串,抹去路径/行号等动态字段后哈希,或对哈希冲突组辅以模糊匹配。

直接剔除重复堆栈不现实——堆栈里有时间戳、线程ID、内存地址等必然变化的字段,硬比对会漏删或误删。真正可行的是提取「异常指纹」,再按指纹去重。
用 traceback.format_exception 标准化堆栈再哈希
Python 原生的 traceback.format_exception 能把任意异常对象转成规范字符串(不含动态值),是生成指纹的基础。关键不是原始日志行,而是还原出异常类型 + 消息 + 关键帧(如最末尾的 File 行和错误行)。
- 别用正则直接匹配日志里的堆栈块——格式不统一(log4j/structlog/stderr 混用时尤其麻烦)
- 先用
re.split(r'(?=Traceback \(most recent call last\):)', log_text)切分出疑似异常块,再逐块尝试ast.literal_eval或exec还原异常对象(仅限可信日志) - 更稳妥的做法:用
logging.handlers.RotatingFileHandler配合自定义Formatter,在写入时就记录指纹,后续清理成本归零
处理多行堆栈时避免被中间日志行打断
真实日志常夹杂 INFO/WARN 行,比如:
ERROR:root:Exception occurred
Traceback (most recent call last):
File "app.py", line 42, in run
raise ValueError("timeout")
ValueError: timeout
INFO:root:Retrying...
ERROR:root:Exception occurred
Traceback (most recent call last):
这时不能简单按空行切分。必须用状态机识别堆栈起始与结束:
- 遇到
"Traceback (most recent call last):"开始收集 - 持续收集直到遇到空行、或下一个非缩进行(如
INFO:、WARNING:)、或匹配到^[a-zA-Z]+Error: - 用
re.sub(r'File ".*?", line \d+,', 'File "<file>", line <line>,')</line></file>抹掉路径和行号细节(保留结构即可)
hashlib.sha256 指纹 vs difflib.SequenceMatcher 模糊匹配
严格哈希适合完全一致的堆栈变体(如仅时间戳/线程名不同);但若堆栈中变量值不同(KeyError: 'user_123' vs KeyError: 'user_456'),需模糊匹配。
- 哈希方案快且确定:
hashlib.sha256(normalized_stack.encode()).hexdigest()[:8] - 模糊方案慎用:
difflib.SequenceMatcher(a=stack1_lines, b=stack2_lines).ratio() > 0.9,只适用于小批量(>1000 条堆栈时性能断崖下跌) - 生产环境建议折中:先哈希粗筛,对哈希冲突的组内再跑模糊匹配(实际冲突率极低)
真正难的不是算法,是日志源头是否可控。如果日志来自第三方服务(如 Java 应用输出的混合日志),连堆栈起始标记都可能被截断,这时候得先做日志协议解析,而不是急着写去重脚本。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











