【爬虫框架-0】从一个真实需求说起

前言

当我第一次接到这个需求时,觉得很简单:

每天早上 08:00 自动采集10000条商品数据,采集完成后立即验证数据完整性,然后生成报表推送到业务系统。

用 Scrapy 写个爬虫,加个定时任务,不就搞定了?

但实际执行时我发现了一个致命问题:

plain 复制代码
08:00 - 触发采集任务
08:?? - 采集什么时候结束?
08:?? - 数据验证要等到什么时候才能开始?

这个看似简单的"等待采集完成"的需求,让我重新审视了爬虫框架的设计。


一、问题的本质:爬虫是异步的,但业务流程是同步的

业务流程的期望

我的业务逻辑是一个清晰的流水线:

plain 复制代码
[采集数据] → [验证完整性] → [清洗] → [推送业务系统]

每一步都依赖上一步的完成信号。

爬虫框架的现实

但 Scrapy 这类框架的运行模式是:启动后在后台运行,你不知道它什么时候结束,拿不到执行结果,无法在结束的那一刻触发下一步。

核心矛盾:

  • 爬虫框架是异步执行的(后台运行)
  • 业务流程需要同步等待(等采集完成再继续)

二、两种方案:不同统计时机的选择

你在什么时候统计采集结果?

这个看似简单的问题,决定了整个架构的设计。


方案A:爬虫结束时统计 → 一次性进程模式

核心思路

让爬虫任务"有始有终",结束时自然获得统计数据。

架构流程:

plain 复制代码
外部调度器 → 启动爬虫进程 → 绑定本次URL列表 → 爬虫处理完毕 
→ 进程退出时生成统计 → 外部读取统计文件 → 继续下一步
实现思路

如何统计:

  • Scrapy 自带的 Stats Collector 会记录所有指标(成功数、失败数、耗时等)
  • 爬虫关闭时将统计写入文件或数据库
  • 外部调度器通过"进程退出"这个信号,知道可以读取统计了

如何等待:

  • 使用阻塞式的进程调用(subprocess.run)
  • 进程不退出,调用方就一直等
  • 退出后立即读取统计数据
这个方案的好处
  • 实现极简:不需要 Redis、消息队列等中间件
  • 完成信号明确:进程退出就等于任务完成,不存在模糊判断
  • 统计数据可靠:Scrapy 原生的统计系统已经很完善
  • 任务隔离好:每次都是全新进程,不会互相影响
  • 调试友好:一个任务一个日志,出问题容易定位
但也有明显的问题
  • 启动开销:每次都要初始化框架(加载配置、连接数据库、初始化下载器),可能需要2-5秒
  • 不适合高频任务:如果每小时甚至每10分钟执行一次,频繁启停很浪费
  • 重试逻辑复杂:如果第一次采集失败了200条,需要重新启动进程处理失败URL,状态传递麻烦
  • 分布式实现困难:需要额外设计任务分发机制
适合什么场景
  • 低频定时任务(每天1-2次)
  • 任务边界清晰(明确的开始和结束)
  • 单机或小规模爬虫
  • 对实时性要求不高的场景

方案B:爬虫运行中统计 → 常驻进程模式

核心思路

爬虫永不停止,统计数据在运行过程中持续更新。

架构流程:

plain 复制代码
爬虫进程常驻运行 → 监听任务队列(Redis/RabbitMQ) → 收到任务
→ 开始采集并实时更新统计 → 任务完成标记状态 → 外部轮询状态
→ 发现完成 → 读取统计 → 继续下一步
实现思路的演进

这个方案看起来简单,但实际上有很多细节要处理。我按从简单到复杂的顺序来说。

2.1 初级版本:简单轮询

统计方式:

  • 在 Scrapy 的 Pipeline 或中间件中,每处理一个 item 就更新 Redis 计数器
  • 成功数:redis.incr('task:20241204:success')
  • 失败数:redis.incr('task:20241204:failed')

完成检测:

  • 外部调度器不断轮询 Redis:while redis.get('task:20241204:status') != 'completed'
  • 爬虫在队列为空且等待一段时间后,设置状态为 completed

问题在哪:

  • 完成判断不准确:队列空了不等于真的完成了(可能正在下载、可能有重试任务)
  • 轮询延迟:每10秒查一次,意味着最多10秒的延迟
  • 状态不一致风险:如果爬虫崩溃,Redis 状态可能是错的

2.2 进阶版本:心跳机制

改进思路:

  • 不仅统计数量,还记录"最后更新时间"
  • 爬虫每处理一条数据,就更新心跳:redis.set('task:20241204:heartbeat', now())
  • 外部调度器检测:如果心跳超过 N 秒未更新,且队列为空,则认为完成

这样改进后:

  • 能检测爬虫是否卡死或崩溃
  • 完成判断更可靠(结合心跳+队列状态)

但还是有问题:

  • 参数难调:心跳超时设多久?太短会误判,太长会延迟
  • 仍然是轮询:无法做到"完成瞬间通知"

2.3 高级版本:任务状态机 + 实时计数

核心改进:不再依赖"队列是否为空",而是追踪每个 URL 的状态。

状态机设计:

plain 复制代码
每个 URL 的生命周期:
pending → downloading → success
                      ↓ (失败)
                      → retry_1 → retry_2 → ... → max_retry_exceeded

统计方式:

在 Redis 中维护详细的任务状态:

  • 总任务数:redis.hset('task:20241204', 'total', 10000)
  • 成功数:redis.hset('task:20241204', 'success', 9800)
  • 失败数:redis.hset('task:20241204', 'failed', 200)
  • 重试中:redis.hset('task:20241204', 'retrying', 150)
  • 最终放弃:redis.hset('task:20241204', 'abandoned', 50)

完成判断逻辑:

plain 复制代码
if success + abandoned == total:
    任务真正完成了

关键改进点:

  • 不依赖队列状态:即使队列空了,如果 success + abandoned < total,说明还有任务在重试
  • 精确感知完成:只有当所有 URL 都达到终态(成功或放弃),才算完成
  • 支持复杂重试逻辑:可以清楚知道有多少在重试、重试了几次

实现细节:

  1. 启动任务时:将所有 URL 标记为 pending,记录 total
  2. 每次请求成功:更新 success++,从状态机中移除该 URL
  3. 每次请求失败:检查重试次数
    • 未达上限:标记为 retrying,重新加入队列
    • 达到上限:标记为 abandoned++
  4. 完成检测线程:不断检查 success + abandoned == total

方案B总体来看

这个方案的优势:

  • 资源利用率高:进程复用,不用反复初始化
  • 适合高频任务:如果每小时甚至更频繁,常驻模式更合适
  • 支持分布式:多个爬虫进程从同一队列消费,天然负载均衡
  • 重试逻辑内聚:失败的 URL 直接放回内部队列,不需要重启

但代价也很明显:

  • 架构复杂:需要 Redis、消息队列、状态管理、心跳检测等
  • 状态管理成本高:需要维护详细的任务状态,代码量大

三、真实场景暴露的新问题

先假设上面的方案都可以很快实现, 但是在实际使用中,还有一个致命的业务约束。

问题场景

假设我要采集 10000 条商品数据:

  • 第一轮采集:9800 条成功,200 条失败(网络超时、反爬拦截等)
  • 按照常规流程:标记"完成" → 验证 → 生成报表

但业务要求:

在进入清洗环节前,必须确保采集任务 100% 成功。

如果有失败的 URL,必须立即重试,直到成功或达到最大重试次数。

为什么有这个要求?

因为我的下游是实时数据分析系统:

  • 数据一旦进入清洗,就会被推送到分析系统
  • 分析系统基于"完整数据集"计算指标(价格趋势、库存预警)
  • 如果数据不完整(缺了200条),计算结果就是错的,会触发业务误判

所以,采集阶段必须保证数据完整性。

方案A的困境

如果用一次性进程:

  • 第一次启动:9800成功,200失败
  • 需要重新启动处理失败的200条
  • 如果还有失败...继续启动
  • 可能需要启动3-5次才能100%完成

问题在于:

  • 启动开销累积:每次2秒,5次就是10秒纯浪费
  • 状态传递麻烦:失败的 URL 怎么传给下一次?写文件?存数据库?
  • 重试逻辑分散:一部分在爬虫内,一部分在调度器,难维护

方案B的天然优势

常驻进程 + 运行中统计的架构,天生适合这种场景:

  • 失败的 URL 直接放回爬虫内部队列
  • 无需重启,持续处理直到达到最大重试
  • 重试逻辑完全内聚在爬虫内部

所以对于"必须完整采集"的场景,方案B更合适。


四、方案B的深层挑战:如何精确感知"真正完成"?

使用常驻进程后,新的问题出现了。

完成的定义变复杂了

在有重试机制的情况下,"任务完成"的判断变得模糊:

plain 复制代码
初始队列:10000 个 URL
    ↓
第一轮采集:9800 成功,200 失败 → 200个自动重试
    ↓
第二轮采集:180 成功,20 失败 → 20个继续重试
    ↓
第三轮采集:15 成功,5 失败 → 5个达到最大重试次数
    ↓
最终:9995 成功,5 彻底失败
    ↓
现在才算"真正完成"

这就回到了最初的问题:我怎么知道它"真的"完成了?

梦回同步爬虫。


五、第三种可能:调度器实时获取爬虫状态

那么,有没有一种框架,能同步获取到发布后的任务结果、是否成功等状态,同时消费者依然能自定义消费速度、并发数?

换句话说:生产者能获取消费者的结果,但同时消费者又是解耦的?

我想要的理想状态

plain 复制代码
调度器提交任务 → 同步等待 → 爬虫持续运行并实时反馈统计
→ 所有URL达到终态 → 立即返回结果 → 开始下一步

有!

下篇续。

相关推荐
月光技术杂谈5 小时前
基于Python+Selenium的淘宝商品信息智能采集实践:从浏览器控制到反爬应对
爬虫·python·selenium·自动化·web·电商·淘宝
sugar椰子皮6 小时前
【爬虫框架-2】funspider架构
爬虫·python·架构
APIshop9 小时前
用“爬虫”思路做淘宝 API 接口测试:从申请 Key 到 Python 自动化脚本
爬虫·python·自动化
xinxinhenmeihao1 天前
爬虫如何使用代理IP才能不被封号?有什么解决方案?
爬虫·网络协议·tcp/ip
2501_938810111 天前
什么IP 适用爬虫 采集相关业务
爬虫·网络协议·tcp/ip
第二只羽毛2 天前
主题爬虫采集主题新闻信息
大数据·爬虫·python·网络爬虫
0***h9422 天前
初级爬虫实战——麻省理工学院新闻
爬虫
是有头发的程序猿2 天前
Python爬虫实战:面向对象编程在淘宝商品数据抓取中的应用
开发语言·爬虫·python
Onebound_Ed2 天前
Python爬虫进阶:面向对象设计构建高可维护的1688商品数据采集系统
开发语言·爬虫·python