python爬虫内存泄漏主因是response等对象被全局缓存或闭包持有导致引用不释放,tracemalloc通过对比快照定位list.append等高增长行,gc.set_debug可暴露循环引用+__del__造成的uncollectable对象。

Python爬虫在大规模抓取时内存泄漏不是“会不会发生”的问题,而是“什么时候暴露”的问题——只要存在全局缓存、未释放的响应对象、循环引用或带 __del__ 的类,泄漏就已在累积。
为什么 response 对象不释放会导致 RSS 持续上涨
Scrapy 或 requests 构建的 response 对象(尤其是含 body 的)本身可能占用几 MB 内存。若你在 parse 方法中把整个 response 存进模块级 list、闭包变量或 pipeline 的实例属性里,它就脱离了函数作用域,引用计数不会归零。
- 常见错误:用
self.items.append(response)临时缓存所有响应,等批量入库——这等于把全部 HTML 原文钉在内存里 - Scrapy 默认会复用
Response对象的部分结构,但 body 字节流仍需完整持有,除非显式调用response.release()(仅限 aiohttp backend) - requests 库的
Response更危险:默认不关闭连接,response.content和response.text都是强引用,且response.history可能链式持有多个响应
如何用 tracemalloc 定位泄漏源头行号
tracemalloc 是 Python 3.4+ 内置工具,无需安装,适合生产环境轻量介入。关键是启动时机和快照对比——不能只拍一次,要抓“增长段”。
- 在爬虫主循环开始前调用
tracemalloc.start() - 每处理 N 个请求(如 100 个)后执行
snapshot = tracemalloc.take_snapshot(),保存到列表 - 用
snapshot.compare_to(prev_snapshot, "lineno")找出新增分配最多的代码行 - 重点关注
list.append、dict.__setitem__、re.compile(正则编译缓存)、lxml.etree.fromstring(解析树未释放)
为什么 gc.collect() 有时无效,而 gc.set_debug(gc.DEBUG_UNCOLLECTABLE) 能暴露真问题
当对象因循环引用 + __del__ 被 GC 归入 “uncollectable” 列表时,gc.collect() 返回值可能显示回收了 0 个对象,但实际这些对象永远卡住。开启 debug 模式后,GC 会在控制台打印所有无法回收的对象类型及数量。
- 典型场景:自定义中间件类持有
response和request,且类中定义了__del__方法 - 修复方式不是删
__del__,而是改用weakref.finalize(obj, callback),避免参与循环引用判定 - 检查
gc.garbage列表(Python gc.get_referents(obj) 追踪谁在持有着它
Scrapy 用户最容易忽略的三个内存陷阱
Scrapy 表面封装了调度和回收逻辑,但很多配置项和写法会绕过其内存管理机制。
-
FEED_EXPORTERS或自定义 pipeline 中,用items.append(item)累积所有数据再统一导出——应改用流式写入(如逐行写 CSV、分批 insert DB) -
CONCURRENT_REQUESTS设得过高(如 > 32),导致大量Request和Response对象并行驻留;配合DOWNLOAD_DELAY不足,更易堆积 - 启用
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'时,默认使用scrapy.utils.request.request_fingerprint,若 URL 含大量动态参数(如 timestamp、session_id),指纹字典会无限膨胀
真正难处理的从来不是单个泄漏点,而是多个小泄漏叠加后的“雪崩阈值”——比如 pipeline 缓存 + response.body 持有 + traceback 未清理,三者各自涨 10MB,合起来就压垮 2GB 限制。排查时必须横向比对多个快照,盯住增长最陡的那几行代码,而不是只看总量。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











