🔥本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!!

全文目录:
-
-
- [🌟 开篇语](#🌟 开篇语)
- [📚 上期回顾](#📚 上期回顾)
- [🎯 本篇目标](#🎯 本篇目标)
- [💡 断点续爬的核心思想](#💡 断点续爬的核心思想)
- [🔑 任务状态设计](#🔑 任务状态设计)
- [🛠️ 代码实战:支持断点续爬的爬虫](#🛠️ 代码实战:支持断点续爬的爬虫)
- [🎬 实际运行演示](#🎬 实际运行演示)
- [⚠️ 新手常见坑](#⚠️ 新手常见坑)
-
- [坑 1:状态更新不及时](#坑 1:状态更新不及时)
- [坑 2:重试逻辑死循环](#坑 2:重试逻辑死循环)
- [坑 3:任务粒度太大](#坑 3:任务粒度太大)
- [🚀 进阶优化](#🚀 进阶优化)
-
- [优化 1:失败任务优先级](#优化 1:失败任务优先级)
- [优化 2:指数退避重试](#优化 2:指数退避重试)
- [优化 3:死信队列人工介入](#优化 3:死信队列人工介入)
- [📊 完整代码示例](#📊 完整代码示例)
- [📝 小结](#📝 小结)
- [🎯 下期预告](#🎯 下期预告)
- [🌟 文末](#🌟 文末)
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
-
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》
订阅后更新会优先推送,按目录学习更高效~
📚 上期回顾
上一篇《Python爬虫零基础入门【第六章:增量、去重、断点续爬·第1节】增量采集:只抓新增/更新(新手也能做)!》内容中,我们搞定了增量采集,学会了用时间戳或 ID 做边界,只抓新数据。现在你的爬虫效率提升了 10 倍------第一次跑全量,之后每次只采集几十条新增内容。
但真实世界总是残酷的:你兴冲冲地跑了 3 个小时,爬到第 87 页时------网络断了、服务器挂了、或者你不小心关了终端。😱
第二天重启,发现又从第 1 页开始爬。前面 86 页白干了!
今天我们就来解决这个痛点------断点续爬,让爬虫具备"记忆力"。💾
🎯 本篇目标
看完这篇,你能做到:
- 理解任务状态机制(PENDING/RUNNING/SUCCESS/FAILED)
- 设计失败队列(哪些任务需要重试)
- 实现断点续爬(中断后继续,不重复劳动)
- 处理死信任务(重试多次仍失败的兜底方案)
验收标准:爬到第 5 页时手动中断,重启后从第 6 页继续。
💡 断点续爬的核心思想
类比一下你追剧:
- 没有断点:每次打开 App,都从第 1 集开始(崩溃)
- 有断点:App 记住你看到第 8 集 23 分钟,下次直接跳到这里(舒服)
爬虫也一样:
- 没有断点:中断后从头再来,浪费时间和流个任务的状态,失败的重试,成功的跳过
关键问题:怎么记录"进度"?
🔑 任务状态设计
状态机模型
我们给每个采集任务定义 4 种状态:
json
PENDING → 等待执行(刚创建,还没开始)
RUNNING → 执行中(正在采集)
SUCCESS → 成功(已完成,不再重试)
FAILED → 失败(需要重试)
状态流转:
json
PENDING → RUNNING → SUCCESS ✅
↓
FAILED → RUNNING → SUCCESS ✅
↓
(重试N次后) → DEAD ☠️ (死信任务)
任务表结构
用 例(MySQL/PostgreSQL 也一样):
python
import sqlite3
from datetime import datetime
from enum import Enum
class TaskStatus(Enum):
"""任务状态枚举"""
PENDING = "PENDING"
RUNNING = "RUNNING"
SUCCESS = "SUCCESS"
FAILED = "FAILED"
DEAD = "DEAD" # 死信任务
class TaskDB:
"""任务状态管理"""
def __init__(self, db_path="tasks.db"):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self._init_table()
def _init_table(self):
"""初始化任务表"""
self.conn.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT UNIQUE NOT NULL, -- 任务唯一标识(如 page_5)
url TEXT NOT NULL, -- 目标 URL
status TEXT DEFAULT 'PENDING', -- 任务状态
retry_count INTEGER DEFAULT 0, -- 重试次数
max_retries INTEGER DEFAULT 3, -- 最大重试次数
error_msg TEXT, -- 失败原因
created_at TEXT, -- 创建时间
updated_at TEXT -- 更新时间
)
""")
self.conn.commit()
def add_task(self, task_id, url, max_retries=3):
"""添加新任务"""
now = datetime.now().isoformat()
try:
self.conn.execute("""
INSERT INTO tasks (task_id, url, status, max_retries, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (task_id, url, TaskStatus.PENDING.value, max_retries, now, now))
self.conn.commit()
except sqlite3.IntegrityError:
# 任务已存在,跳过
pass
def get_pending_tasks(self, limit=100):
"""获取待执行的任务(PENDING 或 FAILED 且未超重试次数)"""
cursor = self.conn.execute("""
SELECT task_id, url, retry_count, max_retries
FROM tasks
WHERE (status = ? OR status = ?)
AND retry_count < max_retries
ORDER BY created_at ASC
LIMIT ?
""", (TaskStatus.PENDING.value, TaskStatus.FAILED.value, limit))
return cursor.fetchall()
def update_status(self, task_id, status, error_msg=None):
"""更新任务状态"""
now = datetime.now().isoformat()
if status == TaskStatus.FAILED:
# 失败时,重试次数 +1
self.conn.execute("""
UPDATE tasks
SET status = ?, error_msg = ?, retry_count = retry_count + 1, updated_at = ?
WHERE task_id = ?
""", (status.value, error_msg, now, task_id))
else:
self.conn.execute("""
UPDATE tasks
SET status = ?, updated_at = ?
WHERE task_id = ?
""", (status.value, now, task_id))
self.conn.commit()
def mark_dead(self, task_id):
"""标记为死信任务(重试次数耗尽)"""
self.update_status(task_id, TaskStatus.DEAD)
def get_stats(self):
"""获取任务统计"""
cursor = self.conn.execute("""
SELECT status, COUNT(*) as count
FROM tasks
GROUP BY status
""")
stats[0]: row[1] for row in cursor.fetchall()}
return stats
🛠️ 代码实战:支持断点续爬的爬虫
第一步:生成任务队列
python
class ResumableSpider:
"""支持断点续爬的爬虫"""
def __init__(self, base_url):
self.base_url = base_url
self.task_db = TaskDB()
def init_tasks(self, total_pages=10):
"""初始化任务队列(只在第一次运行时调用)"""
print(f"📝 初始化 {total_pages} 个页面任务...")
for page in range(1, total_pages + 1):
task_id = f"page_{page}"
url = f"{self.base_url}/list?page={page}"
self.task_db.add_task(task_id, url)
print("✅ 任务队列初始化完成")
第二步:执行任务并记录状态
python
import requests
import time
def crawl_page(self, task_id, url):
"""采集单个页面"""
print(f"🔍 [{task_id}] 开始采集:{url}")
# 标记为 RUNNING
self.task_db.update_status(task_id, TaskStatus.RUNNING)
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
# 这里省略解析逻辑...
items = self.parse_page(resp.text)
# 成功,标记为 SUCCESS
self.task_db.update_status(task_id, TaskStatus.SUCCESS)
print(f"✅ [{task_id}] 采集成功,获得 {len(items)} 条数据")
return items
except Exception as e:
# 失败,标记为 FAILED 并记录原因
error_msg = str(e)
self.task_db.update_status(task_id, TaskStatus.FAILED, error_msg)
print(f"❌ [{task_id}] 采集失败:{error_msg}")
return None
def parse_page(self, html):
"""解析页面(示例)"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')
items = soup.select('.article-item')
return [{'title': item.text.strip()} for item in items]
第三步:断点续爬主循环
python
def run(self):
"""运行断点续爬"""
print("🚀 开始断点续爬...")
while True:
# 获取待执行任务
tasks = self.task_db.get_pending_tasks(limit=10)
if not tasks:
print("✨ 所有任务已完成!")
break
print(f"\n📦 本轮待处理任务:{len(tasks)} 个")
for task_id, url, retry_count, max_retries in tasks:
print(f"\n{'🔄' if retry_count > 0 else '🆕'} [{task_id}] "
f"重试 {retry_count}/{max_retries}")
items = self.crawl_page(task_id, url)
# 检查是否超过重试次数
if items is None:
current_retry = retry_count + 1
if current_retry >= max_retries:
print(f"☠️ [{task_id}] 重试次数耗尽,标记为死信任务")
self.task_db.mark_dead(task_id)
time.sleep(1) # 礼貌延迟
# 显示进度
self.show_progress()
print("\n" + "="*50)
print("📊 最终统计:")
self.show_progress()
def show_progress(self):
"""显示任务进度"""
stats = self.task_db.get_stats()
total = sum(stats.values())
print(f"\n📈 任务进度:")
print(f" ✅ 成功:{stats.get('SUCCESS', 0)}")
print(f" ⏳ 待处理:{stats.get('PENDING', 0)}")
print(f" 🔄 失败重试:{stats.get('FAILED', 0)}")
print(f" ☠️ 死信:{stats.get('DEAD', 0)}")
print(f" 📊 总计:{total}")
🎬 实际运行演示
第一次运行(中途中断):
bash
$ python spider.py
📝 初始化 10 个页面任务...
✅ 任务队列初始化完成
🚀 开始断点续爬...
📦 本轮待处理任务:10 个
🆕 [page_1] 重试 0/3
🔍 [page_1] 开始采集:https://example.com/list?page=1
✅ [page_1] 采集成功,获得 20 条数据
🆕 [page_2] 重试 0/3
🔍 [page_2] 开始采集:https://example.com/list?page=2
✅ [page_2] 采集成功,获得 20 条数据
...
🆕 [page_5] 重试 0/3
🔍 [page_5] 开始采集:https://example.com/list?page=5
^C # 手动中断(Ctrl+C)
KeyboardInterrupt
第二次运行(断点恢复):
bash
$ python spider.py
🚀 开始断点续爬...
📦 本轮待处理任务:6 个
🆕 [page_5] 重试 0/3 # 从未完成的任务继续!
🔍 [page_5] 开始采集:https://example.com/list?page=5
✅ [page_5] 采集成功,获得 20 条数据
🆕 [page_6] 重试 0/3
🔍 [page_6] 开始采集:https://example.com/list?page=6
✅ [page_6] 采集成功,获得 20 条数据
...
✨ 所有任务已完成!
📊 最终统计:
✅ 成功:10
⏳ 待处理:0
🔄 失败重试:0
☠️ 死信:0
📊 总计:10
关键点 :第二次运行时,page_1 到 page_4 已经是 SUCCESS 状态,直接跳过!
⚠️ 新手常见坑
坑 1:状态更新不及时
现象:任务标记为 RUNNING 后,程序崩溃,重启时又执行了一遍。
原因:update_status 没有立即 commit()。
解决:
python
# 每次更新后立即提交
self.conn.commit()
# 或者使用上下文管理器
with self.conn:
self.conn.execute(...) # 自动 commit
坑 2:重试逻辑死循环
现象:某个任务一直失败,程序卡在那里无限重试。
解决:
python
# 严格检查重试次数
if retry_count >= max_retries:
self.task_db.mark_dead(task_id)
continue # 跳过这个任务
坑 3:任务粒度太大
场景:一个任务是"爬取 100 页",执行到第 50 页挂了。
问题:整个任务重来,前 50 页白干。
解决:任务粒度要小,一个任务对应一页,而不是多页。
python
# ❌ 不好的设计
task_id = "crawl_all_pages"
# ✅ 好的设计
task_id = f"page_{page_num}"
🚀 进阶优化
优化 1:失败任务优先级
某些失败可能是暂时的(如网络抖动),应该优先重试:
python
def get_pending_tasks(self, limit=100):
"""FAILED 任务优先于 PENDING"""
cursor = self.conn.execute("""
SELECT task_id, url, retry_count, max_retries
FROM tasks
WHERE (status = ? OR status = ?)
AND retry_count < max_retries
ORDER BY
CASE WHEN status = ? THEN 0 ELSE 1 END, -- FAILED 优先
created_at ASC
LIMIT ?
""", (TaskStatus.PENDING.value, TaskStatus.FAILED.value,
TaskStatus.FAILED.value, limit))
return cursor.fetchall()
优化 2:指数退避重试
失败后不要立即重试,等待时间逐步增加:
python
import time
def retry_with_backoff(self, task_id, url, retry_count):
"""指数退避重试"""
wait_time = 2 ** retry_count # 1秒 → 2秒 → 4秒 → 8秒
print(f"⏰ 等待 {wait_time} 秒后重试...")
time.sleep(wait_time)
return self.crawl_page(task_id, url)
优化 3:死信队列人工介入
死信任务不应该被丢弃,而是导出给人工排查:
python
def export_dead_tasks(self):
"""导出死信任务"""
cursor = self.conn.execute("""
SELECT task_id, url, error_msg, retry_count
FROM tasks
WHERE status = ?
""", (TaskStatus.DEAD.value,))
dead_tasks = cursor.fetchall()
if dead_tasks:
print(f"\n⚠️ 发现 {len(dead_tasks)} 个死信任务:")
for task_id, url, error_msg, retry_count in dead_tasks:
print(f" [{task_id}] {url}")
print(f" 错误:{error_msg}")
print(f" 重试:{retry_count} 次")
📊 完整代码示例
python
# main.py
from resumable_spider import ResumableSpider
if __name__ == '__main__':
spider = ResumableSpider('https://example.com')
# 第一次运行:初始化任务
# spider.init_tasks(total_pages=10)
# 运行断点续爬
spider.run()
# 导出死信任务(如果有)
spider.export_dead_tasks()
📝 小结
今天我们学会了断点续爬的核心机制:
- 任务状态机(PENDING → RUNNING → SUCCESS/FAILED)
- 失败队列(自动重试,超次数标记死信)
- 进度持久化(中断后无缝继续)
- 重试策略(指数退避、优先级)
断点续爬让你的爬虫具备了"容错性"------不怕中断、不怕失败。记住核心原则:任务粒度要小,状态更新要及时,重试要有上限。
🎯 下期预告
增量解决了"只抓新的",断点解决了"中断恢复",但还有个终极问题:怎么保证同一条数据不会被重复入库?
下一篇《幂等去重:同一条数据反复跑也不会重复入库》,我们会设计一套 dedup_key 机制,让写入操作具备"幂等性"------不管运行多少次,数据库里永远是干净的。
记得验收:手动中断你的爬虫,然后重启,看看能不能从断点继续!加油!
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。