你写爬虫如果还停留在requests.get(url)一把梭的水平那遇到稍微有点规模的采集任务大概率会卡在各种超时、重试和并发问题上。今天这篇教程我聚焦在httpx和aiohttp这两个HTTP客户端库上聊清楚怎么对它们做深度定制让爬虫的并发能力、连接复用、请求稳定性都更可控。这也算是很多老爬虫工程师在实际项目里反复打磨出来的经验适合已经会写简单爬虫、但想提升性能和稳定性的同学。直接开门见山说结论单纯用requests做同步请求在1000个URL这种量级下你会被IO等待活活拖死。而httpx支持同步和异步两种模式aiohttp干脆就是纯异步它们都支持连接复用、并发请求、自定义重试策略还能精细控制超时和连接池。这些特性恰恰是深度定制爬虫HTTP客户端最核心的切入点。1. 为什么我们要对HTTP客户端库做深度定制1.1 默认客户端库的局限很多入门教程会让你直接用requests它确实简单可一旦爬虫规模上来短板就非常明显同步阻塞发一个请求就必须等响应回来了才能继续下一个CPU基本都在空转浪费大量时间。默认没有内置重试机制遇到网络抖动请求直接抛异常整个爬虫跟着崩。连接复用不够灵活requests的会话复用依赖于显式使用Session对象但实际使用中很多人没这个概念导致每次请求都要重新建立TCP连接效率低下。不支持HTTP/2而很多现代网站已经用上了HTTP/2这会导致请求被边缘节点处理得更慢甚至因为协议特征被识别。当然你也可以在requests基础上做各种封装但问题是它的底层架构决定了你折腾再多也很难在性能上跟原生支持异步的库抗衡。这就像你非要在手动挡的车上去搞自动换挡逻辑投入产出比太低。1.2 httpx与aiohttp的核心优势对比我们来看一张对比表这样选型时心里更有底。特性httpxaiohttp编程范式同步 异步双模式纯异步内置重试无需自己封装无需自己封装HTTP/2支持有设置http2True即可无需要额外库如h2连接池控制有通过limits参数有通过connector控制Cookie持久化自动Client内部管理自动ClientSession内部管理学习曲线平缓与requests类似较陡需要熟悉async/await从表里能看出来如果项目要求同时写同步脚本和异步脚本用httpx可以一套API搞定。如果项目本身就定位在高并发异步采集那aiohttp是更专业的选择。我的个人习惯是快速原型、小规模采集用httpx同步模式大规模并发任务直接上aiohttp。两个都值得掌握因为它们解决的是不同场景下的问题。2. httpx深度定制实战2.1 客户端初始化与核心参数用httpx做深度定制首先你得创建一个Client对象而不是每次用httpx.get()这种顶层函数。因为Client内部会维护连接池和Cookie状态复用起来效率高得多。import httpx client httpx.Client( base_urlhttps://example.edu, timeouthttpx.Timeout(10.0, connect5.0), headers{User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36}, http2True, )这里重点说一下timeout参数。它不是简单的数字而是一个httpx.Timeout对象可以分别设置连接超时、读超时、写超时和池超时。我通常会把connect设成5秒整体读取设成10秒这样即使某个网站响应慢也不会拖垮整个任务。尤其在爬虫场景网络波动是常态超时设置不合理会造成大量请求卡死。http2True这个参数很实用它能启用HTTP/2支持。很多网站在HTTP/2下响应更快而且HTTP/2支持多路复用多个请求可以在一个连接上并行发送这在并发采集时能显著减少TCP连接数量。要注意的是启用HTTP/2需要额外安装h2这个库安装命令很简单pip install h2。2.2 连接池复用与限制连接池是深度定制里最容易忽视的一环。在爬虫场景下如果每次请求都新建连接目标网站一次要接受大量短连接很容易触发反爬策略。所以我们要主动控制连接池大小让连接保持活跃并复用。import httpx limits httpx.Limits(max_keepalive_connections10, max_connections50) client httpx.Client(limitslimits)max_keepalive_connections控制保持长连接的数量max_connections控制整个连接池允许的最大连接数。实际配置时要根据目标网站并发请求量来估算。比如你计划并发跑100个任务但连接池只开5个那很多请求就要排队性能反而下降。我一般会按并发数的1.5到2倍来设置max_connections这样既能充分利用连接又不会给目标网站服务器造成过大压力。还有个容易踩的坑limits参数里的max_keepalive_connections如果设得太小比如默认的10在高并发场景下频繁创建和关闭连接反而会增加延迟。我曾经在一个采集任务里把它调成50并发请求的响应时间直接缩短了大约30%。2.3 重试与异常处理机制httpx本身不提供重试机制但这恰恰是深度定制的好地方。爬虫运行在复杂网络环境里连接超时、DNS解析失败、5xx错误都很常见我们需要一个灵活的重试策略。import httpx import time class RetryTransport(httpx.BaseTransport): def __init__(self, max_retries3): super().__init__() self.max_retries max_retries def handle_request(self, request): for attempt in range(self.max_retries): try: response super().handle_request(request) if response.status_code 500: return response except (httpx.ConnectTimeout, httpx.ConnectError): time.sleep(0.5 * (attempt 1)) return response这里用一个简单的重试包装只在连接错误或5xx状态码时重试避免对4xx错误做无意义重试。退避时间用了简单的线性增长实际场景里可以换成指数退避比如time.sleep(2 ** attempt)。重试次数我一般控制在3次以内再多就会放大目标网站的负载反而被更严格地封锁。2.4 会话与Cookie持久化爬虫经常需要维持登录状态Client对象会自动管理Cookie。当你用client.get()请求一个带登录接口的URL时返回的Set-Cookie会自动保存后续请求会自动附带这些Cookie。这个机制省去了手动操作CookieJar的麻烦。import httpx client httpx.Client(base_urlhttps://example.edu) # 登录自动保存Cookie client.post(/login, data{username: your_name, password: your_pass}) # 后续请求自动带上Cookie res client.get(/profile) print(res.status_code)这在爬取需要登录的页面时特别方便。不过要注意如果目标网站有多个登录入口或者Cookie频繁失效你可能需要定时重新登录。另外Client对象用完后记得执行client.close()或者使用上下文管理器否则连接会一直占着资源。3. aiohttp深度定制实战3.1 异步客户端基础用法aiohttp的使用方式跟httpx差别很大它基于asyncio必须用async/await。先说个最常见的误区很多人把aiohttp当成多线程库来用实际上它是单线程事件循环所有请求都是非阻塞IO所以必须用异步写法。import asyncio import aiohttp async def fetch(session, url): async with session.get(url) as response: return await response.text() async def main(): async with aiohttp.ClientSession() as session: html await fetch(session, https://example.edu) print(len(html)) asyncio.run(main())这里ClientSession是核心对象它同样维护连接池和Cookie。最关键的一点是整个应用里尽量复用同一个ClientSession不要每次都新建。因为ClientSession内部有连接复用机制频繁创建会极大地浪费效率。3.2 并发控制与信号量异步爬虫最怕的就是把目标网站冲垮所以并发控制是深度定制的必修课。aiohttp配合asyncio.Semaphore可以非常精确地控制同时发起的请求数量。import asyncio import aiohttp async def fetch_with_limit(session, url, semaphore): async with semaphore: async with session.get(url) as response: return await response.text() async def main(): sem asyncio.Semaphore(10) # 最多允许10个并发请求 async with aiohttp.ClientSession() as session: tasks [fetch_with_limit(session, fhttps://example.edu/{i}, sem) for i in range(100)] results await asyncio.gather(*tasks) asyncio.run(main())信号量的大小取决于目标网站的处理能力和你的网络带宽。我在实际项目里常用的配置是小型网站并发5~10大型网站并发20~30。如果并发开太大响应延迟会明显上升因为请求在排队反而降低整体效率。这个参数需要在测试中反复调整找到最优值。3.3 超时与连接管理aiohttp的超时控制和连接管理都在ClientSession的timeout参数里配置。它支持设置总超时也支持连接超时和读取超时分开设置。import aiohttp timeout aiohttp.ClientTimeout(total10, connect5, sock_read10) async with aiohttp.ClientSession(timeouttimeout) as session: # ...这里total是整体请求的超时时间connect是连接建立超时sock_read是底层的读取超时。有时候你会发现connect设了5秒但DNS解析却花了好几十秒这是因为connect超时可能不包含DNS解析时间这是aiohttp的已知行为。要处理这种情况一个简单做法是设置更大的total超时然后自己用asyncio.wait_for控制总时长。try: async with asyncio.wait_for(session.get(url), timeout15): # ... except asyncio.TimeoutError: print(Request timed out)这样能有效避免请求卡死。另外连接池大小通过connector来控制connector aiohttp.TCPConnector(limit50, keepalive_timeout30) async with aiohttp.ClientSession(connectorconnector) as session: # ...limit是并发连接上限keepalive_timeout控制连接保持活跃的时间。合理设置这两个参数能极大减少TCP三次握手次数提升爬虫效率。3.4 代理与SSL定制爬虫经常会用到代理aiohttp支持每个会话设置代理也支持每个请求单独设置代理。async with aiohttp.ClientSession() as session: proxies http://127.0.0.1:7890 async with session.get(url, proxyproxies) as response: # ...在实际项目中代理IP会经常更换。我通常会准备一个代理池每次请求时随机挑选一个。要注意的是如果代理质量不稳定请求失败率会很高这时重试逻辑特别重要。另外有些目标网站会检查SSL指纹aiohttp默认使用系统的SSL验证你可以在TCPConnector里关闭SSL验证来规避一些指纹检测。connector aiohttp.TCPConnector(sslFalse) # 关闭SSL验证不过关闭SSL验证有安全隐患只建议在可信任的采集场景下使用否则可能会被中间人攻击。4. 常见问题与排查技巧实录4.1 连接池耗尽症状运行一段时间后程序开始疯狂报ConnectionPoolExhaustedException或者TimeoutError但之前都正常。原因连接池里的连接数被占满可能是因为并发任务太多或者有些请求延迟特别高迟迟不释放连接。解决方法首先检查并发数是否过高调低Semaphore值其次确认TCPConnector的limit是否足够通常设置为并发数的2倍最后一定要确保每个请求都会在async with块内完成避免连接泄露。我遇到过一次是response没及时读完导致连接一直占用把limit调大也没用最后找到原因是因为读取响应体太慢。解决方法是缩短read超时时间或者直接不读取body只取header因为有的采集场景只需要状态码。4.2 超时设置失效症状明明设置了timeout10但一个请求跑了30秒还没退出。原因很可能是aiohttp的超时没有覆盖DNS解析时间或者请求已经发出但服务器一直不响应总超时没有生效。排查方法使用asyncio.wait_for包裹整个请求协程强制设置最长时间。比如await asyncio.wait_for(session.get(url), timeout10)。另外检查是否在ClientSession的timeout参数里误用了默认值正确做法是实例化ClientTimeout对象。参数计算方面total超时时间建议设为connect read 2秒的余量。不要设得太紧否则正常的慢响应也会被误杀。4.3 请求头被反爬识别症状返回的页面内容明显不正常比如出现验证码或者所有请求都返回403。原因请求头特征太明显比如默认的User-Agent是Python-httpx/0.23.x很容易被识别。解决技巧模仿真实浏览器的请求头包括User-Agent、Accept-Language、Accept-Encoding、Referer等。不要每次都发一模一样的请求头可以稍微随机化User-Agent列表随机选择一个。请求频率要控制用asyncio.Sleep(0.1~0.5)随机间隔避免规律性太强。我在实际测试中发现有些网站还会检查Sec-Fetch-Site、Sec-Fetch-Mode等前端指标这些也要一并模拟否则就算请求头看起来像浏览器也会被边缘计算识别。4.4 异步爬虫中共享Session的坑症状使用aiohttp时如果在并发任务里直接共用一个ClientSession日志中会出现大量连接重置或EOF错误。原因ClientSession设计初衷是可供多个任务共享但如果你的代码里在某个任务中调用了session.close()其它任务就会遭殃。另外如果asyncio.run()被反复调用ClientSession的事件循环绑定不上也会出错。最佳实践在最顶层的async def main()里定义一个ClientSession然后传给所有子任务绝对不要让子任务自己创建ClientSession。程序结束时在finally块里统一关闭。async def main(): session aiohttp.ClientSession() try: # ... finally: await session.close()5. 独家经验与避坑建议5.1 日志与调试深度定制HTTP客户端库的时候你肯定会遇到各种奇怪的问题。这时候日志就是你的救命稻草。httpx和aiohttp都支持通过Python标准库记录日志但默认不显示。import logging logging.basicConfig(levellogging.DEBUG) logging.getLogger(httpx).setLevel(logging.DEBUG)开启HTTP层日志后你能看到每次请求的URL、状态码、耗时、请求头这对排查反爬陷阱特别有用。aiohttp也类似import aiohttp import logging logging.getLogger(aiohttp.client).setLevel(logging.DEBUG)注意生产环境不要开DEBUG级别否则日志量会非常大我一般用INFO级别记录请求状态和错误。5.2 性能调优建议爬虫性能调优是个平衡木过度优化反而容易触发反爬。我的建议是先用小规模并发测试比如10个请求观察响应时间。根据响应时间调整并发数如果延迟从1秒变成3秒说明服务器或本地网络已经饱和应该降并发。充分利用连接池尽量让连接保持活跃避免频繁重建。对于固定域名的请求保持同一个连接会话不要跨域复用连接。另外处理页面解析时不要把解析逻辑放在IO密集的请求循环里应该先快速抓取响应统一存到列表或队列再做CPU密集的解析这样可以显著提升整体吞吐量。5.3 我的实际体会带过好几个爬虫项目最后发现最影响成败的反而往往是HTTP客户端这种底层环节。有一次我用aiohttp爬一个教育网站一开始用默认设置总是几分钟就挂。后来我深度定制了连接池大小、超时和重试策略把并发控制在合理范围整整跑了三天多都没有中断采集了大概几十万条数据这个结果让我很意外也说不上哪一步特别神奇但每一个细节加在一起稳定性就完全不一样了。最后再分享一个小技巧如果你要在爬虫里做多次请求比如先登录再拉取数据强烈建议用httpx的Client或aiohttp的ClientSession统一管理Cookie。不用自己维护Cookie头省心很多。遇到目标网站改版或者加了新的反爬策略多看看日志里返回的响应头往往能找到一些线索。根据这些信息再调整请求头和访问频率效率会高很多。