requests 快速上手:会话、超时与重试

requests 的价值不在于「能发请求」,标准库也能;它把连接复用、Cookie 管理、编码猜测、重定向这些琐事收进了一套稳定接口,是同步阻塞但足够省心的选择。但它的默认值对采集并不安全:超时默认永不超时,会话默认不复用,网络抖动默认不重试。本章按「一次请求的最低配置」把这三件事补齐。

安装与最低起点

python -m pip install requests

requests 是同步阻塞库,调试方便、生态成熟,绝大多数采集任务够用;只有在确认瓶颈是网络等待、且目标站点允许的前提下,才值得换 httpx 之类的异步方案。

get 的关键参数

参数作用实践建议
params拼到 URL 的查询参数传字典让它做 URL 编码,不要手工拼字符串
headers请求头至少给出可识别的 User-Agent,注明用途与联系方式
timeout超时,秒或 (连接, 读取) 元组每个请求都必须显式设置
allow_redirects是否自动跟随重定向排查登录态时设为 False,直接看 302 的 Location
verify是否校验证书保持 True,不要为了方便关掉证书校验
import requests

resp = requests.get(
    "https://example.com/api/list",
    params={"page": 1, "size": 20},        # 自动 URL 编码,中文参数也安全
    headers={"User-Agent": "Mozilla/5.0 (compatible; MyCollector/1.0)"},
    timeout=(3, 10),                       # 连接 3 秒、读取 10 秒,必须显式写
    allow_redirects=True,                  # 默认 True;排查登录态时改成 False
)
print(resp.status_code, resp.url)          # 最终 URL 能看出参数编码与重定向结果

timeout=(3, 10) 到底管什么

写法含义结论
不写永不超时,卡住就一直等永远不要这样写
timeout=10连接与读取各最多 10 秒可以,但两类超时混在一起难定位
timeout=(3, 10)建连最多 3 秒,连上后读取最多 10 秒推荐的默认写法

不写超时的后果不是「慢」,而是脚本可能永久挂死在半开连接上:任务既没完成也没报错,监控里看不到任何异常。采集量一大,这类挂死是最难排查的问题之一,成本远高于多敲几个字符。

Session:复用连接与 Cookie

用法连接复用Cookie 保持适用场景
requests.get()一次性请求、接口探活
requests.Session()同站点批量采集、需要登录态
Session + HTTPAdapter还需要自动重试与连接池上限

状态码判断与重试

raise_for_status() 在 4xx/5xx 时抛出 HTTPError,比手写 if resp.status_code != 200 更难漏判;注意它不把 3xx 当错误,判断依据是重定向后的最终状态。重试必须带退避,否则等于用更高频率继续撞墙。

参数含义建议值
total总重试次数3,够覆盖抖动,再多就是给对方添压力
backoff_factor退避系数,等待 factor * 2^(n-1)0.5 ~ 1
status_forcelist哪些状态码触发重试[429, 500, 502, 503, 504]
allowed_methods允许重试的方法["GET", "HEAD"],写方法不要重试

异常分类与处理姿势

异常触发条件处理姿势
ConnectTimeout建连阶段超时目标不可达或网络抖动,退避后重试
ReadTimeout已连接但读取响应超时服务端处理慢,可重试但要降低频率
Timeout上面两者的父类兜底捕获,日志里记下具体子类
ConnectionErrorDNS 失败、连接被重置有限次重试,仍失败则停止并告警
TooManyRedirects重定向次数超过上限多为登录跳转循环,检查会话与 Location
HTTPErrorraise_for_status() 抛出4xx 说明请求本身不对,别盲目重试

捕获顺序要先具体后宽泛:ConnectTimeoutReadTimeout 写在 Timeout 前面,Timeout 写在 RequestException 前面,否则永远进不了细分分支。

可直接运行的完整脚本

"""带会话复用、显式超时、自动重试与频率控制的采集骨架。"""

import random
import time

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

BASE_URL = "https://example.com"
UA = "Mozilla/5.0 (compatible; MyCollector/1.0; +https://example.com/bot)"
MIN_INTERVAL = 1.0      # 两次请求之间的最小间隔,合规要求,不要调成 0


def build_session() -> requests.Session:
    """会话挂上重试策略:只重试安全方法,并尊重 Retry-After。"""
    retry = Retry(total=3, backoff_factor=0.5, respect_retry_after_header=True,
                  status_forcelist=[429, 500, 502, 503, 504],
                  allowed_methods=["GET", "HEAD"])           # 写方法一律不重试
    session = requests.Session()
    session.headers.update({"User-Agent": UA, "Accept-Language": "zh-CN,zh;q=0.9"})
    session.mount("https://", HTTPAdapter(max_retries=retry, pool_connections=10,
                                          pool_maxsize=10))
    return session


def main() -> None:
    with build_session() as session:
        for page in range(1, 6):
            url = f"{BASE_URL}/api/list"
            try:
                resp = session.get(url, params={"page": page, "size": 20}, timeout=(3, 10))
                resp.raise_for_status()              # 4xx/5xx 抛 HTTPError
                items = resp.json().get("items", [])
                print(f"第 {page} 页拿到 {len(items)} 条")
            except requests.exceptions.Timeout:
                print(f"请求超时:{url}")
            except requests.exceptions.HTTPError as exc:
                print(f"状态码异常 {exc.response.status_code}:{url}")
            except requests.exceptions.ConnectionError:
                print(f"连接失败,检查网络或降低并发:{url}")
                break                                # 网络已经断了,别继续
            time.sleep(MIN_INTERVAL + random.uniform(0, 0.5))   # 固定间隔加随机抖动


if __name__ == "__main__":
    main()

常见坑与要点对照

现象常见原因处理要点
脚本莫名挂死没有设置超时每个请求写 timeout=(3, 10)
反复 429重试不带退避、并发过高backoff_factor,尊重 Retry-After
采集几百页后变慢每次新建会话、反复握手复用 Session,必要时调整连接池上限
重试后重复写入数据把写方法也放进了重试列表allowed_methods 只留 GETHEAD

并发不是越高越好:重试加多线程在对方看来可能接近攻击,先测出对方能承受的速率,再决定线程数与间隔。合规上同样如此:确认 robots.txt 与服务条款允许该路径,保持请求间隔,不采集个人隐私与受版权保护的付费内容,不绕过登录、付费墙、验证码等访问控制——重试与并发都建立在对方允许的前提之上。

小结:每个请求显式写 timeout=(3, 10),批量采集用 Session 复用连接与 Cookie,网络抖动交给 HTTPAdapter 加退避重试,异常按具体到宽泛逐层捕获,并在每次请求之间保留合规的间隔。

笔记加载中…