1. 项目概述:千万级电商数据爬取系统设计
这个爬虫项目源自去年我们团队接到的电商价格监控需求,客户需要实时追踪15个主流电商平台的3C类目商品数据,日均数据量在300万条左右。传统单机爬虫在测试阶段就暴露出明显的性能瓶颈——完成一次全量抓取需要近40小时,且频繁遭遇IP封禁。这迫使我们转向分布式架构,最终实现的系统可以稳定维持每秒200+的请求量,数据入库延迟控制在3秒内。
系统核心由四部分组成:(1)基于Scrapy-Redis的任务调度层,实现请求的分布式分配;(2)动态代理池和请求指纹去重机制,保证高可用性;(3)MongoDB分片集群存储,支持水平扩展;(4)可视化监控看板,实时展示爬虫状态和数据质量。特别要说明的是,我们放弃了常见的MySQL方案,因为测试中发现当单表超过500万条记录时,即便加了索引,查询性能也会急剧下降,而MongoDB的文档模型和分片能力完美适配了商品数据半结构化的特性。
重要提示:商业爬虫必须严格遵守robots.txt协议,我们的实践是在代码中集成RobotsParser模块,自动识别并遵守目标网站的爬取限制,同时将请求频率控制在人类浏览的合理范围内(通常每秒不超过2次请求)。
2. 技术架构深度解析
2.1 分布式爬虫核心组件
Scrapy-Redis构成了系统的骨架,其工作原理值得深入剖析。主节点运行的Redis服务器存储着三类关键数据:
- 待爬队列(spider:requests):采用优先级队列存储Request对象
- 请求指纹(spider:dupefilter):Bloom过滤器实现,占用空间仅为传统哈希表的1/8
- 项目缓存(spider:items):临时存储爬取结果,等待持久化
我们在原生架构上做了两处关键改进:
- 动态权重调度算法:根据URL域名自动调整优先级,对反爬严格的网站降低请求密度
- 心跳检测机制:每30秒检查各工作节点状态,自动重启僵死的爬虫进程
配置示例(scrapy_redis默认配置增强版):
python复制# settings.py
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
REDIS_URL = 'redis://:password@master-node:6379/0'
# 自定义配置
SCHEDULER_QUEUE_CLASS = 'project.queue.PriorityQueue'
SCHEDULER_SERIALIZER = "project.picklecompat.PickleSerializer"
STATS_CLASS = "scrapy_redis.stats.RedisStatsCollector"
2.2 MongoDB集群优化实践
面对日均300GB的原始数据,我们设计了如下分片策略:
- 按电商平台分片(shard key=platform)
- 3个配置服务器 + 2个mongos路由 + 6个分片节点(3副本集×2)
- WiredTiger存储引擎,配置snappy压缩
关键参数调优记录:
yaml复制# mongod.conf
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 12 # 建议不超过物理内存60%
journalCompressor: snappy
collectionConfig:
blockCompressor: snappy
indexConfig:
prefixCompression: true
写入性能对比测试结果(单位:ops/sec):
| 并发线程数 | 无索引 | 单字段索引 | 复合索引 |
|---|---|---|---|
| 10 | 8,742 | 7,105 | 6,883 |
| 50 | 9,156 | 8,927 | 8,674 |
| 100 | 9,203 | 9,018 | 8,905 |
经验之谈:MongoDB 4.0+版本的事务功能在爬虫场景要慎用,我们的测试显示开启事务后写入吞吐量下降达40%。对于商品数据这种独立文档,其实不需要强一致性保证。
3. 反爬对抗体系构建
3.1 动态代理池实现
市面上90%的代理服务商我们都有测试,最终选型方案是:
- 国内站点:混用芝麻代理和快代理的独享IP套餐
- 国际站点:Luminati的住宅IP+自建AWS出口IP轮询
代理健康检查算法(Python实现核心逻辑):
python复制def check_proxy(proxy):
try:
start = time.time()
resp = requests.get('https://httpbin.org/ip',
proxies={"http": proxy, "https": proxy},
timeout=10)
latency = time.time() - start
if resp.json().get('origin') in proxy:
return {
'valid': True,
'latency': latency,
'stable': latency < 2.0
}
except Exception:
pass
return {'valid': False}
3.2 请求特征伪装技术
浏览器的真实请求包含上百个特征头,我们通过实测总结了关键参数:
- User-Agent轮频:准备120个常见UA,按权重分配使用频率
- TLS指纹伪装:使用pycurl模拟Chrome的TLS握手特征
- 鼠标轨迹模拟:对关键操作生成贝塞尔曲线轨迹坐标
请求头优化示例:
http复制GET /product/12345 HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Referer: https://www.example.com/category/electronics
Upgrade-Insecure-Requests: 1
X-Forwarded-For: 118.122.123.124
4. 数据存储与处理流水线
4.1 MongoDB Schema设计
商品数据采用灵活但有约束的混合Schema:
python复制{
"_id": ObjectId("5f8d8a7b8c3a1b2c3d4e5f6g"),
"platform": "jd", # 平台标识
"item_id": "100003456789",
"crawl_time": ISODate("2023-05-20T14:32:45Z"),
"basic_info": {
"title": "Apple iPhone 14 Pro Max 256GB",
"price": 9999.00,
"original_price": 10999.00,
"sales": 25000,
"shop": {"name": "Apple官方旗舰店", "type": "self"}
},
"specs": [
{"name": "颜色", "value": "深空黑"},
{"name": "版本", "value": "5G全网通"}
],
"extras": {} # 预留扩展字段
}
索引策略:
- 必须创建:platform+item_id唯一索引
- 推荐创建:basic_info.price范围索引、crawl_time时间索引
- 避免创建:specs数组字段的全索引
4.2 数据清洗流程
原始数据需要经过五步标准化处理:
- 字符编码统一转UTF-8
- 价格字段提取(处理¥1,299.00等格式)
- 规格参数结构化(将"颜色:黑色;内存:256GB"拆解)
- 图片URL规范化(相对路径转绝对URL)
- 敏感信息脱敏(商家联系方式等)
价格清洗正则示例:
python复制import re
def extract_price(text):
pattern = r'[¥¥]?\s*(\d{1,3}(?:,\d{3})*(?:\.\d{2})?)'
match = re.search(pattern, text)
if match:
return float(match.group(1).replace(',', ''))
return None
5. 可视化监控系统实现
5.1 实时看板技术栈
采用Grafana+Prometheus+自定义Exporter方案:
- 爬虫节点通过statsd上报指标
- Prometheus每15秒抓取一次数据
- Grafana配置关键仪表盘:
- 请求成功率/失败率
- 代理IP健康状态
- MongoDB集群负载
- 数据增长趋势
核心监控指标示例:
python复制# 在Spider中埋点
class MySpider(scrapy.Spider):
def __init__(self):
self.stats = statsd.StatsClient('monitor-host', 8125)
def parse(self, response):
self.stats.incr('request.count')
if response.status != 200:
self.stats.incr('request.error')
return
# 解析逻辑...
self.stats.timing('parse.time', time.time() - start_time)
5.2 数据分析案例
价格波动分析SQL(在MongoDB Shell中执行):
javascript复制db.products.aggregate([
{
$match: {
"basic_info.title": /iPhone 14 Pro Max 256GB/,
"crawl_time": {
$gte: ISODate("2023-05-01"),
$lte: ISODate("2023-05-31")
}
}
},
{
$group: {
_id: {
platform: "$platform",
date: { $dateToString: { format: "%Y-%m-%d", date: "$crawl_time" } }
},
avgPrice: { $avg: "$basic_info.price" },
minPrice: { $min: "$basic_info.price" },
maxPrice: { $max: "$basic_info.price" }
}
},
{ $sort: { "_id.date": 1 } }
])
6. 性能调优实战记录
6.1 爬虫节点优化
通过cProfile发现的性能瓶颈及解决方案:
- XPath解析耗时:改用PyQuery后解析速度提升3倍
- 重复DNS查询:启用dnscache模块,缓存时间设为300秒
- 日志I/O阻塞:改用异步日志库logzero,写入吞吐量提升50%
内存优化配置示例:
python复制# settings.py
import gc
gc.set_threshold(700, 10, 10) # 调高GC阈值
# 禁用不需要的中间件
DOWNLOADER_MIDDLEWARES = {
'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100,
# 其他中间件...
}
6.2 MongoDB写入优化
批量插入的黄金法则:
- 理想批次大小:100-500个文档/次
- 使用unordered_insert提升吞吐量
- 合理利用bulk_write操作
实测写入速度对比(单位:万条/分钟):
| 写入方式 | 无索引 | 有索引 |
|---|---|---|
| 单条插入 | 1.2 | 0.8 |
| 批量插入(100) | 8.7 | 6.5 |
| bulk_write | 12.4 | 9.1 |
7. 踩坑经验与故障排查
7.1 典型问题速查表
| 故障现象 | 可能原因 | 解决方案 |
|---|---|---|
| Redis连接数暴涨 | 爬虫异常退出未释放连接 | 增加连接池回收超时设置 |
| MongoDB查询超时 | 未设置最大执行时间 | 添加maxTimeMS参数 |
| 代理IP大量失效 | 目标网站更新了IP检测策略 | 切换代理供应商或调整轮频策略 |
| 数据重复率超过5% | Bloom过滤器内存不足 | 扩大redis内存并重置过滤器 |
| 爬虫节点CPU持续100% | 解析回调陷入死循环 | 添加递归深度保护机制 |
7.2 血泪教训
- 不要相信任何免费代理IP池,我们曾因使用某个知名免费服务导致所有爬虫IP被目标网站批量封禁
- MongoDB的_id默认生成方式(时间戳+机器标识)在分布式环境下可能导致冲突,建议改用UUID
- 永远为每个Request设置errback回调,我们曾因未处理503错误导致丢失重要数据
- 爬虫日志一定要包含完整上下文信息(至少包含URL和时间戳),否则排查问题如同大海捞针
8. 法律合规与道德考量
虽然技术实现很吸引人,但必须强调合规底线:
- 严格遵守目标网站的robots.txt协议
- 设置合理的爬取间隔(建议≥2秒/请求)
- 禁止爬取用户个人信息等敏感数据
- 数据使用范围需获得授权
- 商业用途前务必咨询法律顾问
我们在项目中采取的合规措施:
- 自动识别并遵守robots.txt
- 请求头包含明确的Contact信息
- 数据存储加密处理
- 设置严格的访问权限控制
最后分享一个实用技巧:在开发阶段,可以先用https://httpbin.org/这个服务测试爬虫的各种行为,它能回显所有请求细节,非常适合调试请求头、代理等设置。
