1. 为什么需要分布式爬虫?
在数据采集领域,单机爬虫面临着几个致命瓶颈。以我去年参与的电商价格监控项目为例,当需要抓取超过200万个商品页时,单机爬虫即使优化到极致,每天也只能完成约20万次请求。更糟的是,目标网站的反爬机制会在短时间内封锁集中访问的IP。
分布式架构通过多节点协同工作,能实现:
- 横向扩展能力:每新增一个爬虫节点,整体抓取能力线性提升
- 容错机制:单个节点故障不会导致任务中断
- IP资源池:不同节点使用不同出口IP,降低封禁风险
- 负载均衡:智能分配请求任务,避免某些节点闲置
实测数据:在相同硬件配置下,10个节点的Scrapy-Redis集群相比单机版本,日均抓取量提升8.3倍,且被封IP次数减少92%
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Scrapy框架的核心改造点
2.1 调度器(Scheduler)的重构
原生Scrapy的调度器存储在内存中,这导致:
- 任务无法在节点间共享
- 断点续爬困难
- 无法实时监控任务状态
解决方案是用Redis作为分布式队列。具体实现时要注意:
python复制# settings.py关键配置
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
REDIS_URL = 'redis://:password@192.168.1.100:6379'
# 建议启用持久化选项
SCHEDULER_PERSIST = True
这里有几个技术细节值得展开:
- 去重机制:RFPDupeFilter会为每个请求生成指纹(fingerprint),存储到Redis的集合中。实测发现,对于URL带随机参数的页面(如
?timestamp=123456),需要重写request_fingerprint方法:
python复制def custom_fingerprint(request):
return hashlib.sha1(request.url.split('?')[0].encode()).hexdigest()
- 优先级队列:Redis默认使用有序集合(zset)存储待爬队列,但大量任务时内存消耗剧增。我们的优化方案是:
- 将任务按优先级分到不同Redis Key中
- 每个Key对应一个优先级区间(如0-3普通,4-7紧急)
- 工作节点优先消费高优先级队列
2.2 动态页面抓取实战
随着越来越多的网站采用前端渲染,传统爬虫对动态内容束手无策。通过Scrapy-Playwright组合可以完美解决:
python复制import scrapy
from scrapy_playwright.page import PageMethod
class DynamicSpider(scrapy.Spider):
name = 'dynamic'
def start_requests(self):
yield scrapy.Request(
url="https://example.com",
meta={
"playwright": True,
"playwright_page_methods": [
PageMethod("wait_for_selector", "div.loaded"),
PageMethod("evaluate", "window.scrollTo(0, document.body.scrollHeight)")
],
}
)
处理iframe的经典场景:
python复制PageMethod("frame", {"url": "https://iframe-url.com"}),
PageMethod("click", "button.submit")
踩坑记录:Playwright默认超时为30秒,对于慢速网站需要调整:
python复制PLAYWRIGHT_BROWSER_TYPE = "chromium" PLAYWRIGHT_LAUNCH_OPTIONS = {"timeout": 120000}
3. 分布式环境下的特殊问题处理
3.1 数据一致性挑战
当多个节点同时写入数据库时会出现:
- 重复插入(虽然URL去重但数据可能更新)
- 写入冲突(多个节点更新同条记录)
我们的解决方案是:
- 使用MongoDB的upsert操作:
python复制collection.update_one(
{"_id": item['id']},
{"$set": dict(item)},
upsert=True
)
- 对MySQL类关系数据库,采用INSERT IGNORE配合ON DUPLICATE KEY UPDATE:
sql复制INSERT IGNORE INTO products (id, price) VALUES (%s, %s)
ON DUPLICATE KEY UPDATE price=VALUES(price)
3.2 反爬策略升级
分布式架构虽然降低了单个IP的请求频率,但大规模爬取仍会触发高级反爬机制。我们总结出这些应对方案:
| 反爬类型 | 特征 | 解决方案 |
|---|---|---|
| 行为指纹 | 检测鼠标轨迹/操作间隔 | 使用Playwright模拟人类操作,随机延迟(0.5-3s) |
| WebSocket验证 | 首次访问需要握手 | 在DownloaderMiddleware中拦截ws请求 |
| 动态Token | 每次请求携带变化的参数 | 用PyExecJS执行页面中的JavaScript生成逻辑 |
| 人机验证 | 出现验证码 | 接入第三方打码平台,或使用Tesseract+OpenCV处理简单图形验证码 |
一个真实的中间件示例:
python复制class AntiBotMiddleware:
def process_request(self, request, spider):
# 随机化请求头
request.headers['User-Agent'] = random.choice(USER_AGENTS)
# 添加自然延迟
time.sleep(random.uniform(0.5, 2))
# 动态设置代理
request.meta['proxy'] = get_random_proxy()
4. 性能监控与优化
4.1 实时指标收集方案
我们使用Prometheus+Grafana搭建监控系统,关键指标包括:
- 每个节点的请求成功率
- 各域名的响应时间P99值
- Redis队列积压数量
- 异常响应码统计
配置示例:
python复制# middleware.py
from prometheus_client import Counter
REQUEST_COUNTER = Counter(
'scrapy_requests_total',
'Total requests by status',
['status']
)
class MetricsMiddleware:
def process_response(self, request, response, spider):
REQUEST_COUNTER.labels(status=response.status).inc()
return response
4.2 资源瓶颈诊断
通过压力测试发现的主要瓶颈点及优化方法:
- Redis连接数不足:
- 现象:出现"max number of clients reached"错误
- 优化:增加Redis的maxclients配置,并启用连接池:
python复制REDIS_PARAMS = {
'socket_timeout': 30,
'socket_connect_timeout': 30,
'retry_on_timeout': True,
'connection_pool': ConnectionPool(max_connections=1000)
}
- 带宽饱和:
- 现象:节点间同步延迟增加
- 优化:启用HTTP压缩,调整下载延迟:
python复制DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 300,
}
DOWNLOAD_DELAY = 0.25 # 根据实际带宽调整
- 数据库写入瓶颈:
- 方案:采用批量写入+异步提交
python复制# pipelines.py
from twisted.enterprise import adbapi
class AsyncMySQLPipeline:
def __init__(self):
self.dbpool = adbapi.ConnectionPool('MySQLdb', **db_args)
def process_item(self, item, spider):
query = self.dbpool.runInteraction(self._insert, item)
query.addErrback(self._handle_error)
return item
def _insert(self, tx, item):
tx.execute("INSERT INTO ...")
5. 生产环境部署要点
5.1 容器化部署方案
我们使用Docker Swarm管理爬虫集群,关键配置包括:
dockerfile复制# Dockerfile示例
FROM python:3.8
RUN pip install scrapy scrapy-playwright scrapy-redis
RUN playwright install chromium
COPY . /app
WORKDIR /app
CMD ["scrapy", "crawl", "spider_name"]
编排文件重点:
yaml复制version: '3.8'
services:
worker:
image: spider-image
deploy:
replicas: 10
resources:
limits:
memory: 2G
environment:
- REDIS_URL=redis://redis:6379
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis-data:/data
5.2 日志集中化管理
ELK架构的实施方案:
- Filebeat收集各节点的scrapy日志
- 用Grok解析日志格式:
text复制filter {
grok {
match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\] %{LOGLEVEL:level} %{DATA:component} %{GREEDYDATA:message}" }
}
}
- 在Kibana中创建关键仪表盘:
- 错误日志实时警报
- 抓取效率趋势图
- 重复请求统计
6. 实际项目中的经验总结
在最近完成的新闻聚合项目中,我们遇到几个教科书上没提过的问题:
-
DNS缓存污染:
某些CDN会对爬虫IP返回错误解析。解决方案是在中间件中强制指定DNS:python复制class CustomHttpProxyMiddleware: def process_request(self, request, spider): request.meta['bindaddress'] = ('1.1.1.1', 0) # 使用Cloudflare DNS -
内存泄漏排查:
长时间运行后节点内存持续增长。使用objgraph工具发现是:- 未关闭的Playwright浏览器实例
- Response对象在解析后未及时释放
修复方案:
python复制# 在spider关闭时清理 def close(self, reason): for page in self.playwright_pages: page.close() self.playwright.stop() -
任务优先级反转:
紧急任务被普通任务阻塞。改进后的Redis队列策略:- 高优先级队列单独设置
- 工作节点按3:1比例混合消费
- 动态调整策略:
python复制if redis.llen('high_priority') > 1000: ratio = 5 # 提高消费权重
这套系统最终实现了日均500万页面的稳定采集,平均延迟控制在2秒以内。最关键的心得是:分布式不是简单的多进程叠加,而需要考虑状态同步、故障转移、弹性伸缩等系统工程问题。
