京东面试题:说一下Scrapy如何去重?如何自定义去重规则?
在 Scrapy 爬虫框架中,数据去重是一个关键环节,直接影响爬取效率和数据质量。
默认情况下,Scrapy 主要通过 URL 作为唯一标识符 进行去重,但它的去重机制远不止于此,还可以基于请求内容、请求头、Cookies 甚至自定义规则来优化去重策略。
今天,我们就来深入探讨 Scrapy 的去重原理,并提供实战代码示例,最后给出面试中常见的去重问题及最优回答。
Scrapy 的去重是由 调度器(Scheduler) 负责的。每当一个新的请求(Request)进入 Scrapy 时,调度器会检查该请求是否已经被处理过,具体的去重机制如下:
1. Scrapy 默认的去重机制
Scrapy 默认使用 RFPDupeFilter 进行去重,该类基于 请求的 URL 进行去重,它的工作方式如下:
Scrapy 在 请求队列(Scheduler) 中维护了一个 已访问 URL 的哈希表。 每当一个新请求进入时,Scrapy 计算该请求的 URL 哈希值,并检查是否已经存在于该哈希表中: 如果存在:该请求被丢弃,避免重复爬取相同的页面。 如果不存在:该 URL 会被添加到哈希表中,并进入爬取队列。
Scrapy 的默认去重基于 RFPDupeFilter,该类的核心代码位于 scrapy.dupefilters 模块,核心逻辑如下:
import hashlibclass RFPDupeFilter:
def request_fingerprint(self, request):
# 计算请求的唯一指纹(基于URL)
return hashlib.sha1(request.url.encode()).hexdigest()
def request_seen(self, request):
fp = self.request_fingerprint(request)
if fp in self.fingerprints:
return True # URL 已经访问过,丢弃请求
self.fingerprints.add(fp) # 记录新 URL
return False
2. Scrapy 持久化去重
默认情况下,Scrapy 在内存中维护去重信息(即存储于 Python 的 set 结构中)。但是,如果爬虫因意外终止或重启,去重记录会丢失,导致重复爬取已访问的 URL。因此,我们可以通过 持久化去重 来解决这个问题。
Scrapy 允许使用 DUPEFILTER_CLASS 设置自定义去重过滤器。例如,可以使用 scrapy_redis 来实现 Redis 持久化去重:
# 在 settings.py 中启用 Redis 去重过滤器
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER_PERSIST = True
这样,爬取的 URL 记录会被存储在 Redis 中,即使爬虫重启也不会重复爬取相同的页面。
3. 自定义去重策略
Scrapy 默认的去重仅基于 URL,但有些场景下,仅仅依赖 URL 并不足够,比如:
不同的请求方法(GET/POST) 不同的请求参数 相同的 URL 但返回不同的页面内容
此时,我们需要自定义去重逻辑。Scrapy 允许我们重写 request_fingerprint 方法,以不同的方式计算请求的唯一标识。
例如,假设我们要基于 URL + 请求参数 进行去重:
from scrapy.dupefilters import RFPDupeFilter
import hashlibclass CustomDupeFilter(RFPDupeFilter):
def request_fingerprint(self, request):
# 使用 URL + 请求参数 生成唯一指纹
data = request.url + str(request.body) # 结合 URL 和 body
return hashlib.sha1(data.encode()).hexdigest()
然后,在 settings.py 中启用这个自定义去重类:
DUPEFILTER_CLASS = "myproject.custom_filters.CustomDupeFilter"
这样,即使 URL 相同,但 POST 请求的参数不同,Scrapy 也不会认为它是重复请求。
4. 如何在 Scrapy 爬虫中查看去重情况
如果你想查看 Scrapy 处理的去重数据,可以在 调试模式 下运行爬虫,并启用 DUPEFILTER_DEBUG 选项:
DUPEFILTER_DEBUG = True
这将允许 Scrapy 在日志中输出重复请求的信息,比如:
Filtered duplicate request: <GET https://example.com/page1> (referer: None)
此外,你也可以通过 stats 访问去重的数量:
from scrapy.statscollectors import StatsCollectorclass MySpider(scrapy.Spider):
name = "my_spider"
def parse(self, response):
duplicates = self.crawler.stats.get_value("dupefilter/filtered", 0)
self.logger.info(f"去重的请求数量: {duplicates}")
Scrapy 的去重机制是面试中常见的考点,下面是回答:
Scrapy 默认使用
RFPDupeFilter进行去重,主要基于 URL 进行哈希存储,如果请求的 URL 在去重集合中已存在,则会被过滤掉。默认情况下,去重记录存储在内存中,并不会持久化。但是,如果爬虫重启,去重信息会丢失。为了支持持久化去重,我们可以使用
scrapy_redis,将去重数据存储在 Redis 中:DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"如果默认的 URL 去重不满足需求,我们还可以自定义去重规则,比如:
基于 URL + 请求参数 基于 Headers 基于完整的请求体 自定义去重可以通过 重写
request_fingerprint方法 来实现:class CustomDupeFilter(RFPDupeFilter):
def request_fingerprint(self, request):
data = request.url + str(request.body) # 结合 URL 和 body 生成指纹
return hashlib.sha1(data.encode()).hexdigest()这样,即使 URL 相同,但请求参数不同,Scrapy 也不会认为它是重复的请求。
Scrapy 的去重机制虽然默认基于 URL,但可以通过 持久化存储 和 自定义规则 来优化。无论是应对爬取大规模网站,还是处理复杂请求,我们都可以通过灵活调整 Scrapy 的去重策略来提高爬取效率。
最后,我为大家打造了一份deepseek的入门到精通教程,完全免费:https://www.songshuhezi.com/deepseek
也可以看我写的这篇文章《DeepSeek满血复活,直接起飞!》来进行本地搭建。
对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》。