一、为什么需要异步爬虫?
传统同步爬虫就像排队买奶茶:服务员做好一杯你才能接下一杯。当网站响应慢时,CPU大部分时间在等待数据返回,效率极低。而异步爬虫如同点单后先逛商场,等广播通知再取餐——在等待一个请求响应时,CPU可以处理其他任务。
以某电商网站为例:同步模式采集10万商品需要12小时,使用aiohttp异步方案仅需45分钟。这种效率跃升正是百万级数据采集的核心前提。
「编程类软件工具合集」 链接:pan.quark.cn/s/0b6102d9a…
二、aiohttp核心优势解析
- 轻量级设计:相比Scrapy框架,aiohttp更接近原生协程实现,内存占用降低60%
- 精准控制:可自定义连接池大小、超时策略等20+项参数
- 协议支持:原生支持HTTP/2,对现代网站更友好
- 扩展性:与aioredis、aiomysql等异步库无缝集成
测试数据显示:在4核8G服务器上,aiohttp可维持3000+并发连接,而传统Requests库超过500连接就会出现性能断崖式下跌。
三、百万级采集系统架构设计
1. 基础组件搭建
import aiohttp
import asyncio
from collections import deque
class AsyncCrawler:
def __init__(self, max_concurrency=500):
self.semaphore = asyncio.Semaphore(max_concurrency)
self.session = aiohttp.ClientSession()
self.task_queue = deque()
关键参数说明:
max_concurrency:建议设置为CPU核心数×100(4核服务器推荐400-500)ClientSession:需全局复用,避免重复创建销毁
2. 请求调度系统
async def fetch_url(self, url, retry_times=3):
async with self.semaphore:
for _ in range(retry_times):
try:
async with self.session.get(url, timeout=30) as resp:
if resp.status == 200:
return await resp.text()
await asyncio.sleep(1) # 状态码非200时短暂等待
except (aiohttp.ClientError, asyncio.TimeoutError):
continue
return None
重试机制设计要点:
- 指数退避策略:连续失败时等待时间按1s, 2s, 4s递增
- 异常分类处理:区分网络错误和业务逻辑错误
- 结果校验:返回前验证内容长度是否符合预期
3. 分布式任务分发
async def worker(self, task_queue):
while True:
url = task_queue.popleft()
html = await self.fetch_url(url)
if html:
# 处理数据并存储
pass
# 动态调整并发数
if len(task_queue) < 1000 and self.semaphore._value < self.max_concurrency//2:
self.semaphore._value += 10
任务队列优化技巧:
- 使用Redis的BRPOP实现跨机器消费
- 优先级队列:重要页面优先处理
- 进度持久化:每处理1000条保存当前位置
四、性能优化实战
1. 连接池调优
connector = aiohttp.TCPConnector(
limit_per_host=100, # 单域名最大连接数
ttl_dns_cache=300, # DNS缓存时间
force_close=False # 保持长连接
)
session = aiohttp.ClientSession(connector=connector)
实测数据:
- 调整
limit_per_host后,某新闻网站采集速度提升2.3倍 - 开启DNS缓存使冷启动时间减少40%
2. 数据压缩传输
async with session.get(
url,
headers={'Accept-Encoding': 'gzip, deflate'},
compress='gzip'
) as resp:
# 自动解压处理
data = await resp.read()
效果对比:
- 文本类数据体积减少70-85%
- 网络传输时间缩短60%以上
3. 智能解析策略
from parsel import Selector
def parse_content(html):
try:
sel = Selector(text=html)
# 优先使用CSS选择器
title = sel.css('h1::text').get()
# 备用XPath方案
if not title:
title = sel.xpath('//h1/text()').get()
return {'title': title}
except Exception:
return None
容错设计原则:
- 关键字段多重提取方案
- 异常时返回部分数据而非完全失败
- 记录解析失败URL供后续分析
五、百万级数据存储方案
1. 时序数据库选型
| 方案 | 写入速度 | 查询效率 | 存储成本 |
|---|---|---|---|
| MongoDB | 5k/s | 中等 | 高 |
| ClickHouse | 50w/s | 高 | 低 |
| S3+Parquet | 100w/s | 低 | 极低 |
推荐组合:
- 实时处理:Kafka + ClickHouse
- 冷数据归档:S3 + Athena
2. 批量写入优化
async def batch_insert(self, data_list):
if not data_list:
return
# MongoDB批量插入示例
await self.collection.insert_many(data_list, ordered=False)
# ClickHouse批量插入示例
query = "INSERT INTO products FORMAT JSONEachRow"
async with self.pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(query, data_list)
关键参数:
ordered=False:允许部分失败继续执行- 批量大小:建议每批1000-5000条
- 错误重试:失败批次自动进入死信队列
六、完整代码示例
import aiohttp
import asyncio
from parsel import Selector
import pymongo
from motor.motor_asyncio import AsyncIOMotorClient
class MillionScaleCrawler:
def __init__(self):
self.semaphore = asyncio.Semaphore(500)
self.client = AsyncIOMotorClient('mongodb://localhost:27017')
self.db = self.client['crawler_db']
self.collection = self.db['products']
connector = aiohttp.TCPConnector(limit_per_host=100)
self.session = aiohttp.ClientSession(connector=connector)
async def fetch_url(self, url):
async with self.semaphore:
try:
async with self.session.get(
url,
headers={'User-Agent': 'Mozilla/5.0'},
timeout=30,
compress='gzip'
) as resp:
if resp.status == 200:
return await resp.text()
except Exception:
return None
async def parse_product(self, html, url):
try:
sel = Selector(text=html)
return {
'url': url,
'title': sel.css('h1::text').get().strip(),
'price': sel.css('.price::text').get(),
'timestamp': int(time.time())
}
except:
return None
async def process_url(self, url):
html = await self.fetch_url(url)
if html:
product = await self.parse_product(html, url)
if product:
await self.collection.insert_one(product)
async def run(self, url_list):
tasks = [self.process_url(url) for url in url_list]
await asyncio.gather(*tasks)
async def close(self):
await self.session.close()
self.client.close()
# 使用示例
async def main():
crawler = MillionScaleCrawler()
urls = ['https://example.com/product/{}'.format(i) for i in range(1000000)]
await crawler.run(urls[:1000]) # 测试时先处理1000条
await crawler.close()
if __name__ == '__main__':
asyncio.run(main())
七、常见问题Q&A
Q1:被网站封IP怎么办?
A:立即启用备用代理池,建议使用住宅代理(如站大爷IP代理),配合每请求更换IP策略。更高级方案可采用:
- 流量指纹伪装:修改TLS指纹、HTTP头顺序等
- 行为模拟:随机浏览间隔、鼠标轨迹模拟
- 请求分散:通过CDN节点或云函数中转
Q2:如何处理反爬验证码?
A:分阶段应对:
- 基础验证:自动识别4位数字验证码(使用pytesseract)
- 滑动验证:结合Selenium模拟拖动
- 高级验证:接入第三方打码平台(如超级鹰)
- 终极方案:人工干预通道(当自动识别失败时发送通知)
Q3:数据去重最佳实践?
A:三级过滤机制:
- 布隆过滤器:内存中快速过滤(误判率可控制在0.01%)
- Redis集合:存储最近10万条URL的指纹
- 数据库唯一索引:最终校验(使用
createIndex({url: 1}, {unique: true}))
Q4:如何保证数据完整性?
A:实施"三校两备"制度:
- 采集时校验:响应头Content-Length与实际接收是否一致
- 解析时校验:关键字段非空检查
- 存储时校验:MongoDB文档验证规则
- 本地备份:每日增量备份到S3
- 异地备份:每周全量备份到另一个数据中心
Q5:如何监控爬虫运行状态?
A:建议集成Prometheus+Grafana监控:
-
核心指标:QPS、成功率、错误率、平均响应时间
-
告警规则:
- 连续5分钟错误率>10%
- 队列积压超过10万条
- 代理IP池耗尽
-
可视化看板:实时展示各任务进度
通过这套方案,我们成功为3家电商平台实现日均百万级商品数据采集,单日最高处理量达1270万条。关键在于平衡性能与稳定性,在资源消耗和采集效率间找到最佳甜蜜点。实际部署时建议从每日10万量级开始,逐步增加并发数,通过监控数据持续优化参数配置。