Scrapy检测到重复记录时如何实时停止分页爬取
Scrapy检测到重复记录后立即终止分页爬取的实现方案
问题根因
你写的标志位逻辑不生效,核心原因是没匹配Scrapy的异步调度机制:
parse方法里的for循环是同步执行的,会一次性把当前页所有新闻详情请求、下一页列表请求全部生成并推入Scrapy调度队列,整个过程不会等待之前yield的请求执行完成。等详情请求被调度、跑parse_item修改duplicate_record_flag的时候,当前页的所有请求早就已经入队,自然没法在for循环迭代时实时拿到最新的标志位状态,才会出现当前页所有链接全被爬取的问题。
实现方案
方案1:列表页前置重复判断(性能最优,优先推荐)
绝大多数新闻类网站的列表页会直接展示新闻标题,和详情页的title字段完全一致,完全不需要等进入详情页再查库判断重复。直接把重复校验逻辑移到列表页解析阶段,从源头拦截无效请求,一旦碰到重复内容直接停爬,性能最高、逻辑最可靠。
修改后的代码参考:
class NewsSpider(scrapy.Spider): name = 'news' allowed_domains = ['....'] start_urls = ['....'] def parse(self, response, **kwargs): next_page = response.xpath('//a[@class="next page-numbers"]/@href').get() # 同时提取每条新闻的链接和标题 news_items = response.xpath('//div[@class="content-column"]/div/article/div/div[1]/a') for news in news_items: link = news.xpath('./@href').get() # 注意:这里的标题xpath要根据你实际页面结构调整,和你存库的title字段保持一致 title = news.xpath('.//h2/text()').get('').strip() # 同步查库判断重复,命中立刻停爬 if CryptocurrencyNews.objects.filter(title=title).exists(): self.crawler.engine.close_spider(self, reason='Detect duplicate record, stop crawling') return # 确认是新内容才发详情页请求 yield scrapy.Request(url=link, callback=self.parse_item) # 当前页所有内容都是新内容,才继续爬取下一页 if next_page: yield scrapy.Request(url=next_page, callback=self.parse) def parse_item(self, response): item = CryptocurrencyNewsItem() # 原有详情解析、存库逻辑即可,不需要再做重复判断 # ... item.save() return item
方案2:必须进详情页判断重复的适配方案
如果遇到特殊场景,列表页标题和详情页存库标题不一致、必须进入详情页才能做重复校验,做两个调整即可:
- 用Scrapy自带的stats全局存储代替自定义类变量做重复标记,避免多协程下的状态不同步问题
- 给请求设置优先级,保证当前页所有详情请求优先于下一页列表请求调度,同时触发重复时直接调用引擎方法关闭爬虫,清空调度队列内的剩余请求
代码参考:
class NewsSpider(scrapy.Spider): name = 'news' allowed_domains = ['....'] start_urls = ['....'] def parse(self, response, **kwargs): # 先检查重复标记,命中直接终止 if self.crawler.stats.get_value('has_duplicate'): return next_page = response.xpath('//a[@class="next page-numbers"]/@href').get() news_links = response.xpath('//div[@class="content-column"]/div/article/div/div[1]/a/@href').getall() for link in news_links: if self.crawler.stats.get_value('has_duplicate'): return # 详情页请求设置高优先级,优先调度 yield scrapy.Request(url=link, callback=self.parse_item, priority=10) if next_page and not self.crawler.stats.get_value('has_duplicate'): # 下一页请求低优先级,等当前页所有详情处理完再调度 yield scrapy.Request(url=next_page, callback=self.parse, priority=0) def parse_item(self, response): item = CryptocurrencyNewsItem() # 原有详情解析逻辑 # ... if CryptocurrencyNews.objects.filter(title=item['title']).exists(): self.crawler.stats.set_value('has_duplicate', True) # 直接调用引擎方法关闭爬虫,清空剩余待调度请求 self.crawler.engine.close_spider(self, reason='Detect duplicate record, stop crawling') return item.save() return item
注意事项
- 要确认目标网站的新闻列表是按发布时间倒序排列(最新内容在最前),这样碰到第一条重复内容时,后续内容都是已经爬过的历史数据,停爬不会漏新帖子,这是增量爬取的基本前提
- 增量爬取不需要太高并发,建议把settings里的
CONCURRENT_REQUESTS设为2-4即可,避免触发停爬前已经调度了过多无效请求 - 不要在回调里靠
raise CloseSpider停爬,这个异常只能终止当前回调的执行,没法清空已经进入调度队列的请求,直接调用self.crawler.engine.close_spider()是最可靠的立即停爬方式
内容的提问来源于stack exchange,提问作者Mehrdad HMT
相关产品推荐
相关产品推荐

