requests.get下载文件常失败,主因是网络不稳定、临时5xx/429错误、DNS超时等;需按错误类型区别重试(如5xx可重试,4xx不重试),配合指数退避、显式timeout及stream=True流式写入防OOM。

为什么直接用 requests.get 下载文件经常失败?
网络请求不稳定、临时 503/429 错误、DNS 解析超时、连接被中间设备中断——这些都会让 requests.get(url) 直接抛出 requests.exceptions.ConnectionError、Timeout 或 ChunkedEncodingError。不加控制地重试,可能触发服务端限流;完全不重试,小概率失败就导致整个流程中断。
关键不是“多试几次”,而是:按错误类型区别对待 + 指数退避 + 限制总耗时。
-
ConnectionError和Timeout值得重试(网络抖动) -
HTTPError中的 4xx(如 404、403)通常不重试(客户端问题) -
HTTPError中的 5xx(如 502、504)可重试(服务端临时故障) - 首次重试延迟建议 1 秒,后续按 2ⁿ 指数增长,上限设为 10 秒
- 总重试次数建议 ≤ 3 次,避免长时间卡死
用 urllib3.util.retry.Retry 配合 requests.Session 最省心
自己写 while 循环 + sleep 容易漏掉异常分类或退避逻辑。requests 底层基于 urllib3,它内置的 Retry 类已覆盖常见策略,且与 Session 深度集成。
注意:必须把 Retry 绑定到 Session 的 mount 上,不能只传给单次 get() 调用:
立即学习“Python免费学习笔记(深入)”;
import requests
from urllib3.util.retry import Retry
<p>session = requests.Session()
retry_strategy = Retry(
total=3,
status_forcelist=(502, 503, 504, 429),
backoff_factor=1,
allowed_methods=["HEAD", "GET", "OPTIONS"] # 注意:默认不含 POST,下载用 GET 就够了
)
adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)</p><h1>后续所有 session.get() 都自动带重试</h1><p>response = session.get(url, timeout=(3.05, 27)) # (connect_timeout, read_timeout)
容易踩的坑:timeout 必须显式传入,否则重试时可能沿用默认的长超时;backoff_factor=1 表示第 n 次重试前等待 1×2ⁿ⁻¹ 秒(即 1s → 2s → 4s);allowed_methods 若不指定,默认禁用 GET 重试(历史兼容行为)。
边下载边写入文件,避免内存爆满
大文件(如 >10MB)用 response.content 会一次性加载进内存,容易 OOM。应使用 stream=True + 分块读取 + open(..., "wb") 追加写入。
但要注意:开启 stream=True 后,response.status_code 仍可立即获取,而响应体需手动迭代 response.iter_content();若中途重试失败,已写入的部分文件要清理,否则下次下载会得到损坏文件:
- 先生成临时文件名(如
filename + ".tmp") - 下载完成并校验(如比对
Content-Length或 MD5)后,再os.replace()覆盖原文件 - 任何异常都需
os.unlink(tmp_path)清理残留 - 别用
shutil.copyfileobj—— 它不支持超时和重试感知,底层仍是阻塞读
示例关键片段:
with session.get(url, stream=True, timeout=(3.05, 27)) as response:
response.raise_for_status() # 触发 4xx/5xx 异常,交由 Retry 处理
tmp_path = f"{filename}.tmp"
with open(tmp_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
# 校验长度
expected = response.headers.get("Content-Length")
if expected and str(os.path.getsize(tmp_path)) != expected:
raise RuntimeError("Download incomplete")
os.replace(tmp_path, filename)
重试失败后,怎么知道是哪类错误?
当最终抛出异常时,原始错误链会被 Retry 包装成 MaxRetryError,但内层原因仍可追溯。直接打印 exc.reason 或用 raise_from 展开更清晰:
try:
response = session.get(url, stream=True, timeout=(3.05, 27))
except requests.exceptions.RetryError as exc:
cause = exc.reason # 是 ConnectionError?Timeout?还是 MaxRetryError?
if isinstance(cause, requests.exceptions.Timeout):
print("反复超时,可能是网络或目标不可达")
elif isinstance(cause, requests.exceptions.ConnectionError):
print("连不上服务器,检查 URL 或代理设置")
else:
print(f"其他重试终止原因: {cause}")
真实场景中,503 响应头常带 Retry-After 字段,这时应优先尊重该值而非固定退避——但 Retry 类不自动解析它,需手动提取并在自定义重试逻辑中处理。普通下载任务里,标准 Retry 已覆盖 95% 场景;真有 Retry-After 依赖,就得绕过 Retry 自己实现循环了。


















