Python中如何捕获并记录详细错误信息

0 阅读6分钟

在Python开发中,优雅地处理异常和记录详细的错误信息是编写健壮程序的关键。无论是开发Web应用、数据分析脚本还是自动化工具,当程序在用户环境中运行时,完善的错误处理机制能帮助我们快速定位和解决问题。本文将深入探讨Python中异常捕获的高级技巧和日志记录的最佳实践。

一、Python异常处理基础

1.1 基本的try-except语句

try:
    # 可能引发异常的代码
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"发生了除零错误: {e}")

1.2 捕获多种异常

try:
    # 可能引发多种异常的代码
    value = int("abc")
    result = 10 / value
except (ValueError, ZeroDivisionError) as e:
    print(f"捕获到异常: {type(e).__name__}: {e}")

1.3 完整的异常处理结构

try:
    # 主要逻辑
    data = {"key": "value"}
    print(data["nonexistent_key"])
except KeyError as e:
    print(f"键错误: {e}")
except Exception as e:
    print(f"其他异常: {e}")
else:
    print("代码执行成功,无异常发生")
finally:
    print("无论是否发生异常,都会执行此代码块")

二、获取详细的错误信息

2.1 使用traceback模块

traceback模块提供了获取完整调用栈信息的功能:

import traceback
import sys

def risky_operation():
    return 1 / 0

try:
    risky_operation()
except Exception as e:
    # 获取完整的异常信息
    exc_type, exc_value, exc_traceback = sys.exc_info()
    
    # 格式1: 基本traceback信息
    print("基本Traceback:")
    traceback.print_exc()
    
    # 格式2: 转换为字符串
    error_str = traceback.format_exc()
    print("\n格式化的错误信息:")
    print(error_str)
    
    # 格式3: 获取调用栈的帧信息
    print("\n详细调用栈:")
    for frame, lineno in traceback.walk_tb(exc_traceback):
        print(f"文件: {frame.f_code.co_filename}")
        print(f"行号: {lino}")
        print(f"函数: {frame.f_code.co_name}")

2.2 自定义错误信息收集函数

import sys
import traceback
from datetime import datetime

def get_detailed_error_info():
    """获取详细的错误信息"""
    exc_type, exc_value, exc_traceback = sys.exc_info()
    
    error_details = {
        "timestamp": datetime.now().isoformat(),
        "exception_type": exc_type.__name__ if exc_type else None,
        "exception_message": str(exc_value) if exc_value else None,
        "traceback": traceback.format_exc(),
        "local_variables": {}
    }
    
    # 获取局部变量(最后一个帧)
    if exc_traceback:
        tb_frame = exc_traceback.tb_frame
        while tb_frame:
            error_details["local_variables"].update(tb_frame.f_locals)
            tb_frame = tb_frame.f_back
    
    return error_details

# 使用示例
try:
    x = 10
    y = 0
    result = x / y
except Exception:
    error_info = get_detailed_error_info()
    print("错误详情:", error_info)

三、日志记录最佳实践

3.1 配置日志系统

import logging
import logging.handlers
from pathlib import Path

def setup_logging(log_file="app_error.log"):
    """设置日志配置"""
    
    # 创建日志目录
    log_path = Path(log_file)
    log_path.parent.mkdir(parents=True, exist_ok=True)
    
    # 创建logger
    logger = logging.getLogger("AppLogger")
    logger.setLevel(logging.DEBUG)
    
    # 防止重复添加handler
    if not logger.handlers:
        # 文件handler - 记录所有级别
        file_handler = logging.handlers.RotatingFileHandler(
            log_file,
            maxBytes=10 * 1024 * 1024,  # 10MB
            backupCount=5,
            encoding='utf-8'
        )
        file_handler.setLevel(logging.DEBUG)
        
        # 控制台handler - 只记录INFO及以上
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.INFO)
        
        # 设置格式
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - '
            '[%(filename)s:%(lineno)d] - %(message)s'
        )
        file_handler.setFormatter(formatter)
        console_handler.setFormatter(formatter)
        
        # 添加handler
        logger.addHandler(file_handler)
        logger.addHandler(console_handler)
    
    return logger

3.2 带错误上下文的日志装饰器

import functools
import traceback
from typing import Callable, Any

def log_errors(logger: logging.Logger):
    """记录函数错误的装饰器"""
    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            try:
                return func(*args, **kwargs)
            except Exception as e:
                # 记录详细错误信息
                error_details = {
                    "function": func.__name__,
                    "module": func.__module__,
                    "args": args,
                    "kwargs": kwargs,
                    "traceback": traceback.format_exc(),
                    "error": str(e)
                }
                
                logger.error(f"函数 {func.__name__} 执行失败: {error_details}")
                raise  # 重新抛出异常
        return wrapper
    return decorator

# 使用示例
logger = setup_logging()

@log_errors(logger)
def process_data(data: dict):
    """处理数据的函数"""
    return data["nonexistent_key"]

四、高级错误处理模式

4.1 上下文管理器处理异常

from contextlib import contextmanager
import sys
import traceback

@contextmanager
def error_handler(context: str = ""):
    """错误处理的上下文管理器"""
    try:
        yield
    except Exception as e:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        
        error_log = {
            "context": context,
            "error_type": exc_type.__name__,
            "error_message": str(exc_value),
            "traceback": traceback.format_tb(exc_traceback),
            "timestamp": datetime.now().isoformat()
        }
        
        # 这里可以添加错误上报逻辑
        print(f"捕获到错误 ({context}): {error_log}")
        
        # 可以选择是否重新抛出异常
        # raise

# 使用示例
with error_handler("数据处理"):
    result = 10 / 0

4.2 自定义异常类

class AppBaseError(Exception):
    """应用基础异常类"""
    
    def __init__(self, message: str, error_code: int = 500, **kwargs):
        self.message = message
        self.error_code = error_code
        self.context = kwargs
        super().__init__(self.message)
    
    def to_dict(self) -> dict:
        """转换为字典格式"""
        return {
            "error": self.__class__.__name__,
            "message": self.message,
            "code": self.error_code,
            "context": self.context,
            "traceback": traceback.format_exc()
        }


class ValidationError(AppBaseError):
    """数据验证异常"""
    
    def __init__(self, field: str, value: Any, rule: str):
        message = f"字段 '{field}' 的值 '{value}' 不符合规则: {rule}"
        super().__init__(message, error_code=400, field=field, value=value, rule=rule)


# 使用示例
def validate_age(age: int):
    if age < 0 or age > 150:
        raise ValidationError(field="age", value=age, rule="0 <= age <= 150")
    return True

五、实战:完整的错误处理模块

# error_handler.py
import logging
import sys
import traceback
from datetime import datetime
from typing import Optional, Dict, Any
from pathlib import Path
import json

class ErrorHandler:
    """完整的错误处理器"""
    
    def __init__(self, 
                 app_name: str = "MyApp",
                 log_dir: str = "logs",
                 enable_file_log: bool = True,
                 enable_console_log: bool = True):
        
        self.app_name = app_name
        self.log_dir = Path(log_dir)
        self.log_dir.mkdir(exist_ok=True)
        
        # 初始化日志
        self.logger = self._setup_logger(
            enable_file_log=enable_file_log,
            enable_console_log=enable_console_log
        )
        
        # 错误统计
        self.error_stats = {
            "total_errors": 0,
            "error_by_type": {},
            "last_error_time": None
        }
    
    def _setup_logger(self, enable_file_log: bool, enable_console_log: bool) -> logging.Logger:
        """设置logger"""
        logger = logging.getLogger(self.app_name)
        logger.setLevel(logging.DEBUG)
        
        # 避免重复添加handler
        if logger.handlers:
            return logger
        
        formatter = logging.Formatter(
            '%(asctime)s - %(levelname)s - '
            '[%(filename)s:%(lineno)d] - %(message)s'
        )
        
        if enable_file_log:
            # 错误日志
            error_file = self.log_dir / f"{self.app_name}_error.log"
            error_handler = logging.handlers.RotatingFileHandler(
                error_file, maxBytes=10 * 1024 * 1024, backupCount=5, encoding='utf-8'
            )
            error_handler.setLevel(logging.ERROR)
            error_handler.setFormatter(formatter)
            logger.addHandler(error_handler)
            
            # 调试日志
            debug_file = self.log_dir / f"{self.app_name}_debug.log"
            debug_handler = logging.handlers.RotatingFileHandler(
                debug_file, maxBytes=10 * 1024 * 1024, backupCount=5, encoding='utf-8'
            )
            debug_handler.setLevel(logging.DEBUG)
            debug_handler.setFormatter(formatter)
            logger.addHandler(debug_handler)
        
        if enable_console_log:
            console_handler = logging.StreamHandler()
            console_handler.setLevel(logging.INFO)
            console_handler.setFormatter(formatter)
            logger.addHandler(console_handler)
        
        return logger
    
    def capture_exception(self, 
                         exception: Exception, 
                         context: Optional[Dict[str, Any]] = None,
                         level: str = "ERROR") -> Dict[str, Any]:
        """捕获并记录异常"""
        
        exc_type, exc_value, exc_traceback = sys.exc_info()
        
        error_info = {
            "timestamp": datetime.now().isoformat(),
            "app_name": self.app_name,
            "exception": {
                "type": exc_type.__name__ if exc_type else None,
                "message": str(exc_value),
                "module": exc_type.__module__ if exc_type else None
            },
            "traceback": traceback.format_exc(),
            "context": context or {},
            "system_info": {
                "python_version": sys.version,
                "platform": sys.platform
            }
        }
        
        # 更新统计
        self.error_stats["total_errors"] += 1
        error_type = error_info["exception"]["type"]
        self.error_stats["error_by_type"][error_type] = \
            self.error_stats["error_by_type"].get(error_type, 0) + 1
        self.error_stats["last_error_time"] = error_info["timestamp"]
        
        # 记录日志
        log_message = json.dumps(error_info, ensure_ascii=False, indent=2)
        
        if level == "ERROR":
            self.logger.error(log_message)
        elif level == "WARNING":
            self.logger.warning(log_message)
        else:
            self.logger.info(log_message)
        
        # 保存到单独的错误文件
        error_file = self.log_dir / f"error_{datetime.now().strftime('%Y%m%d')}.json"
        with open(error_file, 'a', encoding='utf-8') as f:
            f.write(log_message + "\n")
        
        return error_info
    
    def get_error_stats(self) -> Dict[str, Any]:
        """获取错误统计"""
        return self.error_stats.copy()


# 使用示例
if __name__ == "__main__":
    # 初始化错误处理器
    handler = ErrorHandler(app_name="DataProcessor")
    
    try:
        # 模拟业务代码
        data = {"items": [1, 2, 3]}
        result = data["nonexistent_key"]
        
    except Exception as e:
        # 捕获并记录错误
        error_info = handler.capture_exception(
            e,
            context={
                "operation": "data_processing",
                "data_structure": str(data.keys()),
                "user_id": 12345
            }
        )
        
        print("错误信息已记录:")
        print(json.dumps(error_info, indent=2, ensure_ascii=False))
        
        # 获取统计信息
        stats = handler.get_error_stats()
        print(f"\n错误统计: {stats}")

六、生产环境建议

6.1 错误监控和报警

  • 集成Sentry、Rollbar等错误监控服务
  • 设置关键错误的邮件/短信通知
  • 插入广告:各行各业学习千款源码就上:svipm.com.cn
  • 实现错误率监控和报警阈值

6.2 性能考虑

  • 异步记录错误日志,避免阻塞主流程
  • 合理设置日志级别,生产环境避免DEBUG级别
  • 定期清理和归档旧日志

6.3 安全考虑

  • 记录错误时注意敏感信息过滤
  • 避免在错误信息中暴露系统路径、密钥等
  • 设置适当的日志文件权限

结语

完善的错误处理机制是Python应用稳定运行的基石。通过本文介绍的技术,你可以:

  1. 捕获并记录详细的错误信息
  2. 使用专业的日志系统管理错误
  3. 实现可重用的错误处理组件
  4. 为生产环境做好准备

记住,好的错误处理不是避免错误,而是在错误发生时能够快速定位、记录和恢复,为用户提供更好的体验。