前两篇讲了数据清洗和断点续爬,这篇讲爬虫数据的持久化和增量更新------数据存进去了,怎么保证下次爬的时候不重复、不丢失、保持最新。
一、数据持久化的三种方案
| 方案 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|
| JSON/CSV 文件 | 数据量小,一次性采集 | 简单,不用装数据库 | 查询慢,不支持并发 |
| MySQL | 数据量大,需要查询统计 | 支持 SQL,适合分析 | 需要提前建表 |
| MongoDB | 数据结构不固定 | 灵活,不用建表 | 不擅长关联查询 |
二、MySQL 持久化最佳实践
1. 建表策略
sql
-- 爬虫数据表,加上唯一约束和索引
CREATE TABLE `crawled_products` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`source_id` VARCHAR(100) NOT NULL COMMENT '原始ID(用于去重)',
`title` VARCHAR(200) NOT NULL COMMENT '标题',
`price` DECIMAL(10,2) DEFAULT 0 COMMENT '价格',
`brand` VARCHAR(100) DEFAULT '' COMMENT '品牌',
`category` VARCHAR(100) DEFAULT '' COMMENT '分类',
`url` VARCHAR(500) DEFAULT '' COMMENT '原文链接',
`raw_data` JSON COMMENT '原始数据(JSON格式)',
`crawl_time` DATETIME NOT NULL COMMENT '爬取时间',
`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
`is_deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除',
-- 唯一索引(用于去重)
UNIQUE KEY `uk_source_id` (`source_id`),
-- 查询索引
KEY `idx_category` (`category`),
KEY `idx_crawl_time` (`crawl_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='爬虫商品数据';
关键设计:
source_id存原始商品 ID,加上唯一索引,实现天然去重raw_data存原始 JSON,方便以后回溯update_time记录更新时间,支持增量更新
2. INSERT ... ON DUPLICATE KEY UPDATE(核心)
python
import pymysql
from datetime import datetime
class MySQLCrawlerStorage:
"""爬虫数据 MySQL 存储"""
def __init__(self):
self.conn = pymysql.connect(
host="localhost",
user="root",
password="123456",
database="spider_db",
charset="utf8mb4",
)
self.cursor = self.conn.cursor()
def save_product(self, item):
"""
保存商品数据(存在则更新,不存在则插入)
ON DUPLICATE KEY UPDATE 是核心去重逻辑
"""
sql = """
INSERT INTO crawled_products
(source_id, title, price, brand, category, url, crawl_time, update_time)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
title = VALUES(title),
price = VALUES(price),
brand = VALUES(brand),
category = VALUES(category),
update_time = VALUES(update_time)
"""
now = datetime.now()
values = (
item["source_id"], # 原始 ID
item["title"],
item["price"],
item["brand"],
item["category"],
item["url"],
now, # 首次爬取时间
now, # 更新时间
)
self.cursor.execute(sql, values)
self.conn.commit()
def save_batch(self, items):
"""批量保存"""
sql = """
INSERT INTO crawled_products
(source_id, title, price, brand, category, url, crawl_time, update_time)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
title = VALUES(title),
price = VALUES(price),
brand = VALUES(brand),
update_time = VALUES(update_time)
"""
now = datetime.now()
values_list = [
(
item["source_id"], item["title"],
item["price"], item["brand"],
item["category"], item["url"],
now, now,
)
for item in items
]
self.cursor.executemany(sql, values_list)
self.conn.commit()
print(f"批量保存 {len(items)} 条,影响行数: {self.cursor.rowcount}")
def close(self):
self.cursor.close()
self.conn.close()
核心逻辑: ON DUPLICATE KEY UPDATE 表示------如果 source_id 重复了就更新价格等信息,否则插入新记录。这是增量更新的灵魂。
3. 爬虫中的完整用法
python
import requests
from bs4 import BeautifulSoup
import time
class ProductCrawler:
def __init__(self):
self.storage = MySQLCrawlerStorage()
self.session = requests.Session()
def crawl_list(self, category, pages=5):
"""爬取指定分类的商品列表"""
for page in range(1, pages + 1):
url = f"https://example.com/products?cat={category}&page={page}"
resp = self.session.get(url)
soup = BeautifulSoup(resp.text, "html.parser")
items = []
for product in soup.select(".product-item"):
items.append({
"source_id": product.get("data-id"), # 原始商品ID
"title": product.select_one(".title").text.strip(),
"price": product.select_one(".price").text.strip(),
"brand": product.select_one(".brand").text.strip(),
"category": category,
"url": product.select_one("a").get("href"),
})
# 批量保存到 MySQL(自动去重和更新)
self.storage.save_batch(items)
print(f"第 {page} 页完成,保存 {len(items)} 条")
time.sleep(1)
def run(self):
"""定时执行"""
print(f"开始增量采集: {datetime.now()}")
for cat in ["手机", "电脑", "平板"]:
self.crawl_list(cat, pages=5)
print("采集完成")
# 生成统计报告
self.report()
def report(self):
"""统计本次采集结果"""
sql = """
SELECT category, COUNT(*) AS total,
SUM(IF(crawl_time = update_time, 1, 0)) AS new,
SUM(IF(crawl_time != update_time, 1, 0)) AS updated
FROM crawled_products
WHERE DATE(update_time) = CURDATE()
GROUP BY category
"""
self.cursor.execute(sql)
for row in self.cursor.fetchall():
print(f"分类: {row[0]}, 总数: {row[1]}, 新增: {row[2]}, 更新: {row[3]}")
def close(self):
self.storage.close()
# 每日执行
crawler = ProductCrawler()
crawler.run()
crawler.close()
三、爬虫与 API 接口整合
如果爬虫数据需要对外提供查询接口,可以整合到 Flask/FastAPI 中:
python
from flask import Flask, jsonify, request
import pymysql
app = Flask(__name__)
def get_db():
return pymysql.connect(
host="localhost", user="root",
password="123456", database="spider_db",
charset="utf8mb4",
)
@app.route("/api/products")
def product_list():
"""提供 RESTful 接口查询爬虫数据"""
conn = get_db()
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 分页参数
page = int(request.args.get("page", 1))
size = int(request.args.get("size", 20))
category = request.args.get("category")
# 查询
where = "WHERE is_deleted = 0"
params = []
if category:
where += " AND category = %s"
params.append(category)
sql = f"SELECT * FROM crawled_products {where} ORDER BY update_time DESC LIMIT %s OFFSET %s"
cursor.execute(sql, params + [size, (page - 1) * size])
products = cursor.fetchall()
# 统计总数
cursor.execute(f"SELECT COUNT(*) AS total FROM crawled_products {where}", params)
total = cursor.fetchone()["total"]
cursor.close()
conn.close()
return jsonify({
"code": 200,
"data": products,
"total": total,
"page": page,
"size": size,
})
if __name__ == "__main__":
app.run(port=5000)
四、自动同步的定时策略
python
import schedule
import time
def daily_sync():
"""每日增量同步"""
print(f"开始每日同步: {datetime.now()}")
crawler = ProductCrawler()
try:
crawler.run()
finally:
crawler.close()
# 每天早上 6:00 执行
schedule.every().day.at("06:00").do(daily_sync)
# 也可以用固定间隔(每2小时)
# schedule.every(2).hours.do(daily_sync)
while True:
schedule.run_pending()
time.sleep(60) # 每分钟检查一次
五、数据一致性保障
python
def atomic_sync(self):
"""
原子化同步:要么全部成功,要么全部回滚
防止同步到一半程序崩溃导致数据不完整
"""
try:
self.conn.begin() # 开启事务
for item in items:
sql = """
INSERT INTO crawled_products (...) VALUES (...)
ON DUPLICATE KEY UPDATE ...
"""
self.cursor.execute(sql, values)
self.conn.commit() # 全部成功才提交
print(f"✅ 成功同步 {len(items)} 条")
except Exception as e:
self.conn.rollback() # 出错就全部回滚
print(f"❌ 同步失败,已回滚: {e}")
六、MySQL vs MongoDB 选择建议
继续沿用之前的建议:
| 场景 | 推荐存储 |
|---|---|
| 同一种商品,字段固定,需要 SQL 统计分析 | ✅ MySQL |
| 多种来源数据,字段不一,优先灵活存储 | ✅ MongoDB |
| 数据量百万级以上,需要快速搜索 | ✅ Elasticsearch |
新手建议: 先用 MySQL,结构清晰,遇到问题好排查。等用熟了再尝试 MongoDB。
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。