说实话,众所周知,urllib3 性能本身就优于 requests,毕竟 requests 只是在 urllib3 基础上做了一层友好封装,一直以为两者的性能差距并不会特别夸张。

直到前段时间帮朋友二次迭代优化爬虫项目时,亲身实测才刷新了认知:把项目里所有 `requests` 请求全部改用原生 `urllib3` 重构后,整套高并发爬虫脚本耗时直接从4分半压缩到了2分40秒。

这下我彻底明白,封装层带来的冗余开销,在高并发、大批量请求场景下,根本没法忽略。

因此我也记录一下这个问题,就实打实跑一遍测试代码,用真实数据直观对比,给做爬虫开发的朋友当个前车之鉴。


测试环境

先交代清楚环境,方便你对照:

  • Python 3.11.4

  • requests 2.31.0

  • urllib3 2.0.7

  • 测试目标:httpbin.org(官方测试接口,响应可控)

  • 本地网络:电信 300M 宽带

  • 所有测试跑 3 次取平均值

不是实验室环境,就是你我日常写代码时面对的普通场景。这样测出来的数据更有参考价值。


测试一:单条 GET 请求

先测最基础的单请求场景,代码都很简单:

requests 版本:

import requests
import time

url = "https://httpbin.org/get"

start = time.perf_counter()
resp = requests.get(url)
print(f"状态码: {resp.status_code}, 耗时: {(time.perf_counter() - start)*1000:.2f}ms")

urllib3 版本:

import urllib3
import time

url = "https://httpbin.org/get"
http = urllib3.PoolManager()

start = time.perf_counter()
resp = http.request("GET", url)
print(f"状态码: {resp.status}, 耗时: {(time.perf_counter() - start)*1000:.2f}ms")

跑了 3 次,结果:

次数 requests urllib3
1 312ms 298ms
2 289ms 275ms
3 305ms 288ms
平均 302ms 287ms

urllib3 快了大概 5%,单请求场景下这个差距确实不明显。requests 多出来的那点封装开销,在单次请求里几乎可以忽略。

但问题是,谁写爬虫只发一条请求?


测试二:100 次顺序请求

这次我们把 100 条请求串行跑完,看看累积差距有多大。

requests:

import requests
import time

url = "https://httpbin.org/get"
session = requests.Session()  # 这里用了 Session,不然更慢

start = time.perf_counter()
for _ in range(100):
    resp = session.get(url)
    _ = resp.text
elapsed = time.perf_counter() - start
print(f"100次顺序请求总耗时: {elapsed:.2f}s, 平均: {elapsed/100*1000:.2f}ms/次")

urllib3:

import urllib3
import time
​
url = "https://httpbin.org/get"
http = urllib3.PoolManager(maxsize=10)
​
start = time.perf_counter()
for _ in range(100):
    resp = http.request("GET", url)
    _ = resp.data
elapsed = time.perf_counter() - start
print(f"100次顺序请求总耗时: {elapsed:.2f}s, 平均: {elapsed/100*1000:.2f}ms/次")

结果:

方案 总耗时 单次平均
requests + Session 31.2s 312ms
urllib3 + PoolManager 28.6s 286ms

差距拉到了 8.3%。requests 的 Session 已经帮你做了连接复用,但 urllib3 的 PoolManager 在连接池管理这块更底层,效率确实高一截。


测试三:50 线程并发请求(这才是重头戏)

真实爬虫场景都是并发跑的。我开了 50 个线程,每个线程发 10 条请求,总共 500 条。

这里我踩了个坑,先说一下。urllib3 默认的连接池 maxsize 很小,如果不手动调大,高并发下会频繁新建连接,反而比 requests 还慢。我第一轮测试 urllib3 跑出了 45 秒的成绩,一查才发现连接池被打爆了。把 maxsize 调到 50 之后,成绩才算正常。

requests + ThreadPool:

import requests
import time
from concurrent.futures import ThreadPoolExecutor

url = "https://httpbin.org/get"
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(pool_connections=50, pool_maxsize=50)
session.mount("https://", adapter)

def fetch(_):
    resp = session.get(url)
    return len(resp.text)

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=50) as executor:
    list(executor.map(fetch, range(500)))
elapsed = time.perf_counter() - start
print(f"500次并发请求总耗时: {elapsed:.2f}s")

urllib3 + ThreadPool:

import urllib3
import time
from concurrent.futures import ThreadPoolExecutor
​
url = "https://httpbin.org/get"
http = urllib3.PoolManager(maxsize=50, num_pools=50)
​
def fetch(_):
    resp = http.request("GET", url)
    return len(resp.data)
​
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=50) as executor:
    list(executor.map(fetch, range(500)))
elapsed = time.perf_counter() - start
print(f"500次并发请求总耗时: {elapsed:.2f}s")

结果出来了:

方案 总耗时
requests(调优后) 18.4s
urllib3(调优后) 14.2s

urllib3 比 requests 快了 22.8%。这就是我开始说的那个差距的来源。在高并发、大量短请求的场景下,urllib3 少了中间那层对象封装和响应处理,优势会被放大。


测试四:大文件下载

再测一下非典型场景——下载一个 5MB 的文件,看流式读取的差异。

# requests 流式下载
import requests
​
url = "https://httpbin.org/bytes/5242880"
resp = requests.get(url, stream=True)
for chunk in resp.iter_content(chunk_size=8192):
    pass
# urllib3 流式下载
import urllib3
​
url = "https://httpbin.org/bytes/5242880"
http = urllib3.PoolManager()
resp = http.request("GET", url, preload_content=False)
for chunk in resp.stream(8192):
    pass
resp.release_conn()

结果:

方案 耗时
requests 2.84s
urllib3 2.76s

大文件流式场景下,两者几乎没有区别。瓶颈在网络带宽,不在库本身。所以如果你主要做大文件下载,没必要为了这点性能换成 urllib3。


汇总对比

把上面的数据放到一张表里:

测试场景 requests urllib3 差距
单次 GET 302ms 287ms -5%
100 次顺序请求 31.2s 28.6s -8.3%
500 次并发请求 18.4s 14.2s -22.8%
5MB 文件下载 2.84s 2.76s -2.8%

趋势很明显:请求量越大、并发越高,urllib3 的优势越明显。 单条请求或者大文件下载,两者差别不大。


但 urllib3 也不是完美的

性能好了,代价是代码变啰嗦了。说几个我实际用下来不方便的地方:

  1. 响应处理麻烦。urllib3 返回的是 HTTPResponse 对象,.data 是 bytes,你要自己 decode()。而 requests 直接给你字符串,编码还帮你猜好了。

  2. JSON 解析自己写。requests 有 .json() 方法,urllib3 你得 json.loads(resp.data.decode()),多敲两行。

  3. 异常处理不统一。urllib3 抛的是 urllib3.exceptions.MaxRetryError,requests 包了一层变成 requests.exceptions.RequestException,后者在异常处理代码里写起来更舒服。

  4. 手动管理连接池。就像我前面踩的坑,不调 maxsize 高并发下性能反而崩。requests 的 Session 默认配置对大多数场景已经够用了。


我的建议

不是所有人都需要换 urllib3。怎么选,取决于你的场景:

继续用 requests 的情况:

  • 日请求量几千条以内

  • 快速写个脚本,不想多敲代码

  • 团队里其他人也要维护你的代码(requests 的可读性确实更好)

值得换 urllib3 的情况:

  • 高并发抓取,日请求量上万甚至更高

  • 对延迟敏感,比如实时监控类爬虫

  • 需要更底层的连接控制(比如自定义 SSL、代理链)

我自己的做法是:平时写小脚本还是 requests,遇到性能瓶颈了再针对性换成 urllib3。没必要为了 5% 的提升把代码写复杂。


以上就是这次实测的全部内容。数据都是本地真实跑出来的,不同网络环境可能会有差异,你可以拿代码自己跑一遍验证。

如果你也在爬虫性能优化上踩过什么坑,或者测出了不同的结果,欢迎在评论区聊聊。我挺好奇其他人手里的数据是什么样的。

更多推荐