多线程爬虫虽然比单线程快几倍,但线程本身有开销,而且受限于 Python 的 GIL 锁。异步爬虫是更高阶的方案——单线程处理上千个并发请求,比多线程快 5~10 倍。
一、异步 vs 多线程 vs 单线程
| 对比 | 单线程 | 多线程(5线程) | 异步(aiohttp) |
|---|---|---|---|
| 爬取10页(250条) | ~10秒 | ~2.8秒 | ~1秒 |
| 爬取100页(2500条) | ~100秒 | ~30秒 | ~5秒 |
| CPU占用 | 低 | 中 | 低 |
| 代码复杂度 | 简单 | 中等 | 稍高 |
| 适合场景 | 小规模 | 中等规模 | 大规模 |
二、异步爬虫原理
普通爬虫发请求后要等待服务器响应,这段时间 CPU 是空闲的。异步爬虫在等待的过程中切换去发送其他请求,等响应回来了再回来处理。
普通请求:发送 → 等待 → 处理 → 发送 → 等待 → 处理 ...
异步请求:发送 → 发送 → 发送 → 处理 → 处理 → 处理 ...
(同时发多个请求,谁回来了处理谁)
三、环境准备
pip install aiohttp aiodns
四、基本用法
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.com")
print(len(html))
# 运行
asyncio.run(main())
五、实战:异步爬取豆瓣 Top250
import asyncio
import aiohttp
from lxml import etree
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
async def fetch(session, url):
"""异步请求"""
try:
async with session.get(url, headers=HEADERS, timeout=10) as resp:
return await resp.text()
except Exception as e:
print(f"请求失败: {url[:30]}... {e}")
return None
async def scrape_page(session, page):
"""异步爬取单页"""
url = f"https://movie.douban.com/top250?start={page * 25}"
html = await fetch(session, url)
if not html:
return []
tree = etree.HTML(html)
items = tree.xpath('//ol[@class="grid_view"]/li')
movies = []
for item in items:
rank = item.xpath('.//em/text()')[0]
title = item.xpath('.//span[@class="title"]/text()')[0]
rating = item.xpath('.//span[@class="rating_num"]/text()')[0]
people_text = item.xpath('.//span[contains(text(), "人评价")]/text()')
people = people_text[0].strip() if people_text else "0人评价"
movies.append({
"rank": int(rank),
"title": title,
"rating": float(rating),
"people": people
})
print(f"第{page+1}页完成,{len(movies)}条")
return movies
async def crawl_all():
"""异步爬取全部10页"""
async with aiohttp.ClientSession() as session:
# 创建10个爬取任务
tasks = [scrape_page(session, page) for page in range(10)]
# 并发执行所有任务
results = await asyncio.gather(*tasks)
# 合并结果并排序
all_movies = []
for movies in results:
all_movies.extend(movies)
all_movies.sort(key=lambda x: x["rank"])
return all_movies
运行
import time
start = time.time()
movies = asyncio.run(crawl_all())
print(f"\n共{len(movies)}部电影,耗时{time.time()-start:.1f}秒")
输出效果:
第1页完成,25条
第3页完成,25条
第5页完成,25条
第2页完成,25条
...
共250部电影,耗时1.2秒
六、异步爬虫 + CSV 保存
爬取后自动保存到文件:
import csv
async def crawl_and_save():
movies = await crawl_all()
with open("douban_top250.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["rank", "title", "rating", "people"])
writer.writeheader()
writer.writerows(movies)
print(f"已保存到 douban_top250.csv(共{len(movies)}条)")
asyncio.run(crawl_and_save())
七、性能对比测试
| 方式 | 250条耗时 | 2500条耗时 | 代码量 |
|---|---|---|---|
| 单线程 | ~10秒 | ~100秒 | 40行 |
| 多线程(5线程) | ~2.8秒 | ~30秒 | 60行 |
| 异步 aiohttp | ~1.2秒 | ~5秒 | 70行 |
数据量越大,异步优势越明显。
八、踩坑提醒
1. 异步函数中不能使用 requests
# 错误:requests 是同步库,会阻塞事件循环
async def fetch(url):
return requests.get(url)
# 正确:使用 aiohttp
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.text()
2. 控制并发数
同时发太多请求会被封,用 asyncio.Semaphore 限制:
sem = asyncio.Semaphore(5) # 最多5个并发
async def fetch(session, url):
async with sem: # 排队等待
async with session.get(url) as resp:
return await resp.text()
3. 超时设置
异步请求一定要设置超时,避免某个请求卡死:
timeout = aiohttp.ClientTimeout(total=10)
async with session.get(url, timeout=timeout) as resp:
...
4. 异步不能直接跟 matplotlib 混用
matplotlib 是同步的,先爬取完数据,再同步画图。
总结
异步爬虫是处理大规模数据采集的最佳方案。虽然学习曲线比 requests 陡一些,但带来的性能提升非常值得。建议爬取 100 页以下用多线程,100 页以上用异步。
爬虫系列完结! 从 requests 入门 → XPath → BS4 → Selenium → 反爬 → 多线程 → 异步,一套完整的爬虫技能链已经覆盖。后续将分享更多实战项目和 Java 开发相关文章。
如果对你有帮助,欢迎点赞、评论、关注【张老师技术栈】,持续分享 Java/Python/爬虫 实战干货。

7597

被折叠的 条评论
为什么被折叠?



