增量抓取实战:Redis 指纹校验、带宽优化

📂 所属阶段:第四阶段 — 实战演练(项目开发篇)


1. 什么是增量爬虫?

在讲代码前,得先把增量爬虫和「普通全量爬虫」的区别理明白——全量爬虫每次启动都会把所有目标页面再爬一遍,不管内容有没有变;而增量爬虫,只会抓取「第一次出现」或「已更新过」的页面

对新手来说,这一点在练手阶段可能没什么感觉,但到了生产级项目(比如监控竞品价格、爬取每日热点聚合站),全量爬取的坏处会直接暴露:

  • 浪费服务器/云主机的流量带宽
  • 拖慢爬取速度和更新频率
  • 容易触发目标网站的反爬阈值

所以今天的实战,就用 Scrapy + Redis 实现最基础但场景通用的 URL 指纹式增量爬虫


2. 实战代码:URL 指纹校验版增量爬虫

2.1 前置准备

你得先装好项目依赖:

pip install scrapy redis

并且本地/远程启动 Redis 服务(默认端口 6379,不需要密码的情况演示更方便,生产记得加认证哦)。

2.2 完整代码解析

import scrapy
import redis
import hashlib
# 引入 Scrapy 自带的 Request(原代码漏写了,补全)
from scrapy.http import Request

class IncrementalSpider(scrapy.Spider):
    # 爬虫名称,Scrapy 启动时必须指定的参数
    name = "incremental_demo"
    # 允许爬取的域名范围(避免跑飞到其他网站)
    allowed_domains = ["quotes.toscrape.com"]
    # 起始 URL(用 Scrapy 官方提供的测试爬虫网站,练手很安全)
    start_urls = ["https://quotes.toscrape.com/"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 连接 Redis 服务
        # 生产环境建议把 host/port/db/password 放到 scrapy.settings 里统一管理
        self.redis_client = redis.Redis(
            host="127.0.0.1",
            port=6379,
            db=0,
            decode_responses=True  # 开启后 Redis 返回的是字符串,不需要手动 .decode()
        )

    def parse(self, response):
        # 遍历当前页的所有名言卡片
        for quote_card in response.css("div.quote"):
            # 提取名言详情页的 URL
            detail_url = quote_card.css("span small a::attr(href)").get()
            # 拼接完整 URL(测试站返回的是相对路径)
            full_detail_url = response.urljoin(detail_url)

            # 生成 URL 指纹(这里用 MD5,速度快;对防碰撞要求极高的场景可以换 SHA256)
            url_fingerprint = hashlib.md5(full_detail_url.encode("utf-8")).hexdigest()

            # Redis 键值对设计:键前缀 + 指纹 = 唯一标识
            redis_key = f"crawled_urls:{url_fingerprint}"

            # 检查 Redis 中是否已存在该键
            # 如果不存在 → 是新 URL → 发起详情页请求 + 记录指纹
            if not self.redis_client.exists(redis_key):
                yield Request(
                    url=full_detail_url,
                    callback=self.parse_quote_detail,
                    # 可以加 meta 传参(比如提取卡片上的作者名)
                    meta={"author": quote_card.css("span small.author::text").get()}
                )
                # 设置指纹键(可以加 expire 参数,比如只保留 7 天内爬过的,避免 Redis 内存溢出)
                self.redis_client.set(redis_key, "1", ex=604800)  # 604800秒=7天

        # 处理下一页
        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

    # 解析名言详情页的回调函数
    def parse_quote_detail(self, response):
        yield {
            "author": response.meta["author"],
            "author_birth_date": response.css("span.author-born-date::text").get(),
            "author_birth_place": response.css("span.author-born-location::text").get(),
            "quote": response.css("div.quote span.text::text").get().strip('“”'),
            "tags": response.css("div.quote div.tags a.tag::text").getall()
        }

2.3 启动爬虫测试

在终端(命令行)中进入 Scrapy 项目根目录,执行:

scrapy crawl incremental_demo -o quotes.json

第一次运行时,Scrapy 会爬完全站的名言详情页,生成带完整作者信息的 quotes.json第二次运行时,你会发现终端里只有「起始页请求」「下一页请求」,完全没有发起详情页请求——这就是增量爬取生效的标志!


3. 扩展优化方向

3.1 不是所有场景都只用 URL 指纹!

有些网站的 URL 是固定的,但内容会动态更新(比如新闻首页、电商商品详情页),这时候 URL 指纹就没用了,你可以:

  • 响应内容的哈希值 当指纹(但要过滤掉页面上动态生成的随机元素,比如广告位、时间戳、访客统计 ID 等)
  • 目标页面的 Last-Modified / ETag 响应头 做时间戳/校验比较(很多静态/半静态网站都会提供)
  • 配合 关系型数据库/Elasticsearch 存储已爬取的内容关键信息(比如电商商品的价格、库存变化)

3.2 生产级 Redis 配置建议

  • 不要用默认的 db=0,单独开一个数据库放爬虫指纹
  • 一定要加密码认证
  • 集群部署时用 Redis Cluster 代替单机 Redis
  • 内存不足时开启 Redis 的 LRU(最近最少使用)淘汰策略

4. 小结

增量抓取的核心逻辑其实很简单:先判断「要不要爬」,再决定「爬不爬」

今天演示的 URL 指纹版增量爬虫,虽然基础,但适合绝大多数「静态列表页推新详情页」的场景——比如:

  • 监控新闻资讯站的新文章
  • 爬取论坛的新帖子
  • 同步电商平台的新商品

💡 记住:在生产级爬虫项目中,增量抓取是性价比最高的优化手段之一,没有之一。


🔗 扩展阅读