Python爬虫零基础入门【第六章:增量、去重、断点续爬·第3节】幂等去重:同一条数据反复跑也不会重复入库!

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

全文目录:

      • [🌟 开篇语](#🌟 开篇语)
      • [📚 上期回顾](#📚 上期回顾)
      • [🎯 本篇目标](#🎯 本篇目标)
      • [💡 什么是幂等性?](#💡 什么是幂等性?)
      • [🔑 去重键(dedup_key)设计](#🔑 去重键(dedup_key)设计)
        • [策略 1:业务唯一标识(推荐)](#策略 1:业务唯一标识(推荐))
        • [策略 2:内容哈希(兜底方案)](#策略 2:内容哈希(兜底方案))
        • [策略 3:组合键(最稳定)](#策略 3:组合键(最稳定))
      • [🛠️ 代码实战:三种去重策略](#🛠️ 代码实战:三种去重策略)
        • [策略 A:跳过重复(INSERT IGNORE)](#策略 A:跳过重复(INSERT IGNORE))
        • [策略 B:覆盖更新(REPLACE / UPSERT)](#策略 B:覆盖更新(REPLACE / UPSERT))
        • [策略 C:保留历史(判断后插入)](#策略 C:保留历史(判断后插入))
      • [📊 批量写入 + 去重优化](#📊 批量写入 + 去重优化)
      • [⚠️ 新手常见坑](#⚠️ 新手常见坑)
        • [坑 1:去重键设计不当](#坑 1:去重键设计不当)
        • [坑 2:忘记建唯一索引](#坑 2:忘记建唯一索引)
        • [坑 3:去重键冲突处理不当](#坑 3:去重键冲突处理不当)
      • [🚀 进阶优化](#🚀 进阶优化)
        • [优化 1:去重前置(内存去重)](#优化 1:去重前置(内存去重))
        • [优化 2:布隆过滤器(海量数据场景)](#优化 2:布隆过滤器(海量数据场景))
        • [优化 3:软删除 + 版本控制](#优化 3:软删除 + 版本控制)
      • [📝 小结](#📝 小结)
      • [🎯 下期预告](#🎯 下期预告)
      • [🌟 文末](#🌟 文末)
        • [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
        • [✅ 互动征集](#✅ 互动征集)

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 👉 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏《Python爬虫实战》

订阅后更新会优先推送,按目录学习更高效~

📚 上期回顾

上一期《Python爬虫零基础入门【第六章:增量、去重、断点续爬·第2节】断点续爬:失败队列、重放、任务状态!》内容中我们搞定了断点续爬,学会了用任务状态表管理采集进度。现在你的爬虫已经很"抗造"了------网络断了、程序崩了,重启后都能从上次中断的地方继续干活。

但新问题又来了:测试时你手欠,同一个页面跑了 3 遍;或者为了保险,增量边界设置得宽松了点,有几条数据被重复采集了。结果数据库里出现了好几条一模一样的记录。😓

删数据?太危险。改代码重跑?太麻烦。

今天我们就来解决这个痛点------幂等去重,让你的爬虫具备"重复采集不重复入库"的能力。不管运行多少次,数据库永远是干净的!✨

🎯 本篇目标

看完这篇,你能做到:

  1. 理解幂等性概念(同一操作执行多次结果一致)
  2. 设计去重键(dedup_key)(source + id / hash)
  3. 实现三种去重策略(跳过 / 覆盖 / 保留历史)
  4. 处理冲突场景(更新 vs 插入)

验收标准:同一条数据采集 5 次,数据库里只有 1 条记录

💡 什么是幂等性?

先看个数学例子:

json 复制代码
f(x) = x²

f(3) = 9
f(f(3)) = f(9) = 81  ❌ 结果不同,非幂等

 

g(x) = abs(x)  # 绝对值

g(-5) = 5
g(g(-5)) = g(5) = 5  ✅ 结果相同,幂等

放到爬虫场景:

  • 非幂等操作 :每次采集都 INSERT,导致重复数据
  • 幂等操作 :基于唯一键 INSERT OR UPDATE,重复执行结果不变

关键问题:怎么判断"这是同一条数据"?

🔑 去重键(dedup_key)设计

策略 1:业务唯一标识(推荐)

适用场景:数据源提供了稳定的唯一 ID。

示例:

python 复制代码
# 文章数据
dedup_key = f"{source}_{article_id}"
# 示例:'sina_12345678'

# 商品数据
dedup_key = f"{platform}_{sku_id}"
# 示例:'jd_100012345678'

优点:

  • 语义清晰,方便追溯
  • 稳定可靠,不会因内容变化而变

缺点:

  • 依赖数据源提供 ID(有些网站没有)
策略 2:内容哈希(兜底方案)

适用场景:数据源没有稳定 ID,但内容相对固定。

示例:

python 复制代码
from hashlib import md5

def generate_dedup_key(title, url):
    """基于标题 + URL 生成哈希去重键"""
    raw = f"{title}_{url}"
    return md5(raw.encode('utf-8')).hexdigest()

# 示例
key = generate_dedup_key(
    title="Python爬虫入门", 
    url="https://example.com/article/123"
)
# 结果:'a3f5c8d9e2b1...'  (32位哈希)

优点:

  • 不依赖数据源 ID
  • 内容相同自动去重

缺点:

  • 内容微小变化(如标题多个空格)会产生新键
  • 哈希值不可读,排查问题稍麻烦
策略 3:组合键(最稳定)

结合业务 ID + 内容特征:

python 复制代码
def generate_combined_key(source, article_id, title):
    """组合键:优先用 ID,无 ID 则用标题哈希"""
    if article_id:
        return f"{source}_{article_id}"
    else:
        title_hash = md5(title.encode('utf-8')).hexdigest()[:8]
        return f"{source}_hash_{title_hash}"

# 示例
key1 = generate_combined_key('weibo', '4856123456789012', '今天天气真好')
# 结果:'weibo_4856123456789012'

key2 = generate_combined_key('blog', None, '我的第一篇博客')
# 结果:'blog_hash_3f8a9c2d'

🛠️ 代码实战:三种去重策略

策略 A:跳过重复(INSERT IGNORE)

原理:尝试插入,遇到唯一键冲突就静默跳过。

适用场景:数据不会更新,只关心"有没有"。

python 复制代码
import sqlite3

class DedupWriter:
    """支持去重的数据写入器"""
    
    def __init__(self, db_path="data.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 articles (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                dedup_key TEXT UNIQUE NOT NULL,  -- 去重键(唯一约束)
                title TEXT,
                url TEXT,
                content TEXT,
                crawled_at TEXT,
                updated_at TEXT
            )
        """)
        self.conn.commit()
    
    def insert_ignore(self, item):
        """跳过重复:遇到冲突就忽略"""
        try:
            self.conn.execute("""
                INSERT INTO articles (dedup_key, title, url, content, crawled_at)
                VALUES (?, ?, ?, ?, datetime('now'))
            """, (item['dedup_key'], item['title'], item['url'], item.get('content')))
            self.conn.commit()
            print(f"✅ 插入成功:{item['title']}")
            return True
        except sqlite3.IntegrityError:
            # 唯一键冲突,跳过
            print(f"⏭️  已存在,跳过:{item['title']}")
            return False

验收测试:

python 复制代码
writer = DedupWriter()

item = {
    'dedup_key': 'test_article_001',
    'title': 'Python爬虫教程',
    'url': 'https://example.com/article/1'
}

# 第 1 次插入
writer.insert_ignore(item)  # ✅ 插入成功

# 第 2 次插入(重复)
writer.insert_ignore(item)  # ⏭️ 已存在,跳过

# 第 3 次插入(还是重复)
writer.insert_ignore(item)  # ⏭️ 已存在,跳过

数据库里只有 1 条记录!✅

策略 B:覆盖更新(REPLACE / UPSERT)

原理:遇到重复就覆盖旧数据(适合数据会更新的场景)。

适用场景:文章会修改、商品价格会变化。

SQLite 版本(REPLACE):

python 复制代码
def upsert_replace(self, item):
    """覆盖更新:REPLACE 策略"""
    self.conn.execute("""
        REPLACE INTO articles (dedup_key, title, url, content, updated_at)
        VALUES (?, ?, ?, ?, datetime('now'))
    """, (item['dedup_key'], item['title'], item['url'], item.get('content')))
    self.conn.commit()
    print(f"🔄 更新成功:{item['title']}")

MySQL 版本(ON DUPLICATE KEY UPDATE):

python 复制代码
def upsert_mysql(self, item):
    """MySQL 的 UPSERT 写法"""
    cursor = self.conn.cursor()
    cursor.execute("""
        INSERT INTO articles (dedup_key, title, url, content, crawled_at, updated_at)
        VALUES (%s, %s, %s, %s, NOW(), NOW())
        ON DUPLICATE KEY UPDATE
            title = VALUES(title),
            url = VALUES(url),
            content = VALUES(content),
            updated_at = NOW()
    """, (item['dedup_key'], item['title'], item['url'], item.get('content')))
    self.conn.commit()
    print(f"🔄 插入或更新:{item['title']}")

PostgreSQL 版本(ON CONFLICT):

python 复制代码
def upsert_postgres(self, item):
    """PostgreSQL 的 UPSERT 写法"""
    cursor = self.conn.cursor()
    cursor.execute("""
        INSERT INTO articles (dedup_key, title, url, content, crawled_at, updated_at)
        VALUES (%s, %s, %s, %s, NOW(), NOW())
        ON CONFLICT (dedup_key) DO UPDATE SET
            title = EXCLUDED.title,
            url = EXCLUDED.url,
            content = EXCLUDED.content,
            updated_at = NOW()
    """, (item['dedup_key'], item['title'], item['url'], item.get('content')))
    self.conn.commit()
策略 C:保留历史(判断后插入)

原理:先查询是否存在,不存在再插入;已存在则保留旧版本。

适用场景:需要追溯数据变化历史。

python 复制代码
def insert_with_history(self, item):
    """保留历史:先查询,不存在才插入"""
    cursor = self.conn.execute("""
        SELECT id FROM articles WHERE dedup_key = ?
    """, (item['dedup_key'],))
    
    existing = cursor.fetchone()
    
    if existing:
        print(f"📦 已存在,保留旧版本:{item['title']}")
        return False
    else:
        self.conn.execute("""
            INSERT INTO articles (dedup_key, title, url, content, crawled_at)
            VALUES (?, ?, ?, ?, datetime('now'))
        """, (item['dedup_key'], item['title'], item['url'], item.get('content')))
        self.conn.commit()
        print(f"✅ 首次插入:{item['title']}")
        return True

📊 批量写入 + 去重优化

单条写入太慢?我们来做批量版本:

python 复制代码
def batch_upsert(self, items, batch_size=100):
    """批量写入(带去重)"""
    total = len(items)
    inserted = 0
    skipped = 0
    
    for i in range(0, total, batch_size):
        batch = items[i:i+batch_size]
        
        for item in batch:
            try:
                self.conn.execute("""
                    INSERT INTO articles (dedup_key, title, url, content, crawled_at)
                    VALUES (?, ?, ?, ?, datetime('now'))
                """, (item['dedup_key'], item['title'], item['url'], item.get('content')))
                inserted += 1
            except sqlite3.IntegrityError:
                skipped += 1
        
        self.conn.commit()  # 每批提交一次
        print(f"📦 批次 {i//batch_size + 1}:插入 {inserted},跳过 {skipped}")
    
    print(f"\n📊 总计:插入 {inserted},跳过 {skipped}")

⚠️ 新手常见坑

坑 1:去重键设计不当

现象:明明是同一篇文章,却被当成两条数据。

原因:去重键选择了会变的字段(如标题、时间)。

python 复制代码
# ❌ 错误示例
dedup_key = f"{title}_{publish_time}"
# 标题改了空格、时间精度变了,就成了"新数据"

# ✅ 正确示例
dedup_key = f"{source}_{article_id}"
# 基于不变的业务 ID
坑 2:忘记建唯一索引

现象:数据库里有重复数据,INSERT IGNORE 不生效。

原因:dedup_key 字段没有 UNIQUE 约束。

解决:

python 复制代码
# 建表时加约束
CREATE TABLE articles (
    dedup_key TEXT UNIQUE NOT NULL  -- 必须有 UNIQUE
)

# 或者事后添加唯一索引
CREATE UNIQUE INDEX idx_dedup_key ON articles(dedup_key);
坑 3:去重键冲突处理不当

场景:不同来源的文章,ID 恰好相同(如微博 ID 和知乎 ID 都是 12345)。

问题:会误判为同一条数据。

解决:去重键必须包含来源标识

python 复制代码
# ❌ 不好的设计
dedup_key = article_id  # 可能冲突

# ✅ 好的设计
dedup_key = f"{source}_{article_id}"  # 'weibo_12345' vs 'zhihu_12345'

🚀 进阶优化

优化 1:去重前置(内存去重)

批量写入前,先在内存中去重,减少数据库压力:

python 复制代码
def deduplicate_in_memory(items):
    """内存去重:基于 dedup_key"""
    seen = set()
    unique_items = []
    
    for item in items:
        if item['dedup_key'] not in seen:
            seen.add(item['dedup_key'])
            unique_items.append(item)
    
    print(f"📊 内存去重:{len(items)} → {len(unique_items)}")
    return unique_items

# 使用
items = [...]  # 采集到的数据
unique_items = deduplicate_in_memory(items)
writer.batch_upsert(unique_items)
优化 2:布隆过滤器(海量数据场景)

当数据量达到千万级,用集合去重会爆内存,可以用布隆过滤器:

python 复制代码
from pybloom_live import BloomFilter

class BloomDeduplicator:
    """基于布隆过滤器的去重器"""
    
    def __init__(self, capacity=10000000, error_rate=0.001):
        self.bloom = BloomFilter(capacity=capacity, error_rate=error_rate)
    
    def is_duplicate(self, dedup_key):
        """判断是否重复"""
        if dedup_key in self.bloom:
            return True  # 可能重复
        else:
            self.bloom.add(dedup_key)
            return False  # 肯定不重复

# 使用
deduper = BloomDeduplicator()

for item in items:
    if deduper.is_duplicate(item['dedup_key']):
        print(f"⏭️  跳过重复:{item['title']}")
        continue
    
    writer.insert(item)

注意:布隆过滤器有误判率 (false positive),但不会漏判(false negative)。

优化 3:软删除 + 版本控制

如果需要保留数据变更历史,可以用软删除 + 版本号:

python 复制代码
CREATE TABLE articles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    dedup_key TEXT NOT NULL,
    version INTEGER DEFAULT 1,         -- 版本号
    title TEXT,
    content TEXT,
    is_deleted BOOLEAN DEFAULT FALSE,  -- 软删除标记
    created_at TEXT,
    updated_at TEXT,
    UNIQUE(dedup_key, version)         -- 组合唯一键
)

写入逻辑:

python 复制代码
def insert_new_version(self, item):
    """插入新版本(保留历史)"""
    # 查询当前最大版本
    cursor = self.conn.execute("""
        SELECT MAX(version) FROM articles WHERE dedup_key = ?
    """, (item['dedup_key'],))
    
    max_version = cursor.fetchone()[0] or 0
    new_version = max_version + 1
    
    self.conn.execute("""
        INSERT INTO articles (dedup_key, version, title, content, created_at)
        VALUES (?, ?, ?, ?, datetime('now'))
    """, (item['dedup_key'], new_version, item['title'], item.get('content')))
    
    self.conn.commit()
    print(f"📌 插入版本 {new_version}:{item['title']}")

📝 小结

今天我们学会了幂等去重的核心技术

  1. 去重键设计(source + id / hash)
  2. 三种策略(跳过 / 覆盖 / 保留历史)
  3. 批量优化(内存去重 / 布隆过滤器)
  4. 冲突处理(唯一约束 / UPSERT)

记住核心原则:去重键要稳定、唯一约束要加好、插入前可以先去重。有了幂等性,你的爬虫就能"随便跑"------不怕重复采集,不怕数据脏乱。💪

🎯 下期预告

第 6 章到这里就结束啦!我们系统学习了增量、断点、去重三大核心能力,你的爬虫已经具备了"工程化"的基础。

下一章《第 7 章:动态页面入门(Playwright)》,我们将进入新战场------用 Playwright 搞定 JavaScript 渲染的动态网站。不再局限于静态 HTML,真正的硬骨头等着你来啃!🚀

验收作业:写一个带去重的爬虫,同一个 URL 采集 5 次,数据库里只有 1 条记录。截图给我看看!加油!

🌟 文末

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

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

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

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

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

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

✅ 互动征集

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

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


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

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

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


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

相关推荐
Python毕设指南2 小时前
基于深度学习的旅游推荐系统
python·深度学习·数据分析·django·毕业设计·课程设计
深蓝电商API2 小时前
Selenium多窗口切换与Cookie管理
爬虫·python·selenium·测试工具
小北方城市网2 小时前
Spring Cloud 服务治理实战:构建高可用微服务体系
spring boot·python·rabbitmq·java-rabbitmq·数据库架构
写代码的【黑咖啡】2 小时前
Python中的Statsmodels:统计建模与假设检验
开发语言·python
程序员杰哥3 小时前
Pytest自动化测试框架实战
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·pytest
weixin_433179333 小时前
python - 函数 function
开发语言·python
不吃鱼的小时喵3 小时前
【Python】关于python多进程
python
喵手3 小时前
Python爬虫零基础入门【第六章:增量、去重、断点续爬·第1节】增量采集:只抓新增/更新(新手也能做)!
爬虫·python·python爬虫实战·python爬虫工程化实战·python爬虫零基础入门·增量、去重·增量采集
万粉变现经纪人4 小时前
如何解决 pip install pyodbc 报错 缺少 ‘cl.exe’ 或 ‘sql.h’(ODBC 头文件)问题
数据库·python·sql·网络协议·bug·ssl·pip