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 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。

相关推荐
NPE~2 分钟前
[AI]Agent开发——ADK框架使用
人工智能·python·ai·教程·adk·agent开发
矮个史蒂芬4 分钟前
统计verilog .v文件中output 的bit数
python
gwf21634 分钟前
磨损均衡算法(Wear Leveling)——SSD如何让每块闪存“公平退休“?
运维·数据库·人工智能·python·嵌入式硬件·算法·智能硬件
爱吃提升1 小时前
python 分布式爬虫、爬虫合规与综合实战项目
分布式·爬虫·python
审小匠OpenCPAi1 小时前
银行流水核查怎么自动化?单边匹配、双向勾稽与图聚类异常检测的工程对比
java·前端·人工智能·python·审计
元Y亨H2 小时前
Pandas 解析 Excel 导致的内存溢出(MemoryError)
python·excel
金銀銅鐵3 小时前
[Python] 用 turtle 来绘制瑞典国旗
python
Albert Edison3 小时前
【GUI 自动化测试】Pywinauto 常见操作
自动化测试·python·pywinauto
海兰3 小时前
【高速缓存】RedisVL为文本生成嵌入向量实践指南
人工智能·python·机器学习
2601_955760073 小时前
如何用 Claude Opus 5 API 批量扩展长尾关键词和文章选题
前端·python·搜索引擎