Python爬虫零基础入门【第六章:增量、去重、断点续爬·第2节】断点续爬:失败队列、重放、任务状态!

🔥本期内容已收录至专栏《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 页白干了!

今天我们就来解决这个痛点------断点续爬,让爬虫具备"记忆力"。💾

🎯 本篇目标

看完这篇,你能做到:

  1. 理解任务状态机制(PENDING/RUNNING/SUCCESS/FAILED)
  2. 设计失败队列(哪些任务需要重试)
  3. 实现断点续爬(中断后继续,不重复劳动)
  4. 处理死信任务(重试多次仍失败的兜底方案)

验收标准:爬到第 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_1page_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()

📝 小结

今天我们学会了断点续爬的核心机制

  1. 任务状态机(PENDING → RUNNING → SUCCESS/FAILED)
  2. 失败队列(自动重试,超次数标记死信)
  3. 进度持久化(中断后无缝继续)
  4. 重试策略(指数退避、优先级)

断点续爬让你的爬虫具备了"容错性"------不怕中断、不怕失败。记住核心原则:任务粒度要小,状态更新要及时,重试要有上限

🎯 下期预告

增量解决了"只抓新的",断点解决了"中断恢复",但还有个终极问题:怎么保证同一条数据不会被重复入库?

下一篇《幂等去重:同一条数据反复跑也不会重复入库》,我们会设计一套 dedup_key 机制,让写入操作具备"幂等性"------不管运行多少次,数据库里永远是干净的。

记得验收:手动中断你的爬虫,然后重启,看看能不能从断点继续!加油!

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。

相关推荐
轻竹办公PPT2 小时前
2026 年 AI PPT 工具市场观察:国产工具与海外竞品的本土化对决,谁更懂中文职场
人工智能·python·powerpoint
喵手2 小时前
Python爬虫零基础入门【第七章:动态页面入门(Playwright)·第1节】Playwright 第一次:打开页面、等待元素、拿到渲染后 HTML!
爬虫·python·爬虫实战·动态页面·playwright·python爬虫工程化实战·零基础python爬虫教学
一个无名的炼丹师2 小时前
DeepSeek+LangGraph构建企业级多模态RAG:从PDF复杂解析到Agentic智能检索全流程实战
python·pdf·大模型·多模态·rag
历程里程碑2 小时前
哈希3 : 最长连续序列
java·数据结构·c++·python·算法·leetcode·tornado
火云洞红孩儿2 小时前
2026年,用PyMe可视化编程重塑Python学习
开发语言·python·学习
2401_841495642 小时前
【LeetCode刷题】两两交换链表中的节点
数据结构·python·算法·leetcode·链表·指针·迭代法
幻云20102 小时前
Next.js 之道:从入门到精通
前端·javascript·vue.js·人工智能·python
SunnyDays10112 小时前
使用 Python 自动查找并高亮 Word 文档中的文本
经验分享·python·高亮word文字·查找word文档中的文字
深蓝电商API3 小时前
Selenium处理弹窗、警报和验证码识别
爬虫·python·selenium