## 1. 断点续爬的核心价值与实现逻辑 爬虫任务最怕什么?不是被封IP,不是验证码,而是跑了8小时突然断电/断网,第二天发现要从头开始。断点续爬就是让爬虫具备"记忆功能"的关键技术,它能精确记录任务进度,在意外中断后从断点继续执行,而不是傻乎乎地重跑整个流程。 实现断点续爬需要解决三个核心问题: 1. **任务状态持久化**:将正在执行的URL、已完成的URL、失败的URL等状态信息实时保存到数据库或文件 2. **失败队列重试机制**:对请求失败的URL进行分级管理(如瞬时错误重试、永久错误丢弃) 3. **数据去重判断**:确保续爬时不会重复处理已经抓取过的URL > 提示:断点续爬不是简单的"从上次停止的URL继续",而是需要维护完整的任务上下文状态。就像游戏存档不仅要记录关卡进度,还要保存角色装备、技能点等完整信息。 ## 2. 任务状态管理的四种实现方案 ### 2.1 基于内存的轻量级方案(适合小规模爬虫) ```python # 使用Python字典保存状态(易失性,重启后丢失) task_status = { 'pending': ['url1', 'url2'], # 待抓取队列 'success': ['url3', 'url4'], # 成功队列 'failed': {'url5': 'timeout'} # 失败队列及原因 }2.2 基于SQLite的本地持久化方案
import sqlite3 conn = sqlite3.connect('crawler_state.db') cursor = conn.cursor() cursor.execute('''CREATE TABLE IF NOT EXISTS task_status (url TEXT PRIMARY KEY, status TEXT, retry_count INT)''')2.3 基于Redis的高性能方案
import redis r = redis.Redis() # 使用Redis集合存储不同状态URL r.sadd('pending', 'url1') r.smove('pending', 'success', 'url1') # 状态转移2.4 完整的状态机设计(生产级推荐)
class TaskStateMachine: STATUS_PENDING = 'pending' STATUS_FETCHING = 'fetching' STATUS_SUCCESS = 'success' STATUS_FAILED = 'failed' def __init__(self): self.transitions = { self.STATUS_PENDING: [self.STATUS_FETCHING], self.STATUS_FETCHING: [self.STATUS_SUCCESS, self.STATUS_FAILED], self.STATUS_FAILED: [self.STATUS_PENDING] # 允许重试 }3. 失败队列的智能重试策略
3.1 错误分类与处理原则
| 错误类型 | 特征 | 处理建议 | 重试间隔 |
|---|---|---|---|
| 网络瞬时错误 | ConnectionError | 立即重试3次 | 0秒 |
| 服务器限流 | 429状态码 | 指数退避重试 | 2^n秒 |
| 页面不存在 | 404状态码 | 直接丢弃 | - |
| 反爬拦截 | 403状态码 | 更换代理/IP后重试 | 随机5-10秒 |
3.2 实现带优先级的重试队列
from queue import PriorityQueue retry_queue = PriorityQueue() # 错误优先级 = 基础权重 + 重试次数 * 衰减系数 def calculate_priority(error_type, retry_count): weights = { 'network': 100, 'server': 200, 'antibot': 300 } return weights[error_type] + retry_count * 0.84. 生产环境断点续爬完整实现
4.1 状态存储设计(SQLAlchemy示例)
from sqlalchemy import create_engine, Column, String, Integer from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class CrawlTask(Base): __tablename__ = 'crawl_tasks' url = Column(String(500), primary_key=True) status = Column(String(20), index=True) retry_count = Column(Integer, default=0) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, onupdate=datetime.now)4.2 断点恢复流程
- 初始化阶段:加载所有
status=pending的任务到内存队列 - 运行阶段:每次请求前更新状态为
fetching,请求后根据结果更新为success/failed - 崩溃恢复:重启时先处理所有
status=fetching的任务(视为中断的未完成请求) - 定期维护:清理超过7天的失败任务,压缩状态数据库
4.3 关键异常处理代码
try: response = requests.get(url, timeout=10) response.raise_for_status() process_response(response) mark_task_success(url) except requests.exceptions.RequestException as e: error_type = classify_error(e) retry_count = get_retry_count(url) if should_retry(error_type, retry_count): schedule_retry(url, error_type) else: mark_task_failed(url)5. 实战中的七个避坑指南
状态更新原子性:确保"标记抓取中"和"实际发起请求"是原子操作,避免多个进程同时处理同一个URL
with redis.lock(f'lock:{url}'): if get_status(url) == 'pending': set_status(url, 'fetching') # 再发起真实请求状态存储性能:当待抓取URL超过10万时,避免使用SELECT COUNT(*)查询,改用Redis的SCARD等高效命令
分布式协同:在多机部署时,使用Redis/Zookeeper等分布式协调服务,避免各节点状态冲突
状态备份策略:每小时备份状态数据库到云存储,防止主机磁盘损坏导致状态丢失
内存泄漏预防:长期运行的爬虫要定期清理内存中的重复URL缓存,建议使用Bloom Filter
日志关联设计:给每个任务分配唯一trace_id,方便追踪单个URL的全生命周期处理过程
监控指标暴露:通过Prometheus等工具暴露metrics,如:
crawler_tasks_total{status="pending"} 1423 crawler_tasks_total{status="failed"} 32 crawler_retry_count{url="example.com"} 5
6. 进阶:断点续爬与增量抓取的协同
真正的工业级爬虫需要将断点续爬与增量抓取结合使用:
- 首次全量抓取时启用断点续爬保证完整性
- 后续增量更新时通过
updated_at等字段过滤 - 对频繁更新的页面采用"优先队列"设计:
# 动态调整优先级公式 priority = base_priority + (now - last_crawl_time).total_seconds() / 3600
我在实际项目中总结出一个经验法则:对于日均更新量超过1万的网站,应该将重试队列和增量队列物理分离,使用不同worker处理,避免高优先级重试任务阻塞常规增量抓取。具体实现可以参考以下架构:
[新URL队列] --> [增量爬虫Worker] ↑ 同步 ↓ 上报 [状态存储] ←-------┘ ↖ 上报 ↓ 同步 [重试队列] --> [重试专用Worker]