异常处理与日志记录

在AI产品开发和运营过程中,Python脚本可以帮助我们实现很多自动化流程。然而,脚本在执行过程中难免会遇到各种异常情况,如网络中断、数据缺失、程序错误等。如果不对这些异常进行妥善处理,轻则导致任务中断,重则造成数据丢失或系统崩溃。

Python的异常处理机制(try-except)允许我们在代码中预设“安全屋”,当异常发生时,程序不会立即崩溃,而是会跳转到相应的“安全屋”中执行预设的处理逻辑。这样可以保证程序在遇到错误时仍能继续运行,或者至少能够优雅地退出,并给出错误提示。

另一方面,日志记录(logging)则像是一个“黑匣子”,它详细记录了脚本的执行过程,包括正常的任务状态、发生的错误以及异常出现时的上下文信息。通过分析日志,我们可以快速定位问题原因,及时修复错误,并优化程序性能。

对于AI产品经理或产品经理来说,掌握这些技术可以提高我们编写脚本的健壮性和可靠性,更重要的是,它可以帮助我们更好地理解和监控AI产品的运行状态。通过阅读日志,我们可以及时发现潜在问题,并与开发团队协作解决,从而保证产品的稳定性和用户体验。此外,良好的日志记录也有助于产品经理更好地了解用户行为,从而为产品优化提供数据支持。

异常处理概述

在Python中,当程序运行中出现错误时,系统会产生异常(Exception)。如果这些异常没有被捕获和处理,程序就会中断。为避免这种情况,我们可以使用 `try-except` 语句来捕获和处理异常,从而保证程序在出错时仍能继续运行或做出适当响应。

下面是一个示例代码:

try:
    # 尝试执行可能出错的代码
    risky_operation()
except SomeException as e:
    # 当捕获到指定的异常时,执行这里的代码
    print(f"发生错误: {e}")

上述代码中包含了 try 和 except ,这是一个配对的块。它们是异常处理的核心部分

  • try块:放置可能会产生异常的代码。
  • except块:用来捕获并处理异常;可以指定异常类型以处理特定错误。

另外,根据开发需要,你还可以使用:

  • else/finally(可选):else 在没有异常时执行;finally 不管是否发生异常都会执行,用于资源清理。

这是 else 块的一个示例代码:

try:
    result = 10 / 2
except ZeroDivisionError:
    print("捕获到除以零的错误")
else:
    print("没有异常发生,结果是:", result)
下面是 finally 块的一个示例代码:
try:
    file = open("example.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("文件未找到")
finally:
    file.close()
    print("文件已关闭")

接下来我们看一下什么是日志记录:

日志记录概述

当程序运行时,仅仅在控制台上输出错误信息可能不够,我们还需要将错误和重要事件记录下来,以便日后分析和排查问题。这时,Python内置的 logging 模块就能派上用场。

logging模块的基本用法

  1. 配置日志:设置日志的输出级别、格式和存储位置。
  2. 记录日志:通过不同级别的日志记录(DEBUG、INFO、WARNING、ERROR、CRITICAL)输出信息。

下面是一个简单示例,展示了如何在代码中捕获异常并记录日志:

import logging

# 配置日志记录,日志级别设为DEBUG,输出到控制台
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def divide(a, b):
    try:
        result = a / b
        logging.info(f"成功计算 {a} / {b} = {result}")
        return result
    except ZeroDivisionError as e:
        # 捕获除零异常并记录错误
        logging.error("除零错误: 不能除以0", exc_info=True)
        return None

# 测试函数
print(divide(10, 2))
print(divide(10, 0))

代码说明:

  • logging.basicConfig():用于配置日志记录参数。
    • level=logging.DEBUG 表示记录DEBUG级别及以上的信息。
    • format 定义了日志输出格式,包括时间、级别和消息内容。
  • try-except:在 divide 函数中,尝试执行除法运算,如果除数为0则捕获 ZeroDivisionError 并记录错误日志。
  • exc_info=True:在记录错误时输出异常的详细信息,便于调试。

下面我们来看一个把异常处理与日志记录改进结合起来的自动化脚本案例:

在实际业务中,自动化任务常常需要处理大量文件操作、网络请求等。一个健壮的脚本不仅要能捕获错误,还要记录执行状态,方便后期维护。例如,在自动归档日报文件或清理临时文件的脚本中,如果发生错误(如文件不存在、权限不足),我们可以使用异常处理捕获错误,并通过日志记录详细错误信息。

这里是一个改进归档脚本的示例,当文件移动失败时,记录错误日志便于事后排查。

import os      # 用于文件和目录操作
import shutil  # 用于文件的复制、移动等操作
import re      # 用于正则表达式匹配日期格式

# 当前目录路径(你也可以修改为其他需要扫描的目录)
current_dir = '.'

# 定义归档根目录
archive_root = 'archive'

# 使用正则表达式匹配文件名中的日期格式:YYYY-MM-DD
date_pattern = re.compile(r'(\d{4}-\d{2}-\d{2})_report\.txt$')

# 遍历当前目录下的所有文件
for filename in os.listdir(current_dir):
    # 判断文件名是否以"_report.txt"结尾
    if filename.endswith('_report.txt'):
        # 尝试从文件名中提取日期信息
        match = date_pattern.search(filename)
        if match:
            # 获取提取到的日期字符串
            date_str = match.group(1)
            # 构建目标归档目录路径:archive/YYYY-MM-DD
            target_dir = os.path.join(archive_root, date_str)
            # 如果目标目录不存在,则创建它
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
                print(f"创建目录: {target_dir}")
            # 构建源文件和目标文件的完整路径
            source_file = os.path.join(current_dir, filename)
            target_file = os.path.join(target_dir, filename)
            try:
                # 将文件移动到目标归档目录
                shutil.move(source_file, target_file)
                print(f"已移动文件 {filename} 到 {target_dir}")
            except Exception as e:
                print(f"移动文件 {filename} 时出错: {e}")
        else:
            # 如果文件名中不包含日期信息,则跳过或记录日志
            print(f"文件 {filename} 不符合归档格式,已跳过。")

提示词示例:

提示词:

我需要一个Python脚本,它能自动把每天的日报文件从当前文件夹移动到以日期命名的子文件夹(例如“archive/2024-07-26”)。如果目标文件夹不存在,请自动创建。同时,如果移动过程中遇到任何问题(比如文件不存在、目标文件夹不存在、或者其他任何原因导致移动失败),脚本需要把详细的错误信息(包括日志时间、日志级别和日志消息)记录下来,这样我就可以随时查看并了解哪里出了问题,而不需要去查看代码。另外,每天的归档情况也需要记录,例如成功移动了哪些文件,哪些文件因为什么原因没有移动。

在上面的改进归档脚本示例中,我们已经使用 `try-except` 捕获异常,并在文件移动失败时输出错误信息。现在,我们希望在日志中增加一项新信息:记录每个被移动文件的大小。这样不仅可以知道哪些文件成功归档,还可以掌握文件的大小,便于后期统计和排查问题。

同样,我们也可以用提示词让AI大模型对脚本进行修改。这是一个修改代码的提示词示例:

提示词:

请修改下面的归档脚本,在每次移动文件时,除了记录文件移动成功或失败的信息之外,还请在日志中增加记录被处理文件的大小信息。比如,在成功移动日志中,记录“已移动文件 {filename} 到 {target_dir},文件大小为 {size} 字节”,如果移动失败,也尝试获取并记录文件大小。如果获取文件大小失败,则在异常日志中记录相关错误信息。请确保日志输出格式中包含日志时间、日志级别和日志消息。

下面是原脚本的代码:

'''

import os      # 用于文件和目录操作
import shutil  # 用于文件的复制、移动等操作
import re      # 用于正则表达式匹配日期格式

# 当前目录路径(你也可以修改为其他需要扫描的目录)
current_dir = '.'

# 定义归档根目录
archive_root = 'archive'

# 使用正则表达式匹配文件名中的日期格式:YYYY-MM-DD
date_pattern = re.compile(r'(\d{4}-\d{2}-\d{2})_report\.txt$')

# 遍历当前目录下的所有文件
for filename in os.listdir(current_dir):
    # 判断文件名是否以"_report.txt"结尾
    if filename.endswith('_report.txt'):
        # 尝试从文件名中提取日期信息
        match = date_pattern.search(filename)
        if match:
            # 获取提取到的日期字符串
            date_str = match.group(1)
            # 构建目标归档目录路径:archive/YYYY-MM-DD
            target_dir = os.path.join(archive_root, date_str)
            # 如果目标目录不存在,则创建它
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
                print(f"创建目录: {target_dir}")
            # 构建源文件和目标文件的完整路径
            source_file = os.path.join(current_dir, filename)
            target_file = os.path.join(target_dir, filename)
            try:
                # 将文件移动到目标归档目录
                shutil.move(source_file, target_file)
                print(f"已移动文件 {filename} 到 {target_dir}")
            except Exception as e:
                print(f"移动文件 {filename} 时出错: {e}")
        else:
            # 如果文件名中不包含日期信息,则跳过或记录日志
            print(f"文件 {filename} 不符合归档格式,已跳过。")

'''

通过提示词生产的示例代码:

import os
import shutil
import re
import logging

# 配置日志记录,将日志输出到控制台和文件(archive.log)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("archive.log", mode='a', encoding='utf-8'),
        logging.StreamHandler()
    ]
)

# 当前目录路径(你也可以修改为其他需要扫描的目录)
current_dir = '.'

# 定义归档根目录
archive_root = 'archive'

# 使用正则表达式匹配文件名中的日期格式:YYYY-MM-DD
date_pattern = re.compile(r'(\d{4}-\d{2}-\d{2})_report\.txt$')

# 遍历当前目录下的所有文件
for filename in os.listdir(current_dir):
    # 判断文件名是否以"_report.txt"结尾
    if filename.endswith('_report.txt'):
        # 尝试从文件名中提取日期信息
        match = date_pattern.search(filename)
        if match:
            # 获取提取到的日期字符串
            date_str = match.group(1)
            # 构建目标归档目录路径:archive/YYYY-MM-DD
            target_dir = os.path.join(archive_root, date_str)
            # 如果目标目录不存在,则创建它
            if not os.path.exists(target_dir):
                os.makedirs(target_dir)
                logging.info(f"创建目录: {target_dir}")
            # 构建源文件和目标文件的完整路径
            source_file = os.path.join(current_dir, filename)
            target_file = os.path.join(target_dir, filename)
            try:
                # 尝试获取文件大小
                file_size = os.path.getsize(source_file)
            except Exception as size_err:
                logging.warning(f"无法获取文件 {filename} 的大小: {size_err}")
                file_size = "未知"

            try:
                # 将文件移动到目标归档目录
                shutil.move(source_file, target_file)
                logging.info(f"已移动文件 {filename} 到 {target_dir},文件大小为 {file_size} 字节")
            except Exception as e:
                logging.error(f"移动文件 {filename} 时出错: {e},文件大小为 {file_size}", exc_info=True)
        else:
            # 如果文件名中不包含日期信息,则跳过或记录日志
            logging.info(f"文件 {filename} 不符合归档格式,已跳过。")

代码说明:

  • 日志配置
    使用 logging.basicConfig 配置了两个日志处理器:一个输出到 archive.log 文件,另一个输出到控制台。日志格式中包含了时间、级别和消息。

  • 获取文件大小
    在移动文件之前,使用 os.path.getsize(source_file) 获取文件大小。如果获取失败,则记录警告,并将大小标记为“未知”。

  • 日志记录
    成功移动文件时,日志中记录了文件大小;如果移动失败,同样会在错误日志中包含文件大小信息,方便后期排查问题。

练习:

请设计提示词,修改课程中的示例代码,完成下面2项任务:

  • 目前的 except Exception as e 捕获了所有异常,这不利于我们区分错误类型并进行针对性处理。现在请修改代码尝试捕获更具体的异常类型,例如 FileNotFoundErrorPermissionError 等,并根据不同的异常类型采取不同的处理措施。
  • 日志存在不同级别。示例中使用了 logging.infologging.error 两种日志级别,请尝试使用更多的日志级别,例如 logging.debuglogging.warning 等,并了解不同日志级别的含义。

AI 助教

提示:您可在此提出学习中遇到的问题。回答由 AI 生成,可能存在错误,请注意甄别。