116 lines
3.7 KiB
Python
116 lines
3.7 KiB
Python
"""应用异常定义:所有业务异常均继承自 AppError
|
|
|
|
错误码规范:
|
|
- 1xxx: 请求/鉴权错误
|
|
- 2xxx: 抓取资源错误
|
|
- 3xxx: 反爬/上游阻断相关错误
|
|
- 4xxx: 页面解析错误
|
|
"""
|
|
|
|
|
|
class AppError(Exception):
|
|
"""应用基础异常,携带错误码、HTTP 状态码和是否可重试信息"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
code: str,
|
|
err_code: int,
|
|
retryable: bool = False,
|
|
status_code: int = 400,
|
|
headers: dict[str, str] | None = None,
|
|
):
|
|
super().__init__(message)
|
|
self.message = message
|
|
self.code = code
|
|
self.err_code = err_code
|
|
self.retryable = retryable
|
|
self.status_code = status_code
|
|
self.headers = headers
|
|
|
|
|
|
class AuthenticationError(AppError):
|
|
"""鉴权失败(Bearer Token 无效或缺失)"""
|
|
|
|
def __init__(self, message: str = "Invalid credentials"):
|
|
super().__init__(
|
|
message=message,
|
|
code="AUTH_INVALID",
|
|
err_code=1001,
|
|
retryable=False,
|
|
status_code=401,
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
|
|
class InvalidRequestError(AppError):
|
|
"""请求参数不合法(如 URL 非乐天站点、缺少必填标识)"""
|
|
|
|
def __init__(self, message: str = "Invalid request"):
|
|
super().__init__(message=message, code="INVALID_REQUEST", err_code=1003, retryable=False)
|
|
|
|
|
|
class ResourceBusyError(AppError):
|
|
"""抓取资源繁忙(等待并发槽位超时)"""
|
|
|
|
def __init__(self, message: str = "Timed out while waiting for a scrape slot"):
|
|
super().__init__(message=message, code="RESOURCE_BUSY", err_code=2002, retryable=True)
|
|
|
|
|
|
class UpstreamRequestError(AppError):
|
|
"""上游请求失败(网络异常、超时、5xx)"""
|
|
|
|
def __init__(self, message: str = "The upstream request failed"):
|
|
super().__init__(message=message, code="UPSTREAM_ERROR", err_code=3001, retryable=True)
|
|
|
|
|
|
class UpstreamBlockedError(AppError):
|
|
"""上游请求被反爬阻断(Akamai 挑战页 / 403 / 页面缺少渲染数据)"""
|
|
|
|
def __init__(self, message: str = "The upstream request was blocked"):
|
|
super().__init__(message=message, code="UPSTREAM_BLOCKED", err_code=3002, retryable=True)
|
|
|
|
|
|
class ItemNotFoundError(AppError):
|
|
"""商品不存在或已下架(详情页 404)"""
|
|
|
|
def __init__(self, message: str = "Item not found"):
|
|
super().__init__(
|
|
message=message,
|
|
code="ITEM_NOT_FOUND",
|
|
err_code=4004,
|
|
retryable=False,
|
|
status_code=404,
|
|
)
|
|
|
|
|
|
class ScrapeParseError(AppError):
|
|
"""页面解析失败"""
|
|
|
|
def __init__(self, message: str = "Failed to parse the target page"):
|
|
super().__init__(message=message, code="PARSE_ERROR", err_code=4001, retryable=False)
|
|
|
|
|
|
class OffIchibaRedirectError(AppError):
|
|
"""商品页跳转到了市场之外的乐天官方子站
|
|
|
|
楽天ブックス(book → books.rakuten.co.jp)、ビックカメラ
|
|
(biccamera → biccamera.rakuten.co.jp)等官方旗舰店有各自独立的站点与
|
|
页面结构,不返回市场统一模板,本服务的详情解析不适用。
|
|
|
|
单独成一类错误是为了让上游能把这些商品路由到别处,而不是当成被反爬拦截去重试。
|
|
"""
|
|
|
|
def __init__(self, requested_url: str, final_url: str):
|
|
super().__init__(
|
|
message=(
|
|
f"商品页跳转至乐天市场以外的站点,当前不支持解析:"
|
|
f"{requested_url} -> {final_url}"
|
|
),
|
|
code="OFF_ICHIBA_REDIRECT",
|
|
err_code=4002,
|
|
retryable=False,
|
|
)
|
|
self.requested_url = requested_url
|
|
self.final_url = final_url
|