用 Python 生成器处理大文件:不要一次把全部数据读进内存
用 Python 生成器处理大文件:不要一次把全部数据读进内存
处理日志文件时,最常见的写法是 readlines() 或 read() 一次读入全部内容,再逐行处理。文件只有几 MB 时这没问题;一旦文件到了几百 MB,内存占用会跟着暴涨,轻则变慢,重则直接被系统杀掉。生成器就是为这种场景准备的:一次只产出一条数据,用完即丢,内存占用始终保持在一个很小的水平。
先看问题出在哪里
# 危险写法:1GB 的日志会先完整进内存
with open("access.log", encoding="utf-8") as f:
lines = f.readlines()
for line in lines:
if "ERROR" in line:
print(line, end="")
readlines() 返回的是一个完整列表,文件多大,列表就多大。改成生成器后,函数每次只返回一行,调用方处理完一行再要下一行:
def error_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
yield line
for line in error_lines("access.log"):
print(line, end="")
这里的关键是 yield:函数执行到 yield 就暂停并交出当前值,下一次迭代才从暂停处继续。文件对象本身就是可迭代的,逐行读本来就不会一次性加载全文件,生成器只是把“过滤”这一步也包进了惰性流水线。
生成器可以串联成流水线
真正好用的是把多个处理步骤串起来,每一步都是生成器,数据像流水一样一站站往下走,中间不产生任何完整列表:
def read_lines(path):
with open(path, encoding="utf-8") as f:
yield from f
def parse(line):
parts = line.split()
return parts[0], parts[6], parts[8] # IP、路径、状态码
def only_errors(rows):
for ip, path, status in rows:
if status.startswith("5"):
yield ip, path, status
for ip, path, status in only_errors(map(parse, read_lines("access.log"))):
print(ip, path, status)
map 本身也是惰性的,所以整条链路在任何时刻内存里都只有当前这一行。需要加一步处理时,再写一个生成器函数插进去即可,不用改上下游。
三个容易踩的坑
第一,生成器只能消费一次。把它转成列表、或用 for 循环跑完一遍之后,第二次迭代什么也得不到。需要复用结果时,要么重新调用生成器函数,要么在确实需要时才转成列表。
第二,不要在生成器里藏“打开后不关闭”的资源。上面的例子把 with open(...) 写在生成器函数内部,生成器被垃圾回收或提前退出时文件不一定立刻关闭;稳妥的做法是在调用方用 with 包住整个消费过程,或者确保消费到底。
第三,生成器不是万能药。它解决的是“数据量大、处理可逐条进行”的问题;如果业务需要随机访问、反复排序或多次统计,惰性流反而会让代码变复杂,这时先读入内存或用数据库才是对的工具。
判断标准很简单:如果处理逻辑是“读一条、算一条、输出一条”,就用生成器;如果要“先看完全部数据再决定”,就别硬套。把大文件拆成小步处理,程序在数据规模翻十倍时依然稳得住,这才是生成器真正的价值。
- 点赞
- 收藏
- 关注作者
评论(0)