Scrapy无限爬取循环问题:寻求替代DEPTH_LIMIT的解决方案
这个问题我碰到过好多次了——用DEPTH_LIMIT确实是饮鸩止渴,一刀切限制深度会把正常的长序列分页也给拦了,太不灵活。给你几个更精准的解决思路,按需选就行:
方案1:给LinkExtractor加精准的URL规则
如果你用的是CrawlSpider,直接通过LinkExtractor的allow参数写正则,把允许爬取的URL路径限制在合理范围内,从根源上切断循环的可能。比如你只需要爬取/one开头、最多3个路径段的URL(比如/one、/one/second、/one/second/third),就可以这么写:
from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class ExampleSpider(CrawlSpider): name = 'example' allowed_domains = ['example.com'] start_urls = ['https://example.com/one'] rules = ( # 只允许/one开头,路径段不超过3个的URL Rule(LinkExtractor(allow=r'^/one(/[^/]+){0,2}$'), callback='parse_item', follow=True), )
这样一来,那些不断叠加的循环链接(比如/one/second/third/second/one)因为路径段超过3个,直接就被过滤掉了。如果你的正常分页是/one/page/1、/one/page/2这种,只要把正则改成r'^/one(/page/\d+|/[^/]+){0,2}$'就行,完全不影响长序列爬取。
方案2:自定义去重过滤器(DupeFilter)
如果你的爬取规则太灵活,没法用固定正则卡死,那就自己写个去重逻辑,专门抓路径里的循环模式。Scrapy默认的去重是看完整URL,但循环生成的URL是新的,所以我们得从路径段入手——比如检测是否出现连续重复的段组合。
先在项目里建个dupefilters.py,写自定义的去重类:
from scrapy.dupefilters import RFPDupeFilter from scrapy.utils.request import request_fingerprint class PathCycleDupeFilter(RFPDupeFilter): def __init__(self, path, debug): super().__init__(path, debug) # 存储已处理过的路径段序列指纹 self.path_fingerprints = set() def request_seen(self, request): # 提取URL的路径段,拆分列表 path_segments = request.url.split('://')[1].split('/')[1:] # 检测是否出现连续重复的两段组合(比如.../second/third/second/third) if len(path_segments) >= 4: last_two = tuple(path_segments[-2:]) prev_two = tuple(path_segments[-4:-2]) if last_two == prev_two: return True # 判定为循环,跳过 # 保留默认的URL指纹去重逻辑 fp = request_fingerprint(request) if fp in self.fingerprints: return True self.fingerprints.add(fp) return False
然后在settings.py里指定使用这个自定义过滤器:
DUPEFILTER_CLASS = 'your_project_name.dupefilters.PathCycleDupeFilter'
这个逻辑能自动识别路径中的循环模式,不会干扰正常的长序列分页(比如/one/page/1→/one/page/2这种路径段不会重复循环,所以不会被拦截)。
方案3:在解析函数里手动过滤链接
要是你用的是普通Spider,不用CrawlSpider那套规则,就在parse方法里自己处理链接。比如提取到相对链接转成绝对URL后,拆分路径段,针对性过滤掉会触发循环的组合:
from scrapy import Spider, Request from urllib.parse import urljoin class ExampleSpider(Spider): name = 'example' allowed_domains = ['example.com'] start_urls = ['https://example.com/one'] def parse(self, response): # 处理当前页面内容... # 提取并过滤链接 for href in response.css('a::attr(href)').getall(): absolute_url = urljoin(response.url, href) path_segments = absolute_url.split('://')[1].split('/')[1:] # 根据实际循环模式过滤,比如跳过同时包含second/third和third/second的URL if len(path_segments) >= 3: if 'second/third' in absolute_url and 'third/second' in absolute_url: continue yield Request(absolute_url, callback=self.parse)
这种方法自由度最高,你可以根据实际观察到的循环模式,精准过滤掉问题链接,完全不影响正常爬取流程。
优先推荐方案1,简单直接又精准;如果规则复杂就用方案2;普通Spider就选方案3。这些方法都不会影响正常的长序列分页爬取,完美解决DEPTH_LIMIT的痛点。
内容的提问来源于stack exchange,提问作者Maxime De Bruyn

