Python 异步爬虫教程 (2024 更新版)
又到了每周爬虫优化环节?明明爬100个带1秒延迟的测试页,同步要蹲一分半钟,多线程怕冲突怕堆资源、进程间通信又麻烦——没错,Python协程异步爬虫才是2024年IO密集型任务的黄金解!
1. 异步爬虫概述
1.1 为什么非异步不可?
爬虫的核心瓶颈从来不是CPU运算,而是网络/磁盘IO等待——比如你敲了一个 requests.get(),接下来几十几百毫秒甚至几秒,程序就在原地“死等”服务器响应,啥也干不了。
而异步爬虫,会在某个任务等待IO时,立刻切去执行其他可运行的任务,把等待时间全用满!
1.2 三大核心优势
2. 核心极简入门(无公式版)
不用搞复杂的底层调度算法,先记住这三个关键词:
2.1 协程(Coroutine)
可以理解为「能暂停、能恢复、由程序员主动控制」的任务单元——类比成看电影:
- 同步线程:一部电影看到底,中途不接电话
- 协程:看电影暂停 → 接紧急电话 → 处理完挂掉 → 回到电影暂停的地方继续看
2.2 事件循环(Event Loop)
这是协程的「总调度员」,在后台一直循环做三件事:
- 检查所有协程:哪些是已经暂停但IO完成的(可以恢复)?哪些是刚启动可以运行的?
- 按规则选一个任务执行
- 任务执行到暂停点(
await),再回到循环
Python 3.7+ 提供了超方便的入口 asyncio.run(),不用手动创建/关闭事件循环了!
2.3 async/await 语法糖
是让协程代码看起来像同步代码的魔法:
async def:告诉Python「这不是普通函数,是协程函数,调用后会返回协程对象,不会立刻执行」
await:只能用在 async def 里,意思是「等这个异步操作完成再往下走,期间你去忙别的任务」
3. 主流异步HTTP工具:aiohttp
Python异步生态里用得最多的就是 aiohttp,2024年最新3.9+版本体验更丝滑!
3.1 最基础的单页爬取
import aiohttp
import asyncio
async def fetch(url):
# 异步上下文管理器管理Session(类比requests.Session)
# 会自动复用连接池、自动清理资源
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
# 等待响应文本读取完成
return await response.text()
async def main():
html = await fetch('https://example.com')
print(html[:200]) # 只打印前200字,避免刷屏
# 3.7+ 标准入口
asyncio.run(main())
3.2 2024版3.9+值得用的小更新
- 默认开启HTTP/2支持(需手动确认依赖
h2 已安装)
- 优化了DNS解析的并发缓存
- 更细粒度的连接复用和超时控制
- 内置对
httpx.Response 格式的兼容(方便迁移老代码)
4. 实战:高性能URL批量爬取
直接上一个带exception-handling、并发控制、数据解析的完整脚本框架!
4.1 完整代码
import aiohttp
import asyncio
from bs4 import BeautifulSoup
from typing import List, Optional, Dict
# 限制并发数,避免被目标网站封IP
CONCURRENT_LIMIT = 20 # 单总连接数
HOST_LIMIT = 5 # 单个域名的最大连接数(非常重要!防封)
TIMEOUT = aiohttp.ClientTimeout(total=30, connect=10)
async def fetch_single(
session: aiohttp.ClientSession,
url: str
) -> Optional[str]:
"""爬取单个URL并处理异常"""
try:
async with session.get(url, timeout=TIMEOUT) as resp:
if resp.status == 200:
# 可以根据需要改成await resp.json()/await resp.read()
return await resp.text()
# 记录非200状态码
print(f"⚠️ {url} 返回状态码 {resp.status}")
return None
except asyncio.TimeoutError:
print(f"⏱️ {url} 超时")
return None
except Exception as e:
print(f"❌ {url} 未知错误: {str(e)[:100]}")
return None
async def parse_single(html: str) -> Optional[Dict]:
"""异步解析单个页面(这里用bs4是同步,但小数据量不影响)"""
if not html:
return None
soup = BeautifulSoup(html, 'lxml') # lxml比html.parser快很多
# 👇 这里替换成你的解析逻辑,比如
title = soup.title.string if soup.title else None
return {"title": title}
async def batch_crawl(urls: List[str]) -> List[Dict]:
"""批量爬取+解析的主协程"""
# 配置连接池
connector = aiohttp.TCPConnector(
limit=CONCURRENT_LIMIT,
limit_per_host=HOST_LIMIT,
force_close=False, # 开启长连接复用
enable_cleanup_closed=True
)
# 批量执行任务
async with aiohttp.ClientSession(connector=connector) as session:
# 生成所有爬取任务
fetch_tasks = [fetch_single(session, url) for url in urls]
# 等待所有爬取任务完成(gather会收集所有结果,即使有失败)
raw_pages = await asyncio.gather(*fetch_tasks)
# 生成所有解析任务(过滤掉None的页面)
parse_tasks = [parse_single(page) for page in raw_pages if page]
# 等待所有解析完成
results = await asyncio.gather(*parse_tasks)
# 最后过滤一下解析失败的None
return [res for res in results if res]
if __name__ == "__main__":
# 测试用的10个带1秒延迟的URL
test_urls = [f"https://httpbin.org/delay/1?num={i}" for i in range(10)]
# 爬取+计时
import time
start = time.time()
final_data = asyncio.run(batch_crawl(test_urls))
end = time.time()
# 输出结果
print(f"\n✅ 成功爬取+解析 {len(final_data)} 条数据")
print(f"⏱️ 总耗时: {end - start:.2f} 秒")
小提示:asyncio.gather() 会按原顺序收集结果,即使某些任务失败(抛出异常)也不会中断整个批次,除非你设置 return_exceptions=True。
5. 最佳实践避坑指南
5.1 防封IP是第一要务
除了上面代码里的 limit_per_host,还可以加:
- 速率限制:用
aiolimiter 库,限制每秒请求数
from aiolimiter import AsyncLimiter
limiter = AsyncLimiter(5, 1) # 单个域名每秒5个请求
async def limited_fetch(session, url):
async with limiter:
return await fetch_single(session, url)
- 随机User-Agent:用
fake_useragent_async 库
- 随机延迟:在
await fetch 前加 await asyncio.sleep(random.uniform(0.1, 0.5))
5.2 不要混用同步阻塞代码
如果在协程里调用 requests.get()、time.sleep()、open() 这种同步阻塞的东西,事件循环会被完全卡住,异步就白用了!
- 换对应的异步库:
requests → aiohttp / httpx,time.sleep → asyncio.sleep,open → aiofiles
5.3 异步解析也很重要?
如果你的数据解析非常非常复杂(比如几万字的长文本、大量正则匹配),可以用:
asyncio.to_thread():把同步解析扔到线程池里跑(Python 3.9+ 内置)
- 专门的异步解析库(比如
selectolax 虽然是同步,但比 bs4 快10倍以上,小数据量/中等数据量直接用就行)
6. 快速性能对比(10条延迟1秒URL测试)
直接用上面的实战脚本简化改一下同步/多线程版本测试:
注意:HOST_LIMIT 要根据目标网站的 robots.txt 或实际反爬策略调整,不是越大越好!
7. 总结
2024年,Python协程异步爬虫已经是入门级但高效率的选择——不用懂复杂的底层,只要记住「用 async def 定义任务,用 await 挂起等待,用 asyncio.run 启动」,再配合 aiohttp 的连接池和exception-handling,就能写出比同步快几十倍的爬虫!
进一步学习的三个核心链接
- aiohttp 官方中文文档(虽旧但核心内容没变)
- Real Python 异步IO入门(英文但讲得超清楚)
- httpx官方文档(aiohttp的替代,API和requests几乎一模一样,支持同步+异步)