1. 项目概述:当爬虫工程遇上解耦设计
第一次看到"数据工厂"这个概念时,我正被一个电商爬虫项目折磨得焦头烂额。那是个需要同时处理商品详情、用户评价、价格波动的复杂系统,各种回调函数嵌套了七八层,新增一个数据字段就要改动五六个文件。直到把DAG(有向无环图)和解耦思想引入爬虫架构,才真正体会到什么叫"流水线作业"的美妙。
这个mini-DAG系统的核心在于用"数据流"代替"控制流"。传统爬虫像是个事必躬亲的管家,从发送请求到存储数据全程参与;而我们的设计让每个环节成为独立车间——URL调度车间只负责生产任务,下载车间专注获取原始数据,解析车间就像流水线上的质检员,存储车间则是最终包装工。各车间通过消息队列连接,用DAG定义生产流程,这样增加新的数据类型就像在流水线上新增一个工位那么简单。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心架构设计
2.1 解耦四层模型
我们的架构像俄罗斯套娃一样分为四层:
- 调度层:使用Redis的ZSET实现优先级队列,记录示例:
python复制def add_url(self, url, priority=5):
""" 添加URL到待爬队列 """
pipe = self.redis.pipeline()
pipe.zadd(self.waiting_key, {url: -priority}) # 负数实现最小堆
pipe.zincrby(self.counter_key, 1, 'total')
pipe.execute()
- 下载层:基于aiohttp的异步下载器集群,关键配置:
python复制async def fetch(self, session, url):
try:
async with session.get(url,
timeout=ClientTimeout(total=30),
headers=self.rotating_headers) as resp:
return await resp.text()
except Exception as e:
self.logger.error(f"下载失败 {url}: {str(e)}")
raise
- 解析层:采用插件式设计,每个网站对应一个解析类:
python复制class ProductParser(AbstractParser):
@classmethod
def is_match(cls, url):
return 'product/detail' in url
def extract(self, html):
return {
'title': xpath(html, '//h1/text()'),
'price': regex(html, r'"price":"(\d+\.\d+)"')
}
- 存储层:通过抽象接口支持多种数据库:
python复制class StoragePipeline:
def __init__(self):
self.writers = {
'mysql': MySQLWriter(),
'csv': CSVWriter(),
'es': ElasticsearchWriter()
}
def write(self, data_type, items):
for writer in self.writers.values():
writer.bulk_write(data_type, items)
2.2 DAG任务编排
我们用Python的字典定义DAG结构,示例配置:
python复制product_flow = {
'nodes': {
'list_page': {
'task': 'crawl',
'config': {'depth': 3, 'throttle': 1.0}
},
'detail_page': {
'depends_on': ['list_page'],
'task': 'parse',
'parser': 'ProductParser'
},
'image_download': {
'depends_on': ['detail_page'],
'task': 'download',
'type': 'image'
}
}
}
调度器会根据依赖关系自动拓扑排序,遇到失败节点会自动重试上游依赖。实测在抓取10万级商品数据时,这种设计比传统爬虫节省约40%的重复请求。
3. 流式处理实现
3.1 数据流引擎
核心是使用asyncio.Queue实现的生产者-消费者模型:
python复制async def data_pipeline():
download_queue = asyncio.Queue(maxsize=1000)
parse_queue = asyncio.Queue(maxsize=2000)
# 启动工作协程
downloaders = [asyncio.create_task(download_worker(download_queue, parse_queue))
for _ in range(10)]
parsers = [asyncio.create_task(parse_worker(parse_queue))
for _ in range(4)]
# 注入初始任务
for url in seed_urls:
await download_queue.put(('list_page', url))
# 等待任务完成
await download_queue.join()
await parse_queue.join()
# 清理工作协程
for worker in downloaders + parsers:
worker.cancel()
3.2 断点续传设计
通过Redis记录任务状态:
- 使用HASH存储已完成的URL指纹
- 用ZSET保存进行中的任务及重试次数
- 定期将队列状态持久化到磁盘
恢复时优先处理进行中任务:
python复制def recover_pipeline():
# 从Redis恢复队列
retrying = redis.zrangebyscore('retry_queue', 0, time.time())
in_progress = redis.zrange('progress_queue', 0, -1)
# 去重处理
done_set = redis.hgetall('done_set')
return [url for url in retrying + in_progress
if url not in done_set]
4. 实战避坑指南
4.1 反爬对抗策略
我们采用分层防御方案:
-
基础层:
- 动态User-Agent池(200+常用UA)
- 代理IP自动切换(验证成功率>95%才使用)
- 请求间隔随机化(0.5-3秒正态分布)
-
高级层:
- 浏览器指纹模拟(通过selenium-wire注入JS)
- TLS指纹混淆(使用curl_cffi库)
- 行为模式模拟(鼠标移动轨迹/页面停留时间)
-
应急方案:
python复制def fallback_parse(html): if '验证码' in html: trigger_captcha_solver() elif 'IP被封' in html: switch_proxy_group() else: send_alert_notification()
4.2 性能优化技巧
-
内存控制:
- 使用生成器逐步yield数据
- 限制单个任务最大内存用量:
python复制import resource resource.setrlimit(resource.RLIMIT_AS, (1_000_000_000, 1_000_000_000)) -
IO优化:
- 合并小文件存储(每1000条记录批量写入)
- 使用zstd压缩传输数据(比gzip节省30%流量)
-
错误隔离:
python复制async def safe_task(task_func, *args): try: return await task_func(*args) except Exception as e: log_exception(e) return None # 不阻断整个流程
5. 扩展应用场景
5.1 实时监控系统
通过添加SSE(Server-Sent Events)接口,实现爬虫状态实时可视化:
python复制async def status_stream(request):
response = web.StreamResponse()
await response.prepare(request)
async for update in redis.pubsub.listen('crawler:status'):
if update['type'] == 'message':
await response.write(f"data: {update['data']}\n\n".encode())
return response
5.2 智能调度系统
结合机器学习预测最优爬取策略:
- 用时间序列预测网站负载低谷期
- 基于历史数据动态调整请求间隔
- 自动识别网站改版(DOM结构变化检测)
python复制class SmartScheduler:
def predict_optimal_time(self, domain):
model = self.load_model(domain)
features = self.collect_features()
return model.predict(features)
这套系统在我参与的多个项目中得到验证,最典型的案例是:
- 某电商价格监控项目:日均处理200万页面,错误率<0.5%
- 新闻聚合平台:支持50+网站同时抓取,新增站点配置时间<1小时
- 社交媒体舆情分析:实现分钟级延迟的热点发现
最后分享一个调试技巧:在开发DAG流程时,可以用graphviz可视化任务依赖关系,这对排查复杂流程的阻塞问题特别有用。保存为dot文件后,一行命令就能生成流程图:
bash复制dot -Tpng workflow.dot -o workflow.png
