“我给 requests 设了 timeout=5,为什么还是卡了一分钟?”因为 timeout 不是总时长。这篇用本机服务器把连接超时、读超时、慢速滴灌的响应、以及不设 timeout 会怎样都复现一遍。
(本机回环做不出连接超时:macOS 上监听队列满了之后,新连接是立刻被重置,而不是卡住) timeout=(0.5, 5) → ConnectTimeout,耗时 503 ms timeout=(1.5, 5) → ConnectTimeout,耗时 1502 ms timeout=1.0 → ConnectTimeout,耗时 1002 ms 异常类的继承关系:ConnectTimeout → ConnectionError → Timeout → RequestException → OSError ReadTimeout 的继承关系:ReadTimeout → Timeout → RequestException
连一个没人回应的地址,timeout=(0.5, 5) 在 503 ms 抛 ConnectTimeout,耗时就是设定值。ConnectTimeout 继承自 ConnectionError,所以 except ConnectionError 能抓到它;ReadTimeout 不在这条继承链上。
这里用了 192.0.2.1(RFC 5737 保留给文档的地址,不属于任何真实站点)。本机回环做不出连接超时——macOS 上监听队列满了,新连接会被立刻重置而不是卡住。
timeout=(5, 1.0) → ReadTimeout,耗时 1002 ms timeout=1.0 → ReadTimeout,耗时 1002 ms timeout=(0.2, 3.0) → 成功 200,耗时 2003 ms
建立连接之后,timeout 的第二个数管“发出请求后等对方数据”。服务器 2 秒后才回应,(5, 1.0) 在约 1 秒时抛 ReadTimeout;(0.2, 3.0) 的连接超时虽然很短,但本机连接瞬间成功,不受影响。
timeout=1.0(每个间隔 0.5 秒)→ 成功 200,收到 6 个字节,耗时 3023 ms timeout=0.3(每个间隔 0.5 秒)→ ConnectionError:HTTPConnectionPool(host='127.0.0.1', port=9960): Read timed out.,耗时 302 ms requests/models.py 第 942-943 行:except ReadTimeoutError as e: → raise ConnectionError(e)
读超时限制的是每次等待数据的时间。服务器每 0.5 秒发一个字节,timeout=1.0 下整个 3 秒的响应照样成功。读响应体阶段超时的异常也不同:requests 在 models.py 里把 ReadTimeoutError 转成了 ConnectionError,所以只捕获 ReadTimeout 会漏掉这种情况。
服务器每 0.2 秒发 1 个字节永不结束,timeout=1.0 + 自己的 2 秒截止时间:超过自己设的 2 秒截止时间,主动停止;耗时 2044 ms,收到 10 个字节
用 stream=True 配合 iter_content,在循环里检查截止时间,到点主动停止并关闭响应。实测滴灌的响应 2.044 秒后被截断,收到 10 个字节。这只是最简单的做法;我没有测同时限制连接阶段和响应头阶段的总时长。
requests.request 文档里对 timeout 的说明::param timeout: (optional) How many seconds to wait for the server to send data before giving up, as a float, or a :ref:`(connect timeout, read timeout) <timeouts>` tuple. 服务器收到请求后一直不回应,3.0 秒后这个没有 timeout 的请求还在阻塞吗?True(服务器已收到 1 个请求) 放行之后它的结果:200,总耗时 3003 ms
默认是 None,也就是一直等。一个收到请求却不回应的服务器,可以让没有 timeout 的请求无限期阻塞。爬虫里每个请求都应显式传 timeout(连接和读取分开传)。