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=True,BusinessException默认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元素都定义一个异常类,结果异常类数量比业务类还多。我的原则是:按「错误处理策略」分组,而不是按「错误发生位置」分组。同一个处理策略的错误,用一个异常类就够了。
异常类的继承与扩展
随着项目变大,你可能会发现异常类也需要「进化」。我常用的扩展方式有两种:
- 添加上下文信息:比如在
BusinessException里加一个business_id字段,记录当前处理的业务单据ID。 - 添加处理建议:比如加一个
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文件,把业务异常类都定义好。哪怕一开始只定义三五个,也比后面临时补要强得多。你想想看,一个跑在生产环境上的机器人,如果连「业务异常」和「技术异常」都分不清,那日志系统再完善也白搭。
嗯,关于自定义异常,今天就聊到这儿。下一章,我们聊聊如何把这些异常和日志系统结合起来,让日志真正「会说话」。