应使用tenacity库实现可控重试,针对ConnectionError和Timeout异常,配合状态码分类处理(4xx不重试、5xx重试)、Range续传及线程池并发控制。

requests.get() 失败时怎么加重试逻辑?
直接调用 requests.get() 遇到网络抖动或服务端临时不可用,会立刻抛出 requests.exceptions.RequestException(比如 ConnectionError、Timeout),不重试就中断下载。得自己套一层重试,别依赖 requests.adapters.HTTPAdapter 的默认重试——它默认只对 5xx 响应重试,对连接失败、超时完全不生效。
推荐用 tenacity 库,比手写 while 循环更可控:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type import requests <p>@retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10), retry=retry_if_exception_type((requests.ConnectionError, requests.Timeout)) ) def download_with_retry(url): return requests.get(url, timeout=10)
注意三点:
• stop_after_attempt(3) 表示最多请求 3 次(首次 + 2 次重试)
• wait_exponential 避免雪崩,第二次重试等待约 1 秒,第三次约 2 秒
• 显式指定 retry_if_exception_type,否则 404 这类 HTTP 错误也会被重试,不合理
HTTP 状态码 4xx/5xx 怎么区分处理?
重试机制不该盲目重试所有错误响应。比如 404 是资源不存在,重试一百次也没用;503 是服务暂时不可用,值得重试;403 可能是权限问题,需要检查 headers 或 token。
在 requests.Response 返回后立即判断状态码,再决定是否 raise 异常触发重试:
立即学习“Python免费学习笔记(深入)”;
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
response = download_with_retry(url)
if response.status_code == 404:
raise FileNotFoundError(f"Resource not found: {url}")
elif response.status_code >= 500:
# 5xx 触发重试(tenacity 会捕获并重试)
response.raise_for_status()
elif response.status_code == 403:
# 记录日志,不重试,人工介入更合适
logger.warning("Forbidden access to %s", url)
return None关键点:
• 不要用 response.raise_for_status() 无差别抛异常,它对 4xx 也 raise,导致本不该重试的错误被重试
• 对 4xx 做分类:400/401/403/404 含义不同,处理策略必须差异化
• 5xx 可统一交给重试机制,但注意某些 5xx(如 501 Not Implemented)重试也没意义
大文件下载中断后如何续传?
单纯重试整个文件,遇到大文件(>100MB)极不友好。得支持 Range 请求,从断点继续下载。
核心是检查服务器是否支持断点续传(看响应头是否有 Accept-Ranges: bytes),然后读取本地已下载文件长度,用 headers={"Range": f"bytes={file_size}-"} 发起请求:
def resume_download(url, filepath):
headers = {}
if os.path.exists(filepath):
file_size = os.path.getsize(filepath)
headers["Range"] = f"bytes={file_size}-"
<pre class='brush:python;toolbar:false;'>response = requests.get(url, headers=headers, stream=True, timeout=30)
# 206 Partial Content 表示续传成功;200 表示不支持续传,需清空重下
if response.status_code == 206:
mode = "ab" # 追加写
elif response.status_code == 200 and os.path.exists(filepath):
os.remove(filepath) # 清掉残缺文件
mode = "wb"
else:
response.raise_for_status()
with open(filepath, mode) as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)</pre>容易踩的坑:
• 忘记检查 response.status_code 就直接写文件,可能把 200 响应(完整新文件)追加到旧文件末尾,造成损坏
• Range 请求返回 200 而不是 206,说明服务器不支持续传,必须删旧文件重新下
• 没设 stream=True,大文件会全加载进内存,OOM 风险高
如何避免重试期间阻塞主线程?
同步重试在下载多个文件时会卡住整个程序。如果并发下载,得用异步或线程池控制并发数和超时。
用 concurrent.futures.ThreadPoolExecutor 最简单,配合 timeout 参数防止单个任务拖垮全局:
from concurrent.futures import ThreadPoolExecutor, TimeoutError
<p>def safe_download(url, filepath, timeout=60):
try:
response = download_with_retry(url) # 带重试的同步函数
with open(filepath, "wb") as f:
f.write(response.content)
return True
except TimeoutError:
logger.error("Download timeout for %s", url)
return False
except Exception as e:
logger.error("Failed to download %s: %s", url, e)
return False</p><p>with ThreadPoolExecutor(max_workers=3) as executor:
futures = [
executor.submit(safe<em>download, url, f"file</em>{i}.bin")
for i, url in enumerate(urls)
]
results = [f.result(timeout=120) for f in futures]要点:
• max_workers 别设太高,HTTP 下载本身受带宽和服务器并发限制,设 3–5 通常够用
• 外层 timeout(submit 的 result timeout)必须设,否则某个死链会让整个 executor hang 住
• 日志里记录具体失败原因,而不是只写 “download failed”,排查时省一半力气
重试和续传逻辑本身不复杂,但组合起来容易漏掉状态码分支、文件完整性校验、磁盘空间预判这些实际生产环境里天天撞见的问题。尤其别在没验证服务器是否支持 Range 的情况下硬上续传——有些 CDN 直接忽略 Range 头,还返回 200,结果下出一堆错位文件。

















