Scrapy结合Pika对接RabbitMQ时,爬虫异常如何实现消息重入队?
问题根因
你的判断完全准确,问题的核心出在Scrapy的异步实现特性上:
CrawlerRunner.crawl()方法基于Twisted异步框架开发,调用后不会阻塞等待爬虫全流程(包括页面爬取、数据解析、存储、资源释放)执行完成,只会立刻返回一个代表异步任务的Deferred对象就直接返回。- 原有代码执行顺序是:调用
run_spider启动爬虫 -> 代码立刻走到basic_ack删除消息 -> 爬虫才在后台异步跑起来。 - 你觉得“正常运行时流程符合预期”本质是巧合:因为消息确认动作根本不等爬虫执行结束,只是正常场景下爬虫没有中途报错,你没察觉到消息已经被提前删除了;异步流程里抛出的异常根本不会被外层的同步try/except捕获,自然也就触发不了消息重新入队的逻辑。
修复逻辑
核心思路是不要在启动爬虫的同步流程里直接做消息确认,要把消息确认的动作绑定到爬虫异步流程的回调上,只有爬虫流程真正走完、无异常时才执行ack,任意异常都走reject把消息重新放回队列:
- 拿到
crawl()返回的Deferred异步对象 - 给对象绑定成功回调:爬虫全流程正常跑完时,执行
basic_ack删除消息 - 给对象绑定失败回调:爬虫任意阶段抛出异常时,执行
basic_reject把消息重新入队 - 单独捕获爬虫启动阶段的同步异常,避免启动失败时消息一直悬挂
调整后的核心代码
def get_message(used_channel, basic_deliver, properties, body): decodebody = bytes.decode(body) # 缓存当前消息的标识,供异步回调使用 cur_delivery_tag = basic_deliver.delivery_tag def on_crawl_success(_): # 只有爬虫全流程正常执行完成才会触发该回调 used_channel.basic_ack(delivery_tag=cur_delivery_tag) logger.info(f"爬虫任务执行完成,消息已确认: {decodebody}") def on_crawl_error(failure): # 爬虫任意阶段抛错都会触发该回调,failure对象包含完整异常栈 logger.error(f"爬虫执行异常,消息将重新入队,异常信息: \n{failure.getTraceback()}") # requeue=True表示消息放回队列头部,等待下次消费 used_channel.basic_reject( delivery_tag=cur_delivery_tag, requeue=True ) try: crawler = CrawlerRunner(settings) # 启动爬虫,拿到异步任务句柄 crawl_deferred = crawler.crawl(MySpider, message=decodebody) # 绑定成功、失败回调 crawl_deferred.addCallback(on_crawl_success) crawl_deferred.addErrback(on_crawl_error) except Exception as e: # 捕获爬虫初始化、参数校验等启动阶段的同步异常 logger.error(f"爬虫启动失败,消息重新入队,错误: {str(e)}") used_channel.basic_reject( delivery_tag=cur_delivery_tag, requeue=True )
注意事项
- 你当前配置了
channel.basic_qos(prefetch_count=1),RabbitMQ一次只会给消费者推送1条未确认的消息,刚好适配串行执行爬虫任务的场景:当前消息没被ack或reject前,不会有新消息推过来,不会出现多任务并行导致的消息确认错乱。 - 不要使用裸
except:捕获异常,会把系统退出信号、键盘中断等正常操作也当成爬虫异常处理,影响服务正常退出。 - 如果担心爬虫持续报错导致消息反复重入队形成死循环,可以在消息自定义头字段中加入重试计数,每次重入队前计数+1,超过阈值就把消息投递到死信队列,避免无效循环。
- 爬虫任务执行完成后可以主动调用crawler的收尾方法清理资源,避免长期运行出现内存泄漏。
内容的提问来源于stack exchange,提问作者shihs
相关产品推荐
相关产品推荐

