爬虫里最常见的优化建议是“用 Session”。这篇不停留在结论上:在本机起一个会记录连接的测试服务器,看 requests.get 和 Session 各自到底开了多少条连接、省下多少时间,再看看哪些写法会让 Session 白用。
每次 requests.get:每轮平均 0.48, 0.47, 0.54, 0.54, 0.50 ms;中位数 0.50 ms 同一个 Session :每轮平均 0.31, 0.30, 0.34, 0.33, 0.35 ms;中位数 0.33 ms 服务器共接受了 1505 条连接,收到 3000 个请求(5 轮 × 2 种做法 × 300 = 3000 个请求) 两种做法的耗时比(每次新建 / Session)= 1.55 单独统计:300 次 requests.get → 服务器接受 300 条连接,每条连接上的请求数 [1] 单独统计:一个 Session 发 300 次 → 服务器接受 1 条连接,每条连接上的请求数 [300]
requests.get 每次调用在内部临时建一个 Session,用完就关(源码 requests/api.py 里就是 with sessions.Session() as session),所以每次调用都是一条新连接。服务器记录的结果和这一点吻合:300 次调用对应 300 条连接。同一个 Session 复用同一条连接发了 300 个请求。
回环网络的往返时间接近 0,所以这里 0.50 ms 对 0.33 ms 的差距基本只是建立连接本身的开销;真实网络上每条新连接还要多付至少一个往返,本文没有测。
每次 requests.get:每轮平均 1.51, 1.51, 1.56, 1.51, 1.57 ms;中位数 1.51 ms 同一个 Session :每轮平均 0.37, 0.35, 0.34, 0.33, 0.35 ms;中位数 0.35 ms 服务器共接受了 755 条连接,收到 1500 个请求(5 × 2 × 150 = 1500) 两种做法的耗时比(每次新建 / Session)= 4.34
HTTPS 下每次新建连接要多做 TLS 握手,单次耗时的比值从 1.55 倍拉到 4.34 倍。绝对值很小(本机回环、EC 证书),但它是每个请求都要付的固定成本。差额主要来自握手只是我的推断——没有把握手单独计时,也没有对比不同的密钥类型。
stream=True,不读也不关 → 服务器接受 4 条连接;各连接上的请求数 [1, 1, 1, 1] stream=True,读完 r.content → 服务器接受 1 条连接;各连接上的请求数 [4] stream=True,不读直接 r.close() → 服务器接受 4 条连接;各连接上的请求数 [1, 1, 1, 1]
复用连接要求上一个响应已经被读完。stream=True 的响应如果既不读也不关,连接还被它占着,下一个请求只能新开;读完 r.content 才会把连接还回池里。不读直接 r.close() 也不行:Response.close() 在内容还没消费时先调用 raw.close(),底层连接被关掉,下次要重新建立。只想看响应头就丢掉的写法,会让 Session 的收益消失。
服务器每次回 Connection: close 并关闭:Session 发 5 次 → 服务器接受 5 条连接,各连接上的请求数 [1, 1, 1, 1, 1] 客户端发 Connection: close,服务器照办(回 Connection: close 并关闭):Session 发 5 次 → 5 条连接,各连接上的请求数 [1, 1, 1, 1, 1] 客户端发 Connection: close,但服务器无视并保持连接:Session 发 5 次 → 1 条连接,各连接上的请求数 [5]
服务器回 Connection: close 并关闭连接,Session 每次也要新建。客户端自己在请求里写 Connection: close 只是“请求服务器关闭”:服务器照办时同样是 5 次 5 条连接;服务器无视它时,Session 仍然复用同一条连接。urllib3 从池里取连接时会检查连接是否已经断开(is_connection_dropped),断开了就新建。