爬虫数据持久化与增量更新——从爬下来到保持最新

前两篇讲了数据清洗和断点续爬,这篇讲爬虫数据的持久化和增量更新------数据存进去了,怎么保证下次爬的时候不重复、不丢失、保持最新。

一、数据持久化的三种方案

方案 适合场景 优点 缺点
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/爬虫 实战干货,不让你白来。