㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期爬虫难度指数:⭐⭐⭐
🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:
-
-
- [🌟 开篇语](#🌟 开篇语)
- [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
- [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
- [3️⃣ 合规与注意事项(必写)](#3️⃣ 合规与注意事项(必写))
- [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
- [5️⃣ 环境准备与依赖安装](#5️⃣ 环境准备与依赖安装)
- [6️⃣ 核心实现:请求与解析层(复用与微调)](#6️⃣ 核心实现:请求与解析层(复用与微调))
- [7️⃣ 核心实现:快照管理器(Snapshot Manager)](#7️⃣ 核心实现:快照管理器(Snapshot Manager))
- [8️⃣ 核心实现:差异比对引擎(Diff Engine) 🌟核心亮点](#8️⃣ 核心实现:差异比对引擎(Diff Engine) 🌟核心亮点)
- [9️⃣ 运行方式与结果展示](#9️⃣ 运行方式与结果展示)
- [🔟 常见问题与排错(FAQ)](#🔟 常见问题与排错(FAQ))
- [1️⃣1️⃣ 进阶优化(加分项)](#1️⃣1️⃣ 进阶优化(加分项))
- [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
- [🌟 文末](#🌟 文末)
-
- [✅ 专栏持续更新中|建议收藏 + 订阅](#✅ 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
- [✅ 免责声明](#✅ 免责声明)
-
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。
💕订阅后更新会优先推送,按目录学习更高效💯~
1️⃣ 摘要(Abstract)
本项目将实现一个具有"记忆"功能的爬虫系统。它不仅能抓取当前数据,还能将其保存为历史快照(Snapshot),并自动加载上一版本的快照进行Deep Diff(深度比对)。
读完本文,你将掌握:
- 快照版本管理:如何以 JSON/HTML 格式标准化存储每日数据。
- 差异计算算法:识别数据的**新增(New)、修改(Modified)、删除(Deleted)**三种状态。
- 核心价值:实现"价格变动监控"、"库存预警"等高价值业务逻辑。
2️⃣ 背景与需求(Why)
为什么要这么做?
单纯的数据堆砌价值有限,数据的变化才是商机。
- 竞品监控:对手什么时候改了标题?什么时候偷偷降价了?
- 套利模型:监控二手房源,一上架低于均价的"笋盘"立刻报警。
目标变更点:
我们将继续以 books.toscrape.com 为靶场,但重点关注以下变化:
- Price Change:价格波动。
- Availability Change:从"缺货"变"有货"。
3️⃣ 合规与注意事项(必写)
- 增量抓取的礼仪 :如果是全量比对,依然需要全量抓取。如果目标站点支持
Last-Modified响应头或 API 提供update_time,优先利用这些字段过滤,减少对服务器的骚扰。 - 存储隐私:保存历史快照会占用大量磁盘空间,注意定期清理(Retention Policy)或进行 gzip 压缩。
- 频率控制:Diff 任务通常每天运行一次即可,避开对方服务器的高峰期(通常是凌晨)。
4️⃣ 技术选型与整体流程(What/How)
技术栈升级:
- Requests + BS4:依然负责抓取。
- JSON:作为快照存储格式(比 CSV 更易于结构化比对)。
- Hash/Dict Set:利用 Python 字典的高效查找特性(O(1) 复杂度)进行比对,避免双重循环(O(N^2))。
整体流程图:
text
[启动任务]
│
├── 1. [Load Snapshot]: 尝试加载昨天的 JSON 文件 (History)
│ (如果没有,则视为空)
│
├── 2. [Fetch & Parse]: 抓取今天的最新数据 (Current)
│
├── 3. [Diff Engine]: 对比 (History) vs (Current)
│ ├── 提取 UPC 作为唯一主键
│ ├── 找出 New (Current 有, History 无)
│ ├── 找出 Deleted (Current 无, History 有)
│ └── 找出 Modified (都有, 但 Price/Stock 不同)
│
├── 4. [Report]: 输出差异报告 (Diff Report)
│
└── 5. [Save Snapshot]: 将 (Current) 存为新的历史文件
5️⃣ 环境准备与依赖安装
除了基础包,不需要额外复杂的库,Python 标准库足矣。
bash
# 目录结构推荐
monitor_spider/
├── snapshots/ # 存放每日的 JSON 快照
│ ├── 2023-10-01.json
│ └── 2023-10-02.json
├── diff_reports/ # 存放差异报告
├── spider_main.py # 主程序
└── utils.py # 工具函数
6️⃣ 核心实现:请求与解析层(复用与微调)
为了节省篇幅,这里简化抓取逻辑,重点在于数据结构化 。我们需要确保抓取的数据里有一个绝对唯一的主键(Unique Key) ,在图书网站里,UPC(通用产品代码)就是最好的主键。
python
import requests
from bs4 import BeautifulSoup
import time
import random
# 模拟 fetch 函数 (和上一篇类似,但为了演示 Diff 逻辑,这里简化)
def fetch_all_books():
"""
模拟抓取全站数据。
在真实场景中,这里是循环分页抓取所有数据。
返回一个以 UPC 为 Key 的字典,方便后续比对!
"""
print("🔄 正在从网络抓取最新数据...")
# 这里我们只抓取第一页作为演示
url = "http://books.toscrape.com/catalogue/page-1.html"
headers = {'User-Agent': 'Mozilla/5.0 ...'}
try:
resp = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(resp.text, 'html.parser')
books_map = {} # 注意:这里用字典,key=upc
articles = soup.select('article.product_pod')
for article in articles:
# 简化提取逻辑,重点演示 Diff
detail_url = "http://books.toscrape.com/catalogue/" + article.select_one('h3 a')['href']
# ⚠️ 真实环境需要进入详情页拿 UPC,为了演示方便,
# 我们这里暂时用书名做 Key (实际开发请务必用 UPC!)
title = article.select_one('h3 a')['title']
price_str = article.select_one('p.price_color').text.replace('£', '').replace('Â', '')
price = float(price_str)
# 假设我们只关心这两个字段用于对比
books_map[title] = {
"title": title,
"price": price,
"url": detail_url,
"timestamp": time.time()
}
print(f"✅ 抓取完成,共获取 {len(books_map)} 条数据")
return books_map
except Exception as e:
print(f"❌ 抓取失败: {e}")
return {}
7️⃣ 核心实现:快照管理器(Snapshot Manager)
这部分负责"存档"和"读档"。
python
import json
import os
from datetime import datetime
SNAPSHOT_DIR = "snapshots"
def save_snapshot(data_dict):
"""
将当前数据保存为带有日期的 JSON 文件
文件名示例: snapshots/books_2023-10-27.json
"""
if not os.path.exists(SNAPSHOT_DIR):
os.makedirs(SNAPSHOT_DIR)
date_str = datetime.now().strftime("%Y-%m-%d")
filename = f"books_{date_str}.json"
filepath = os.path.join(SNAPSHOT_DIR, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data_dict, f, indent=4, ensure_ascii=False)
print(f"💾 快照已保存: {filepath}")
return filepath
def load_latest_snapshot():
"""
加载最近一次的快照文件用于对比
"""
if not os.path.exists(SNAPSHOT_DIR):
return {}
# 获取目录下所有 json 文件
files = [f for f in os.listdir(SNAPSHOT_DIR) if f.endswith('.json')]
if not files:
return {}
# 按文件名排序(因为文件名包含日期,排序后最后一个就是最新的)
files.sort()
latest_file = files[-1]
filepath = os.path.join(SNAPSHOT_DIR, latest_file)
print(f"📂 加载历史快照: {latest_file}")
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
8️⃣ 核心实现:差异比对引擎(Diff Engine) 🌟核心亮点
这是本篇的灵魂!如何高效对比两份数据?
笨办法 :双重 For 循环遍历(性能极差)。
专家办法:利用 Python 集合(Set)运算。
python
def generate_diff(old_data, new_data):
"""
对比新旧数据,生成差异报告
:param old_data: 昨天的字典 {key: {detail}}
:param new_data: 今天的字典 {key: {detail}}
"""
print("\n🔍 开始执行差异比对 (Diff Engine)...")
old_keys = set(old_data.keys())
new_keys = set(new_data.keys())
# 1. 找出新增 (在 new 中但不在 old 中)
added_keys = new_keys - old_keys
# 2. 找出删除 (在 old 中但不在 new 中)
removed_keys = old_keys - new_keys
# 3. 找出共同存在的,检查内容是否变化
common_keys = old_keys & new_keys
modified_items = []
for key in common_keys:
old_item = old_data[key]
new_item = new_data[key]
# 核心:定义什么算"变化"?
# 这里我们要监控价格变化
if old_item['price'] != new_item['price']:
change_log = {
"title": key,
"old_price": old_item['price'],
"new_price": new_item['price'],
"diff_val": round(new_item['price'] - old_item['price'], 2)
}
modified_items.append(change_log)
# 汇总结果
diff_report = {
"summary": {
"total_new": len(added_keys),
"total_removed": len(removed_keys),
"total_modified": len(modified_items)
},
"details": {
"new_books": [new_data[k] for k in added_keys],
"removed_books": [old_data[k] for k in removed_keys],
"price_changes": modified_items
}
}
return diff_report
9️⃣ 运行方式与结果展示
为了让你看到真实的 Diff 效果,我在代码里手动篡改一下历史数据,模拟出"价格变动"的效果。
python
if __name__ == "__main__":
# 1. 尝试加载历史数据
history_data = load_latest_snapshot()
# --- 🧪 模拟测试桩 (Mock) 开始 ---
# 如果没有历史数据(第一次运行),我们伪造一个,方便演示 Diff 效果
if not history_data:
print("⚠️ 初次运行,正在生成模拟历史数据以演示 Diff 效果...")
history_data = {
"A Light in the Attic": {"price": 51.77, "title": "A Light in the Attic"}, # 价格没变
"Tipping the Velvet": {"price": 100.00, "title": "Tipping the Velvet"}, # 模拟:昨天这书卖 100 块 (今天实际是 53.74,说明降价了)
"Old Outdated Book": {"price": 10.00, "title": "Old Outdated Book"} # 模拟:昨天有这本书,今天没了 (下架)
}
# --- 🧪 模拟测试桩 (Mock) 结束 ---
# 2. 抓取最新数据
current_data = fetch_all_books()
# 3. 执行比对
report = generate_diff(history_data, current_data)
# 4. 输出报告
print("\n" + "="*40)
print("📊 每日变动监控报告")
print("="*40)
print(f"🟢 新增上架: {report['summary']['total_new']} 本")
for book in report['details']['new_books'][:3]:
print(f" + {book['title']} (Price: {book['price']})")
print(f"🔴 下架商品: {report['summary']['total_removed']} 本")
for book in report['details']['removed_books']:
print(f" - {book['title']}")
print(f"🟡 价格变动: {report['summary']['total_modified']} 本")
for item in report['details']['price_changes']:
emoji = "📉 降价" if item['diff_val'] < 0 else "📈 涨价"
print(f" {emoji} {item['title']}: {item['old_price']} -> {item['new_price']} (变动: {item['diff_val']})")
# 5. 保存今天的快照,作为明天的历史
save_snapshot(current_data)
运行结果示例:
text
📂 加载历史快照: books_2023-10-26.json
🔄 正在从网络抓取最新数据...
✅ 抓取完成,共获取 20 条数据
🔍 开始执行差异比对 (Diff Engine)...
========================================
📊 每日变动监控报告
========================================
🟢 新增上架: 18 本
+ Soumission (Price: 50.1)
+ Sharp Objects (Price: 47.82)
+ Sapiens: A Brief History of Humankind (Price: 54.23)
🔴 下架商品: 1 本
- Old Outdated Book
🟡 价格变动: 1 本
📉 降价 Tipping the Velvet: 100.0 -> 53.74 (变动: -46.26)
💾 快照已保存: snapshots/books_2023-10-27.json
🔟 常见问题与排错(FAQ)
-
文件读写锁(Permission Denied)
- 问题:脚本正在写 JSON 时,你手动用记事本打开了该文件,导致写入失败。
- 解法 :使用
with open(...)句柄自动管理,尽量不要在运行时手动操作数据文件。
-
主键冲突(Key Error/Duplicate)
- 问题:如果用"书名"做 Key,遇到两本同名书(比如不同出版社)就会导致数据被覆盖。
- 解法 :强烈建议使用 UPC 或 SKU ID 。如果网页没提供,可以用
Hash(书名 + 作者)生成唯一指纹。
-
浮点数精度问题
- 问题 :
19.99变成了19.990000001,导致 Diff 误判为价格变动。 - 解法 :在对比价格时,要么转为
Decimal类型,要么使用round(price, 2)统一保留两位小数再对比。
- 问题 :
1️⃣1️⃣ 进阶优化(加分项)
-
消息推送(Webhook) :
监测到"降价"逻辑后,不要只 print,直接调用 Slack/钉钉/飞书的 Webhook 机器人接口,把降价信息推送到手机上。
pythondef send_alert(message): requests.post("https://oapi.dingtalk.com/robot/send?access_token=xxx", json={"msg": message}) -
数据库版本控制 :
当 JSON 文件大到几百 MB 时,读写会很慢。此时应升级为 SQLite/PostgreSQL 。设计一张
product_history表,字段包含(upc, price, check_date),利用 SQL 窗口函数计算差值。 -
Hash 校验 :
如果字段非常多(几十个),逐个字段对比很慢。可以把所有字段拼成字符串计算 MD5 哈希存下来。下次抓取只比对 MD5,不一样了再细究是哪个字段变了。
1️⃣2️⃣ 总结与延伸阅读
这套系统的核心不在于"爬",而在于 "比"。
通过 快照(Snapshot) -> 对比(Diff) -> 归档(Archive) 的三部曲,我们成功把静态的数据变成了动态的情报。
复盘总结:
- 我们复用了 Requests 爬虫。
- 我们引入了 JSON 序列化作为持久层。
- 我们利用 Python 的 Set 集合运算实现了 O(N) 复杂度的高效比对。
下一步方向:
试试把这个脚本放到服务器的 crontab 里,设置为每天早上 8 点执行。一周后,打开 diff_reports 文件夹,你会发现数据的变化趋势图非常有意思!📈
如果你想学怎么把这些差异数据画成K线图,请随时 Call 我,咱们下回分解!😎👋
🌟 文末
好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
✅ 专栏持续更新中|建议收藏 + 订阅
墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?
评论区留言告诉我你的需求,我会优先安排实现(更新)哒~
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
✅ 免责声明
本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。
使用或者参考本项目即表示您已阅读并同意以下条款:
- 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
- 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
- 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
- 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
