Python爬虫】实测:高并发下urllib3比requests快20%,但我依然选requests…
说实话,众所周知,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 也不是完美的
性能好了,代价是代码变啰嗦了。说几个我实际用下来不方便的地方:
-
响应处理麻烦。urllib3 返回的是
HTTPResponse对象,.data是 bytes,你要自己decode()。而 requests 直接给你字符串,编码还帮你猜好了。 -
JSON 解析自己写。requests 有
.json()方法,urllib3 你得json.loads(resp.data.decode()),多敲两行。 -
异常处理不统一。urllib3 抛的是
urllib3.exceptions.MaxRetryError,requests 包了一层变成requests.exceptions.RequestException,后者在异常处理代码里写起来更舒服。 -
手动管理连接池。就像我前面踩的坑,不调
maxsize高并发下性能反而崩。requests 的 Session 默认配置对大多数场景已经够用了。
我的建议
不是所有人都需要换 urllib3。怎么选,取决于你的场景:
继续用 requests 的情况:
-
日请求量几千条以内
-
快速写个脚本,不想多敲代码
-
团队里其他人也要维护你的代码(requests 的可读性确实更好)
值得换 urllib3 的情况:
-
高并发抓取,日请求量上万甚至更高
-
对延迟敏感,比如实时监控类爬虫
-
需要更底层的连接控制(比如自定义 SSL、代理链)
我自己的做法是:平时写小脚本还是 requests,遇到性能瓶颈了再针对性换成 urllib3。没必要为了 5% 的提升把代码写复杂。
以上就是这次实测的全部内容。数据都是本地真实跑出来的,不同网络环境可能会有差异,你可以拿代码自己跑一遍验证。
如果你也在爬虫性能优化上踩过什么坑,或者测出了不同的结果,欢迎在评论区聊聊。我挺好奇其他人手里的数据是什么样的。
更多推荐
所有评论(0)