🔥本期内容已收录至专栏《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 遍;或者为了保险,增量边界设置得宽松了点,有几条数据被重复采集了。结果数据库里出现了好几条一模一样的记录。😓
删数据?太危险。改代码重跑?太麻烦。
今天我们就来解决这个痛点------幂等去重,让你的爬虫具备"重复采集不重复入库"的能力。不管运行多少次,数据库永远是干净的!✨
🎯 本篇目标
看完这篇,你能做到:
- 理解幂等性概念(同一操作执行多次结果一致)
- 设计去重键(dedup_key)(source + id / hash)
- 实现三种去重策略(跳过 / 覆盖 / 保留历史)
- 处理冲突场景(更新 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']}")
📝 小结
今天我们学会了幂等去重的核心技术:
- 去重键设计(source + id / hash)
- 三种策略(跳过 / 覆盖 / 保留历史)
- 批量优化(内存去重 / 布隆过滤器)
- 冲突处理(唯一约束 / UPSERT)
记住核心原则:去重键要稳定、唯一约束要加好、插入前可以先去重。有了幂等性,你的爬虫就能"随便跑"------不怕重复采集,不怕数据脏乱。💪
🎯 下期预告
第 6 章到这里就结束啦!我们系统学习了增量、断点、去重三大核心能力,你的爬虫已经具备了"工程化"的基础。
下一章《第 7 章:动态页面入门(Playwright)》,我们将进入新战场------用 Playwright 搞定 JavaScript 渲染的动态网站。不再局限于静态 HTML,真正的硬骨头等着你来啃!🚀
验收作业:写一个带去重的爬虫,同一个 URL 采集 5 次,数据库里只有 1 条记录。截图给我看看!加油!
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

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