requests.get() 必须显式设置 timeout,否则会无限等待;推荐使用元组形式 timeout=(connect, read),并分别捕获 timeout 和 connectionerror 异常,同时在并发场景下需配合线程池或 asyncio 的外层超时机制。

requests.get() 必须显式设置 timeout,不设就是无限等待
Python 的 requests 默认不设超时,遇到网络卡顿、服务器假死或防火墙拦截时,requests.get() 会一直挂起,整个爬虫线程就卡死了。这不是 bug,是设计如此——它把“等多久”完全交给你决定。
实操建议:
-
timeout是必填安全项,不是可选项;哪怕只写timeout=10也比不写强 - 推荐拆成元组形式:
timeout=(3, 7),前者是连接超时(connect),后者是读取超时(read);连接阶段卡住说明目标不可达,读取阶段卡住可能是响应慢或流式传输 - 不要用单个数字(如
timeout=5),它只作用于 connect 阶段,read 阶段仍可能无限阻塞 - 超时值不是越小越好:设成
0.5看似激进,但公网 DNS 解析、TLS 握手都可能耗时 200ms+,容易误杀正常请求
捕获 requests.exceptions.Timeout 而不是通用异常
超时抛出的是特定异常类型,不是 Exception 或 ConnectionError。用错 except 会导致超时被吞掉,程序看似“没报错”,实则逻辑跳过或静默失败。
常见错误现象:
- 写了
except Exception:却没做任何日志,结果发现某些 URL 总是漏抓,查半天才发现是超时被静默忽略了 - 把
Timeout和ConnectionError合并在一个 except 块里处理,但两者原因不同:前者是“连上了但没及时回”,后者是“根本连不上”,重试策略应区分
正确做法:
try:
resp = requests.get(url, timeout=(3, 7))
except requests.exceptions.Timeout:
print(f"Timeout on {url}")
# 可重试,但建议加退避:time.sleep(1 * (2 ** retry_count))
except requests.exceptions.ConnectionError:
print(f"Unreachable: {url}")
# 连不上就别重试了,大概率是域名错或网络断了
timeout 对 session 复用和重定向的影响
很多人以为给 Session() 对象设一次 timeout 就全局生效,其实不是:Session 本身不存 timeout,每次调用 session.get() 仍需传入 timeout 参数。另外,重定向(redirect)会复用原始 timeout 设置,但每跳都单独计时——也就是说,302 跳转后的新请求,依然受同一个 timeout 约束。
使用场景与坑点:
- 如果你手动禁用了重定向(
allow_redirects=False),那后续的session.get()调用必须自己处理 302 Location 并发新请求,此时新请求的timeout得重新传,不会继承上一次 - 代理环境下(
proxies=...),DNS 解析和代理握手也计入 connect 时间,容易触发ConnectTimeout,建议 connect 时间略放宽(如(5, 7)) - HTTP/2 或长连接复用(keep-alive)不会绕过 timeout,每个 request-level 的读取阶段仍独立计时
并发请求下 timeout 要配合 concurrent.futures 的 timeout
用 ThreadPoolExecutor 跑多个 requests.get() 时,光设 requests 的 timeout 不够。如果某个请求因系统级问题(比如 GIL 持有、线程饿死)迟迟不返回,外层线程池不会自动中断它——你得用 executor 的 future.result(timeout=...) 再套一层保护。
性能与兼容性影响:
- requests 层 timeout 控制的是 socket 级阻塞,executor 层 timeout 控制的是 Python 线程调度级等待;二者缺一不可
- 别把两层 timeout 设成一样值(比如都设 5),executor 层应略大于 requests 层(如 requests 用
(3, 7),executor 用12),留出异常捕获和上下文切换余量 - asyncio +
aiohttp场景同理,要同时设client.timeout和asyncio.wait_for(..., timeout=...)
真正难处理的,是那些既没抛异常、又没返回响应的“半死连接”——它不触发 requests timeout(因为 TCP 还活着),也不触发 executor timeout(因为线程还在跑),只能靠更底层的 socket 选项或进程级看门狗。这种边界情况,多数人还没碰到,但一旦碰到,就很难 debug。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











