说实话众所周知urllib3 性能本身就优于 requests毕竟 requests 只是在 urllib3 基础上做了一层友好封装一直以为两者的性能差距并不会特别夸张。直到前段时间帮朋友二次迭代优化爬虫项目时亲身实测才刷新了认知把项目里所有 requests 请求全部改用原生 urllib3 重构后整套高并发爬虫脚本耗时直接从4分半压缩到了2分40秒。这下我彻底明白封装层带来的冗余开销在高并发、大批量请求场景下根本没法忽略。因此我也记录一下这个问题就实打实跑一遍测试代码用真实数据直观对比给做爬虫开发的朋友当个前车之鉴。测试环境先交代清楚环境方便你对照Python 3.11.4requests 2.31.0urllib3 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 次结果次数requestsurllib31312ms298ms2289ms275ms3305ms288ms平均302ms287msurllib3 快了大概 5%单请求场景下这个差距确实不明显。requests 多出来的那点封装开销在单次请求里几乎可以忽略。但问题是谁写爬虫只发一条请求测试二100 次顺序请求这次我们把 100 条请求串行跑完看看累积差距有多大。requestsimport 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(f100次顺序请求总耗时: {elapsed:.2f}s, 平均: {elapsed/100*1000:.2f}ms/次)urllib3import urllib3 import time ​ url https://httpbin.org/get http urllib3.PoolManager(maxsize10) ​ start time.perf_counter() for _ in range(100): resp http.request(GET, url) _ resp.data elapsed time.perf_counter() - start print(f100次顺序请求总耗时: {elapsed:.2f}s, 平均: {elapsed/100*1000:.2f}ms/次)结果方案总耗时单次平均requests Session31.2s312msurllib3 PoolManager28.6s286ms差距拉到了8.3%。requests 的 Session 已经帮你做了连接复用但 urllib3 的 PoolManager 在连接池管理这块更底层效率确实高一截。测试三50 线程并发请求这才是重头戏真实爬虫场景都是并发跑的。我开了 50 个线程每个线程发 10 条请求总共 500 条。这里我踩了个坑先说一下。urllib3 默认的连接池maxsize很小如果不手动调大高并发下会频繁新建连接反而比 requests 还慢。我第一轮测试 urllib3 跑出了 45 秒的成绩一查才发现连接池被打爆了。把maxsize调到 50 之后成绩才算正常。requests ThreadPoolimport requests import time from concurrent.futures import ThreadPoolExecutor url https://httpbin.org/get session requests.Session() adapter requests.adapters.HTTPAdapter(pool_connections50, pool_maxsize50) session.mount(https://, adapter) def fetch(_): resp session.get(url) return len(resp.text) start time.perf_counter() with ThreadPoolExecutor(max_workers50) as executor: list(executor.map(fetch, range(500))) elapsed time.perf_counter() - start print(f500次并发请求总耗时: {elapsed:.2f}s)urllib3 ThreadPoolimport urllib3 import time from concurrent.futures import ThreadPoolExecutor ​ url https://httpbin.org/get http urllib3.PoolManager(maxsize50, num_pools50) ​ def fetch(_): resp http.request(GET, url) return len(resp.data) ​ start time.perf_counter() with ThreadPoolExecutor(max_workers50) as executor: list(executor.map(fetch, range(500))) elapsed time.perf_counter() - start print(f500次并发请求总耗时: {elapsed:.2f}s)结果出来了方案总耗时requests调优后18.4surllib3调优后14.2surllib3 比 requests 快了 22.8%。这就是我开始说的那个差距的来源。在高并发、大量短请求的场景下urllib3 少了中间那层对象封装和响应处理优势会被放大。测试四大文件下载再测一下非典型场景——下载一个 5MB 的文件看流式读取的差异。# requests 流式下载 import requests ​ url https://httpbin.org/bytes/5242880 resp requests.get(url, streamTrue) for chunk in resp.iter_content(chunk_size8192): pass# urllib3 流式下载 import urllib3 ​ url https://httpbin.org/bytes/5242880 http urllib3.PoolManager() resp http.request(GET, url, preload_contentFalse) for chunk in resp.stream(8192): pass resp.release_conn()结果方案耗时requests2.84surllib32.76s大文件流式场景下两者几乎没有区别。瓶颈在网络带宽不在库本身。所以如果你主要做大文件下载没必要为了这点性能换成 urllib3。汇总对比把上面的数据放到一张表里测试场景requestsurllib3差距单次 GET302ms287ms-5%100 次顺序请求31.2s28.6s-8.3%500 次并发请求18.4s14.2s-22.8%5MB 文件下载2.84s2.76s-2.8%趋势很明显请求量越大、并发越高urllib3 的优势越明显。单条请求或者大文件下载两者差别不大。但 urllib3 也不是完美的性能好了代价是代码变啰嗦了。说几个我实际用下来不方便的地方响应处理麻烦。urllib3 返回的是HTTPResponse对象.data是 bytes你要自己decode()。而 requests 直接给你字符串编码还帮你猜好了。JSON 解析自己写。requests 有.json()方法urllib3 你得json.loads(resp.data.decode())多敲两行。异常处理不统一。urllib3 抛的是urllib3.exceptions.MaxRetryErrorrequests 包了一层变成requests.exceptions.RequestException后者在异常处理代码里写起来更舒服。手动管理连接池。就像我前面踩的坑不调maxsize高并发下性能反而崩。requests 的 Session 默认配置对大多数场景已经够用了。我的建议不是所有人都需要换 urllib3。怎么选取决于你的场景继续用 requests 的情况日请求量几千条以内快速写个脚本不想多敲代码团队里其他人也要维护你的代码requests 的可读性确实更好值得换 urllib3 的情况高并发抓取日请求量上万甚至更高对延迟敏感比如实时监控类爬虫需要更底层的连接控制比如自定义 SSL、代理链我自己的做法是平时写小脚本还是 requests遇到性能瓶颈了再针对性换成 urllib3。没必要为了 5% 的提升把代码写复杂。以上就是这次实测的全部内容。数据都是本地真实跑出来的不同网络环境可能会有差异你可以拿代码自己跑一遍验证。如果你也在爬虫性能优化上踩过什么坑或者测出了不同的结果欢迎在评论区聊聊。我挺好奇其他人手里的数据是什么样的。