(十四)按失败动作重试 GET
1. 从一次失败推导下一次尝试
第一次读取返回 503,稍后再次读取可能成功;路径写错得到 404,则应该检查路径。页面已经返回 200,但书名节点缺失,也应该检查页面结构,而不是立即把相同内容再下载几次。
| 观察到的失败 | 本章采取的动作 |
|---|---|
| 429、500、502、503、504 | 记录尝试次数,等待后再次 GET |
| 连接失败、读取超时、远端协议中断 | 在尝试次数内重新读取 |
| 404 等其他失败状态 | 将 URL 和状态反馈到任务结果 |
| HTML 结构或书籍字段错误 | 保存具体解析错误,任务留待检查后恢复 |
| 调用者取消或整批时间到期 | 继续取消、收尾与退出 |
# 项目文件:crawler_project/src/catalogcrawl/transport.py
# 这里只重试读取 GET;状态失败与网络等待失败保留 URL 和尝试次数。
from asyncio import sleep
import logging
import math
import httpx
from .errors import CrawlError
logger = logging.getLogger(__name__)
RETRY_STATUS = {429, 500, 502, 503, 504}
def retry_delay(response, attempt):
fallback = 0.05 * 2 ** (attempt - 1)
value = response.headers.get("Retry-After") if response is not None else None
try:
seconds = float(value)
except (TypeError, ValueError):
return fallback
return seconds if math.isfinite(seconds) and seconds >= 0 else fallback
async def get_response(client, url, gate, attempts=3):
if attempts < 1:
raise ValueError("请求尝试次数应至少为 1")
for attempt in range(1, attempts + 1):
await gate.wait()
response = None
try:
response = await client.get(url)
response.raise_for_status()
return response
except httpx.HTTPStatusError as cause:
retryable = cause.response.status_code in RETRY_STATUS
failure = cause
except (
httpx.ConnectError,
httpx.ReadTimeout,
httpx.RemoteProtocolError,
) as cause:
retryable, failure = True, cause
except httpx.RequestError as cause:
raise CrawlError(f"{url}:{type(cause).__name__} {cause}") from cause
if not retryable or attempt == attempts:
raise CrawlError(f"{url}:第 {attempt} 次读取失败,{failure}") from failure
delay = retry_delay(response, attempt)
logger.warning("准备重试 url=%s attempt=%s wait=%.3f", url, attempt, delay)
# 退避让出循环;下一次尝试仍经过共享的 StartGate。
await sleep(delay)没有秒数提示时,等待按 0.05 × 2^(attempt−1) 计算:第一次失败后等 0.05 秒,第二次失败后等 0.10 秒,第三次仍失败就反馈。练习站的 Retry-After 使用秒数形式,函数优先读取该数值;这次服务发出 0,表示下一次尝试可继续经过开始门安排。Retry-After
failure 保留本次异常,最终用 raise ... from failure 把路径、尝试次数和底层原因连起来。response 每轮重新置空,连接失败时也能计算下一次等待。HTML 解析不在这个函数里,所以解析错误不会被误算成网络重试。
2. 让服务连续失败两次再成功
# 项目文件:demos/retry_get.py
# 服务前两次返回 503,第三次成功;重试范围覆盖读取,不覆盖 HTML 字段解析。
import asyncio
import httpx
from crawlsite import running_site
from catalogcrawl.pacing import StartGate
from catalogcrawl.transport import get_response
async def main(base_url):
async with httpx.AsyncClient(timeout=1, trust_env=False) as client:
response = await get_response(
client, base_url + "/books/b103", StartGate(0), attempts=3
)
print(response.status_code)
if __name__ == "__main__":
with running_site() as site:
site.state.failures["/books/b103"] = 2
asyncio.run(main(site.base_url))
print("尝试次数", site.state.counts["/books/b103"])前两次请求得到 503,日志各记录一次重试;第三次得到 200,最后显示 尝试次数 3。这里重试的是读取。上一章新增阅读日志的 POST 会产生新记录,不能直接套进这段循环,当成同一操作的自动重试。