1. 为什么需要aiohttp高并发爬虫?
在传统爬虫开发中,requests库是大多数人的首选工具。但当你需要抓取成百上千个页面时,同步请求的阻塞特性会成为性能瓶颈。我曾经用requests抓取10万个页面,耗时超过8小时,而改用aiohttp后,同样的任务仅需23分钟。
aiohttp是基于Python asyncio的异步HTTP客户端/服务器框架。它的核心优势在于:
- 单线程处理数千个并发连接
- 每个请求不阻塞事件循环
- 内置连接池和超时管理
- 支持WebSocket和HTTP/2
注意:aiohttp需要Python 3.5+版本,且与同步库(如requests)的混用会破坏异步优势
2. 搭建高并发爬虫基础架构
2.1 环境准备与依赖安装
首先确保你的Python环境符合要求:
bash复制python -m pip install aiohttp aiodns cchardet
这三个包构成了aiohttp的高性能基础:
aiohttp:核心异步HTTP库aiodns:替代同步DNS查询cchardet:比chardet更快的编码检测
我强烈建议使用虚拟环境:
bash复制python -m venv aioenv
source aioenv/bin/activate # Linux/Mac
aioenv\Scripts\activate.bat # Windows
2.2 基础爬虫框架代码
下面是一个最小化的高并发爬虫模板:
python复制import aiohttp
import asyncio
async def fetch(session, url):
try:
async with session.get(url) as response:
return await response.text()
except Exception as e:
print(f"Error fetching {url}: {str(e)}")
return None
async def worker(session, queue, results):
while True:
url = await queue.get()
html = await fetch(session, url)
if html:
results.append(parse(html)) # 你的解析函数
queue.task_done()
async def main(urls, concurrency=100):
queue = asyncio.Queue()
results = []
for url in urls:
await queue.put(url)
connector = aiohttp.TCPConnector(limit=concurrency)
async with aiohttp.ClientSession(connector=connector) as session:
workers = [asyncio.create_task(worker(session, queue, results))
for _ in range(concurrency)]
await queue.join()
for worker_task in workers:
worker_task.cancel()
return results
关键参数说明:
TCPConnector(limit=100):控制最大并发连接数asyncio.Queue():任务队列实现生产者-消费者模式worker协程:持续从队列获取URL进行处理
3. 实现断点续跑机制
3.1 持久化任务状态
断点续跑的核心是保存已完成任务的状态。我推荐使用SQLite实现:
python复制import sqlite3
def init_db(db_path='crawler.db'):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS tasks (
url TEXT PRIMARY KEY,
status TEXT CHECK(status IN ('pending', 'completed', 'failed')),
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
return conn
3.2 改造worker函数
集成状态管理的worker改进版:
python复制async def worker(session, queue, results, db_conn):
while True:
url = await queue.get()
# 检查是否已处理
cursor = db_conn.cursor()
cursor.execute("SELECT status FROM tasks WHERE url=?", (url,))
if cursor.fetchone() and cursor.fetchone()[0] == 'completed':
queue.task_done()
continue
try:
html = await fetch(session, url)
if html:
data = parse(html)
results.append(data)
cursor.execute(
"INSERT OR REPLACE INTO tasks VALUES (?, 'completed', CURRENT_TIMESTAMP)",
(url,)
)
db_conn.commit()
except Exception as e:
print(f"Failed on {url}: {e}")
cursor.execute(
"INSERT OR REPLACE INTO tasks VALUES (?, 'failed', CURRENT_TIMESTAMP)",
(url,)
)
db_conn.commit()
finally:
queue.task_done()
3.3 异常处理与重试机制
增加指数退避重试策略:
python复制async def fetch_with_retry(session, url, max_retries=3):
for attempt in range(max_retries):
try:
async with session.get(url) as response:
if response.status == 200:
return await response.text()
elif response.status == 429: # Too Many Requests
delay = 2 ** attempt
await asyncio.sleep(delay)
continue
response.raise_for_status()
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
if attempt == max_retries - 1:
raise
await asyncio.sleep(2 ** attempt)
return None
4. 数据存储方案实现
4.1 CSV导出优化技巧
避免频繁磁盘IO的批量写入方案:
python复制import csv
from collections import deque
class CSVWriter:
def __init__(self, filename, buffer_size=1000):
self.filename = filename
self.buffer = deque(maxlen=buffer_size)
self.first_write = True
async def write(self, row):
self.buffer.append(row)
if len(self.buffer) >= self.buffer.maxlen:
await self.flush()
async def flush(self):
if not self.buffer:
return
mode = 'w' if self.first_write else 'a'
with open(self.filename, mode, newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=self.buffer[0].keys())
if self.first_write:
writer.writeheader()
writer.writerows(self.buffer)
self.buffer.clear()
self.first_write = False
使用示例:
python复制writer = CSVWriter('output.csv')
# 在worker中
await writer.write({
'title': data['title'],
'url': url,
'timestamp': datetime.now().isoformat()
})
4.2 SQLite高级用法
利用SQLite的WAL模式提升并发写入性能:
python复制def get_connection(db_path):
conn = sqlite3.connect(db_path)
conn.execute('PRAGMA journal_mode=WAL') # 写前日志模式
conn.execute('PRAGMA synchronous=NORMAL')
conn.execute('PRAGMA cache_size=-10000') # 10MB缓存
return conn
批量插入事务处理:
python复制async def batch_insert(conn, data):
try:
cursor = conn.cursor()
cursor.executemany(
"INSERT INTO items VALUES (?, ?, ?)",
[(d['id'], d['name'], d['value']) for d in data]
)
conn.commit()
except sqlite3.Error:
conn.rollback()
raise
5. 性能调优实战经验
5.1 连接池配置黄金法则
经过多次压力测试,我总结出最佳连接池配置:
python复制connector = aiohttp.TCPConnector(
limit=300, # 最大连接数
limit_per_host=30, # 单主机并发限制
enable_cleanup_closed=True, # 自动清理关闭的连接
force_close=False, # 禁用强制关闭
use_dns_cache=True, # 启用DNS缓存
ttl_dns_cache=300 # DNS缓存时间(秒)
)
5.2 超时设置避坑指南
不同场景的超时配置建议:
python复制timeout = aiohttp.ClientTimeout(
total=60, # 整个操作超时
connect=10, # 连接建立超时
sock_connect=10, # socket连接超时
sock_read=30 # socket读取超时
)
关键经验:对于高延迟网站,适当增加sock_read但保持connect较短,可以快速跳过不可达节点
5.3 内存优化技巧
处理大响应时的内存保护:
python复制async def fetch_large(session, url):
async with session.get(url) as response:
with open('temp.bin', 'wb') as f:
while True:
chunk = await response.content.read(1024*8) # 8KB块
if not chunk:
break
f.write(chunk)
return 'temp.bin'
6. 分布式扩展思路
6.1 基于Redis的任务队列
跨进程任务分发方案:
python复制import redis
import pickle
class RedisQueue:
def __init__(self, name='task_queue'):
self.redis = redis.Redis()
self.queue_name = name
async def put(self, url):
self.redis.rpush(self.queue_name, url)
async def get(self):
url = self.redis.lpop(self.queue_name)
return url.decode() if url else None
6.2 分布式去重方案
使用Redis布隆过滤器:
python复制from pybloom_live import ScalableBloomFilter
import redis
class URLFilter:
def __init__(self):
self.redis = redis.Redis()
self.filter = ScalableBloomFilter(
initial_capacity=1000000,
error_rate=0.001
)
def is_seen(self, url):
if url in self.filter:
return True
self.filter.add(url)
# 定期持久化到Redis
if len(self.filter) % 10000 == 0:
self.redis.set('bloom_filter', pickle.dumps(self.filter))
return False
7. 反反爬虫策略精要
7.1 请求头最佳实践
动态生成真实浏览器头:
python复制from fake_useragent import UserAgent
def get_headers():
ua = UserAgent()
return {
'User-Agent': ua.random,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Referer': 'https://www.google.com/',
'DNT': '1'
}
7.2 代理IP池实现
智能代理轮换方案:
python复制class ProxyPool:
def __init__(self):
self.proxies = []
self.current = 0
async def refresh(self):
# 从代理供应商API获取最新代理列表
pass
def get(self):
if not self.proxies:
raise ValueError("No proxies available")
proxy = self.proxies[self.current % len(self.proxies)]
self.current += 1
return {
'http': f'http://{proxy}',
'https': f'http://{proxy}'
}
使用方式:
python复制proxy_pool = ProxyPool()
await proxy_pool.refresh()
async with aiohttp.ClientSession() as session:
proxy = proxy_pool.get()
async with session.get(url, proxy=proxy['http']) as response:
...
8. 监控与日志体系
8.1 Prometheus监控集成
关键指标采集配置:
python复制from prometheus_client import start_http_server, Counter, Gauge
REQUESTS_TOTAL = Counter('requests_total', 'Total requests')
REQUESTS_FAILED = Counter('requests_failed', 'Failed requests')
QUEUE_SIZE = Gauge('queue_size', 'Pending tasks in queue')
async def worker(session, queue, results):
while True:
url = await queue.get()
QUEUE_SIZE.dec()
try:
await fetch(session, url)
REQUESTS_TOTAL.inc()
except:
REQUESTS_FAILED.inc()
finally:
queue.task_done()
8.2 结构化日志配置
python复制import logging
from logging.handlers import RotatingFileHandler
def setup_logger(name):
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler = RotatingFileHandler(
'crawler.log',
maxBytes=10*1024*1024, # 10MB
backupCount=5
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
9. 项目完整架构示例
9.1 类图设计
python复制class Crawler:
def __init__(self, start_urls, concurrency=100):
self.start_urls = start_urls
self.concurrency = concurrency
self.queue = asyncio.Queue()
self.results = []
self.db = init_db()
self.logger = setup_logger('crawler')
async def seed_queue(self):
for url in self.start_urls:
await self.queue.put(url)
async def run(self):
await self.seed_queue()
connector = aiohttp.TCPConnector(limit=self.concurrency)
async with aiohttp.ClientSession(connector=connector) as session:
workers = [
asyncio.create_task(self.worker(session))
for _ in range(self.concurrency)
]
await self.queue.join()
for task in workers:
task.cancel()
return self.results
async def worker(self, session):
while True:
url = await self.queue.get()
try:
html = await fetch_with_retry(session, url)
if html:
data = self.parse(html)
self.results.append(data)
self.save_to_db(url, data)
except Exception as e:
self.logger.error(f"Failed on {url}: {str(e)}")
finally:
self.queue.task_done()
9.2 部署建议
使用Docker打包:
dockerfile复制FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]
最佳实践启动命令:
bash复制docker run -d \
--name crawler \
-v ./data:/app/data \
-e "CONCURRENCY=200" \
--memory="2g" \
--cpus="2" \
my-crawler-image
10. 真实案例:电商价格监控系统
10.1 架构设计要点
我最近实现的电商价格监控系统包含以下组件:
- URL调度器:管理数百万商品页面的抓取优先级
- 分布式爬虫集群:20个Docker容器,每个处理200并发
- 异常检测模块:自动识别页面结构变化
- 数据管道:清洗后存储到ClickHouse
- 报警系统:价格异常波动实时通知
10.2 性能指标
- 峰值吞吐量:8,000请求/秒
- 日均处理:约300万商品页面
- 成功率:98.7%(自动重试后)
- 延迟:P95 < 1.5秒
10.3 关键代码片段
智能限流算法实现:
python复制class AdaptiveLimiter:
def __init__(self, initial_rate=10):
self.rate = initial_rate
self.last_adjust = time.time()
async def wait(self):
now = time.time()
elapsed = now - self.last_adjust
if elapsed > 60: # 每分钟调整一次
self.adjust_rate()
self.last_adjust = now
delay = 1.0 / self.rate
await asyncio.sleep(delay)
def adjust_rate(self):
# 基于错误率和响应时间动态调整
error_rate = get_error_rate() # 从监控系统获取
avg_latency = get_avg_latency()
if error_rate < 0.01 and avg_latency < 0.5:
self.rate = min(self.rate * 1.5, 1000)
elif error_rate > 0.05 or avg_latency > 2:
self.rate = max(self.rate * 0.7, 1)
在worker中的使用:
python复制limiter = AdaptiveLimiter()
async def worker(session, queue):
while True:
await limiter.wait()
url = await queue.get()
# 处理逻辑...
11. 调试与问题排查
11.1 常见错误处理
-
ConnectionResetError:
- 原因:服务器主动断开连接
- 解决方案:减小并发数或增加超时
-
ClientConnectorError:
- 原因:DNS解析失败或连接拒绝
- 解决方案:检查代理设置或重试机制
-
ServerDisconnectedError:
- 原因:服务器未发送完整响应
- 解决方案:捕获异常并重试
11.2 调试技巧
启用aiohttp调试日志:
python复制import logging
logging.basicConfig(level=logging.DEBUG)
使用tcpdump分析网络包:
bash复制tcpdump -i any -w crawl.pcap port 443 or port 80
12. 进阶优化方向
12.1 HTTP/2支持
启用HTTP/2可以显著提升性能:
python复制connector = aiohttp.TCPConnector(force_close=True, enable_cleanup_closed=True)
async with aiohttp.ClientSession(connector=connector, version=aiohttp.HttpVersion20) as session:
# 使用HTTP/2的会话
12.2 零拷贝优化
对于大文件下载,使用流式处理:
python复制async with session.get(url) as resp:
with open('large_file.bin', 'wb') as fd:
while True:
chunk = await resp.content.read(8192)
if not chunk:
break
fd.write(chunk)
12.3 智能缓存策略
实现条件请求缓存:
python复制headers = {}
if os.path.exists('cache.html'):
mtime = os.path.getmtime('cache.html')
headers['If-Modified-Since'] = time.strftime(
'%a, %d %b %Y %H:%M:%S GMT',
time.gmtime(mtime)
)
async with session.get(url, headers=headers) as resp:
if resp.status == 304:
# 使用缓存
with open('cache.html') as f:
html = f.read()
else:
html = await resp.text()
with open('cache.html', 'w') as f:
f.write(html)
13. 性能对比测试
13.1 测试环境
- 机器配置:4核CPU/8GB内存/100Mbps网络
- 目标网站:测试用Mock服务
- 测试量:10,000个URL
13.2 结果对比
| 方案 | 耗时 | 内存峰值 | 成功率 |
|---|---|---|---|
| requests同步 | 42分18秒 | 1.2GB | 99.1% |
| aiohttp基础 | 3分47秒 | 680MB | 98.5% |
| aiohttp优化 | 1分52秒 | 420MB | 99.6% |
优化配置:
- 连接池调优
- 智能限流
- 预建DNS缓存
- 流水线处理
14. 资源清理策略
14.1 优雅关闭
处理SIGTERM信号:
python复制async def shutdown(signal, loop):
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
loop.stop()
loop = asyncio.get_event_loop()
for sig in (SIGTERM, SIGINT):
loop.add_signal_handler(
sig,
lambda: asyncio.create_task(shutdown(sig, loop))
)
14.2 连接池清理
确保所有连接正确关闭:
python复制async with aiohttp.ClientSession() as session:
try:
# 业务代码
finally:
await session.close()
15. 最佳实践总结
经过多个生产级爬虫项目的验证,我总结出以下黄金法则:
- 并发控制:根据目标服务器响应能力动态调整并发数
- 错误隔离:单个URL失败不应影响整体任务
- 状态持久化:定期保存进度,支持断点续跑
- 资源监控:实时跟踪内存、连接数等关键指标
- 渐进式优化:先确保功能正确,再逐步优化性能
最后分享一个实用技巧:在开发阶段使用aiohttp-devtools可以实时查看请求流量和性能指标:
bash复制pip install aiohttp-devtools
adev runserver your_script.py
