Scrapy中如何延迟请求的必要性校验,避免不必要的HTTP请求?
我太懂你这种郁闷了——本来想着爬一个父页面就能批量更新几十上百个子页面的parent_page_id,结果Scrapy一启动就把一万五千个待处理的子页面请求全给塞到调度队列里了,根本没给数据库更新留时间,最后白白做一堆无用的HTTP请求,完全违背了优化效率的初衷对吧?
其实核心问题在于:Scrapy的Request一旦被yield出去,就会被调度器加入队列,默认不会再去校验它是否还有执行的必要。要解决这个问题,我们需要把“是否需要请求”的校验逻辑延迟到请求即将发送到HTTP服务器的那一刻,这时候用Scrapy的下载中间件(DownloaderMiddleware)就能完美实现。
具体实现步骤
1. 自定义下载中间件,拦截请求前做校验
下载中间件的process_request方法是每个请求被发送前的最后一道关卡,我们可以在这里重新查询数据库,检查对应子页面的parent_page_id是否已经被更新,从而决定是否跳过这个请求。
在你的爬虫项目的middlewares.py里添加如下中间件:
from scrapy.exceptions import IgnoreRequest import logging import threading from your_project_name.enums import TagType # 替换成你实际的TagType路径 class CheckRequestNecessityMiddleware: def __init__(self, crawler): self.crawler = crawler # 从爬虫实例获取数据库游标(假设你的爬虫里已经初始化了cur) self.db_cursor = crawler.spider.cur # 加线程锁,避免多线程下数据库操作冲突 self.db_lock = threading.Lock() @classmethod def from_crawler(cls, crawler): # 从crawler初始化中间件,方便获取爬虫资源 return cls(crawler) def process_request(self, request, spider): # 只对子页面类型的请求做校验 if request.cb_kwargs.get('tag_type') == TagType.CHILD: child_tag = request.cb_kwargs.get('tag') # 加锁后查询数据库,确保线程安全 with self.db_lock: self.db_cursor.execute( "select parent_page_id from children where tag = %s;", (child_tag,) ) query_result = self.db_cursor.fetchone() # 如果parent_page_id已经存在,直接跳过这个请求 if query_result and query_result[0] is not None: logging.debug(f"[跳过请求] 子页面 {child_tag} 的parent_page_id已更新,无需重复请求") raise IgnoreRequest(f"子页面 {child_tag} 无需执行请求") # 返回None表示让请求继续执行 return None
2. 在settings.py中启用这个中间件
找到项目的settings.py,在DOWNLOADER_MIDDLEWARES配置里添加自定义中间件,注意中间件的优先级(数值越小越先执行,这里选543是因为要在默认的下载中间件之后执行):
DOWNLOADER_MIDDLEWARES = { # 其他中间件... 'your_project_name.middlewares.CheckRequestNecessityMiddleware': 543, }
3. 调整原有爬虫代码(可选,但建议保留初始过滤)
你原来的_needs_call方法可以继续保留,作为初始过滤,提前排除已经处理过的子页面,减少进入调度队列的请求数量;而中间件的校验则作为最终防线,确保即将发送的请求确实是必要的。
一些关键注意事项
- 数据库性能优化:如果请求量很大,每次都查数据库可能会有性能瓶颈,可以考虑用Redis之类的缓存存储已经更新过的子页面tag,减少数据库查询次数。
- 线程安全:如果你的数据库连接不是线程安全的(比如普通的MySQLdb游标),一定要加线程锁,避免多线程下出现数据混乱或报错。
- 异常处理:
IgnoreRequest属于Scrapy的正常忽略逻辑,不会触发你的errback方法,如果需要记录跳过的请求,直接在中间件里打日志即可。
这样修改后,每次子页面请求即将被发送时,都会重新检查数据库的最新状态——如果之前爬过的父页面已经更新了这个子页面的parent_page_id,就会直接跳过请求,彻底避免无用的HTTP调用,完美实现你想要的高效爬取逻辑。
内容来源于stack exchange

