Python requests 超时与重试入门
刚开始用 Python 写网络请求时,很多人只关心能不能拿到数据,却容易忽略两个最常见的翻车点:请求一直卡住,以及偶发失败后直接报错退出。requests 库本身提供了超时和重试的机制,理解它们的分工,就能让脚本在真实网络里稳得多。
超时不是一个数字
requests 的 timeout 参数看起来简单,实际上包含两个阶段。连接超时是指建立 TCP 连接的最长等待时间,读取超时是指发送请求后等待服务器返回数据的最长等待时间。写成 timeout=5 时,两个阶段各最多等 5 秒;写成元组 timeout=(3, 10) 时,连接最多等 3 秒,读取最多等 10 秒。
只给一个很大的总时间并不保险。连接阶段卡住和读取阶段缓慢是两种不同的故障,前者往往是网络或地址问题,后者可能是服务端处理太慢。分开设置的好处是定位更快:连接超时多半要检查地址、代理和网络,读取超时则要看接口本身是不是太重,或者是不是需要分页、降级。
没有超时会发生什么
不传 timeout 时,请求可能一直等下去。对一次性脚本来说只是多等一会儿,对定时任务和批量抓取来说就很危险:一个卡住的请求会拖住整个队列,后面的任务全部排队,严重时还会把连接池占满。因此我的习惯是:所有发往外部的请求都必须显式传 timeout,哪怕只是先给一个保守值,后面再根据接口表现调优。
重试要解决偶发失败
超时、连接重置、服务端短暂返回 502 或 503,很多都属于偶发问题,稍后再试一次往往就好了。重试的核心不是无脑循环,而是三条规则。第一,只对可重试的失败重试,例如超时和 5xx,客户端参数写错导致的 4xx 不应该反复重试。第二,次数要有限,一般 2 到 3 次就够了,太多只会放大服务端压力。第三,两次重试之间要留间隔,最好带一点随机抖动,避免大量客户端在同一秒一起重试。
一个可直接用的写法
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(total=3, backoff_factor=0.5, status_forcelist=[502, 503, 504])
adapter = HTTPAdapter(max_retries=retry)
session = requests.Session()
session.mount('https://', adapter)
resp = session.get('https://example.com/api', timeout=(3, 10))
resp.raise_for_status()
print(resp.status_code)
这里 backoff_factor 会让等待时间逐步拉长,配合 status_forcelist 只对指定的服务端错误重试。注意,默认情况下重试主要针对连接类错误和指定状态码,读取超时等失败是否重试,要结合接口是否幂等来判断。
幂等与副作用
重试最容易踩的坑是重复提交。查询类请求重试一般是安全的,但下单、扣款、创建资源这类有副作用的操作,重复发一次可能就多产生一条记录。遇到这类接口,优先用接口提供的幂等键,或者先查询结果再决定是否补发,而不是直接循环重试。
上线前的检查清单
第一,所有请求都有 timeout,而且连接和读取分开考虑。第二,重试次数有限,间隔带抖动,只重试偶发失败。第三,日志里记录超时类型、状态码和重试次数,方便区分是网络问题还是服务端问题。第四,批量任务要控制并发,避免重试把本已吃紧的服务端彻底压垮。
把超时和重试当成默认配置而不是事后补丁,脚本的健壮性会立刻上一个台阶。下次写 requests 代码时,先问自己一句:这个请求卡住了怎么办,失败了要不要再试一次,答案写进代码里,比事后排查省事得多。
- 点赞
- 收藏
- 关注作者
评论(0)