4、自定义异常:如何定义自己的异常类、在RPA场景中自定义业务异常

说实话,很多RPA开发者一开始都不太重视自定义异常。他们觉得,系统自带的异常类型够用了,干嘛还要自己写?

我刚开始做RPA项目时也这么想。直到有一次,一个财务机器人跑了一整夜,第二天发现它在某个分支流程里静默失败了——没有任何有效日志,根本不知道哪里出了问题。嗯,从那以后,我养成了一个习惯:每个业务场景都定义自己的异常类

为什么需要自定义异常?

你想想看,系统自带的异常像什么?像一把万能钥匙。它能告诉你「出错了」,但说不清「哪里错了」、「为什么错」。在RPA场景里,这远远不够。

举个例子:一个发票录入机器人,可能遇到的情况有:

  • OCR识别失败(技术异常)
  • 发票号码重复(业务异常)
  • 金额超过审批限额(业务规则异常)
  • 系统超时(基础设施异常)

这些错误,用Exception都能捕获。但捕获之后呢?你分不清是重试、跳过、还是直接告警。自定义异常,就是给每个错误贴上「身份标签」。

核心观点:自定义异常不是炫技,是让异常处理变得可管理、可追溯、可自动化决策。

如何定义自己的异常类?

在Python里,定义自定义异常非常简单。我一般遵循这个模板:

class RPAException(Exception):
    """RPA基础异常类"""
    def __init__(self, message, error_code=None, source=None):
        self.message = message
        self.error_code = error_code  # 错误编码,方便日志检索
        self.source = source          # 异常来源模块
        super().__init__(self.message)

    def __str__(self):
        return f"[{self.error_code}] {self.message} (来源: {self.source})"

你看,我加了三个关键字段:message(描述)、error_code(编码)、source(来源)。这样,一旦抛出异常,日志里就能直接看到「哪个模块、什么错误、编码多少」。排查问题时,效率翻倍。

在RPA场景中自定义业务异常

RPA项目里,我习惯把异常分成三层:

异常层级 示例 处理策略
基础设施异常 网络断开、数据库连接失败 重试3次,失败后告警
应用异常 元素未找到、点击无效 截图+日志,跳过当前步骤
业务异常 数据校验不通过、流程状态异常 记录业务日志,人工介入

基于这个分层,我会定义对应的异常子类:

class InfrastructureException(RPAException):
    """基础设施异常:网络、数据库、文件系统"""
    pass

class ApplicationException(RPAException):
    """应用异常:UI操作、元素定位、系统弹窗"""
    pass

class BusinessException(RPAException):
    """业务异常:数据校验、流程规则、业务状态"""
    pass

这样做的好处是什么?我举个例子。在发票录入流程中,如果OCR识别失败,抛出ApplicationException,机器人自动重试一次;如果发票号码已存在,抛出BusinessException,机器人直接跳过并记录;如果数据库连不上,抛出InfrastructureException,机器人停止并通知运维。

我的小技巧:每个自定义异常类,我都会加一个retryable属性。比如InfrastructureException默认retryable=TrueBusinessException默认retryable=False。这样,异常处理框架就能自动决定是否重试。

实战:一个完整的业务异常示例

我记得在做一个银行对账机器人时,遇到过一种情况:对账文件里的金额和系统里的金额不一致。这不是技术错误,是业务数据问题。我定义了一个专门的异常:

class ReconciliationMismatchException(BusinessException):
    """对账金额不匹配异常"""
    def __init__(self, account_no, expected, actual, diff):
        self.account_no = account_no
        self.expected = expected
        self.actual = actual
        self.diff = diff
        message = f"账户{account_no}对账失败:期望{expected},实际{actual},差异{diff}"
        super().__init__(message, error_code="BIZ-001", source="ReconciliationModule")

    def to_dict(self):
        """将异常转为字典,方便写入数据库或发送通知"""
        return {
            "type": "ReconciliationMismatch",
            "account_no": self.account_no,
            "expected": self.expected,
            "actual": self.actual,
            "diff": self.diff,
            "timestamp": datetime.now().isoformat()
        }

你看,这个异常不仅包含了错误信息,还提供了to_dict()方法。这样,异常处理模块可以直接把异常数据写入数据库,或者通过邮件发送给财务人员。省去了额外的数据组装步骤。

注意:自定义异常不要滥用。我见过有人为每个UI元素都定义一个异常类,结果异常类数量比业务类还多。我的原则是:按「错误处理策略」分组,而不是按「错误发生位置」分组。同一个处理策略的错误,用一个异常类就够了。

异常类的继承与扩展

随着项目变大,你可能会发现异常类也需要「进化」。我常用的扩展方式有两种:

  1. 添加上下文信息:比如在BusinessException里加一个business_id字段,记录当前处理的业务单据ID。
  2. 添加处理建议:比如加一个suggestion字段,告诉运维人员「这个错误可以手动修复后重试」。

举个例子:

class BusinessException(RPAException):
    def __init__(self, message, error_code=None, source=None, business_id=None, suggestion=None):
        self.business_id = business_id
        self.suggestion = suggestion
        super().__init__(message, error_code, source)

    def get_alert_message(self):
        """生成告警消息,包含处理建议"""
        msg = f"[{self.error_code}] {self.message}"
        if self.suggestion:
            msg += f"\n建议操作:{self.suggestion}"
        return msg

这样做的好处是,异常本身就能「指导」后续操作。运维人员看到告警消息,不用翻文档就知道怎么处理。

总结一下

自定义异常,说白了就是给你的RPA机器人装上一套「专业术语系统」。系统异常说「出错了」,你的自定义异常会说「发票号码重复,跳过当前记录,继续处理下一张」。后者,才是生产环境真正需要的。

我个人习惯,每个RPA项目一开始就建一个exceptions.py文件,把业务异常类都定义好。哪怕一开始只定义三五个,也比后面临时补要强得多。你想想看,一个跑在生产环境上的机器人,如果连「业务异常」和「技术异常」都分不清,那日志系统再完善也白搭。

嗯,关于自定义异常,今天就聊到这儿。下一章,我们聊聊如何把这些异常和日志系统结合起来,让日志真正「会说话」。