内存泄漏必须主动切断引用、分批释放、限制对象生命周期;response、driver、全局缓存三类对象最易导致泄漏,需提取字段后弃用response、必调driver.quit()、用WeakValueDictionary替代强引用缓存。

直接说结论:内存泄漏不是“等GC来救”,而是必须在代码里主动切断引用、分批释放、限制驻留对象生命周期——尤其当爬虫跑过几百个请求后,response、driver、全局缓存这三类对象最容易钉死内存。
response对象别存进全局list或类属性
Scrapy 和 requests 的 response 默认强持有 body(几MB起步),一旦被塞进模块级 self.items、CACHE = {} 或闭包变量,引用计数就永远不归零。
常见错误写法:self.results.append(response)、CACHE[url] = response、lambda: response 闭包捕获。
- 正确做法:立刻提取需要的字段,比如
response.css('title::text').get()或response.json(),然后让response自然离开作用域 - requests 用户额外注意:
response.content和response.text都是强引用,用完别留着;如果必须缓存,只存response.status_code、response.headers等轻量字段 - Scrapy 用户别依赖
FEED_EXPORTERS自动清理——它只管输出,不管中间对象是否还挂着
driver.quit() 不是可选项,是必做动作
Selenium 场景下,driver.close() 只关标签页,Chrome 渲染进程还在后台吃内存;driver.quit() 才真正杀掉整个浏览器实例、断开 DevTools 连接、释放 V8 实例。
立即学习“Python免费学习笔记(深入)”;
典型崩溃现象:任务管理器里 “Renderer” 进程越积越多,内存占用线性上涨,最终 Chrome 崩溃且 Python 卡死(不抛异常)。
- 循环中每处理 20–50 个 URL 就调一次
driver.quit(),再新建实例——比“一直开着”稳得多 - 多线程环境下,每个线程必须独立调用
driver.quit(),残留进程会叠加 - 启动参数必加:
--no-sandbox、--disable-dev-shm-usage、--disable-gpu;禁用--user-data-dir(除非真要复用登录态)
用 weakref 或流式处理替代全局缓存
把爬取结果全塞进 cache = {} 是最典型的内存泄漏源头,尤其当 key 是 URL、value 是 HTML 字符串时,几百个页面就能吃掉上 GB 内存。
更危险的是自定义类同时持有 request 和 response,又写了 __del__ 方法——这会让 GC 把对象扔进 gc.garbage 列表,永远无法回收。
- 缓存改用
weakref.WeakValueDictionary(),对象在别处无引用时自动踢出 - 大响应内容用流式处理:
requests.get(url, stream=True)+for chunk in r.iter_content(8192): - 调试时加
gc.set_debug(gc.DEBUG_UNCOLLECTABLE),控制台会打印卡死对象类型和数量 - 查谁在强引用某个可疑对象?用
gc.get_referents(obj)一层层追
tracemalloc 必须“对比拍”,单次快照毫无意义
tracemalloc 是 Python 3.4+ 内置工具,轻量但极易误用——只调一次 take_snapshot() 看不出任何问题。
真正有效的方式是:在主循环开始前 tracemalloc.start(),每处理 50–100 个请求后拍一次快照,再用 compare_to(prev_snapshot, "lineno") 找新增分配最多的代码行。
- 重点关注:
list.append、dict.__setitem__、lxml.etree.fromstring、re.compile这些调用点 - 别只盯着第三方库路径,顺藤摸瓜找到你自己的哪一行触发了它们
- 配合
snapshot.statistics("filename")快速定位高分配文件
最常被忽略的一点:Scrapy 的 CLOSESPIDER_PAGECOUNT 和 CLOSESPIDER_ITEMCOUNT 只是终止开关,不是内存保护机制——泄漏照样发生在终止前。真正可控的,只有你对每个 response、每个 driver、每个字典项的生命周期判断。


















