我见过太多人处理大文件时内存直接爆掉。那种看着进度条卡死、电脑风扇狂转的感觉,经历过的人都懂。
有个同事处理10G的日志文件,read()方法直接让内存飙升到95%。他当时脸色都变了。后来他改用生成器,内存占用一直稳定在50MB左右。他当时就喊了一句,这玩意儿太救命了。
普通读取文件的方式会把整个文件塞进内存。像这样:
with open('big.log', 'r') as f: data = f.read() 全部加载到内存
for line in data:
process(line)
文件2G,内存就得2G。文件20G,内存就炸。这方式只适合小文件。大文件这样搞,机器直接罢工。
迭代器是解决这个问题的关键。Python里的文件对象本身就是迭代器。你可以一行一行读取,每次只处理一行数据。内存里永远只存一行内容。代码改一下就行:
with open('big.log', 'r') as f: for line in f: f本身就是迭代器,一行一行读
process(line)
这句for line in f看起来不起眼。但它背后做的事情就是迭代器模式。每次循环只从硬盘读取一行,处理完就扔掉。内存占用基本不变。文件多大都不怕。
自定义迭代器能处理更复杂的需求。比如你有一堆CSV文件要合并处理。每个文件几百万行。这时候写个迭代器类,每次返回一行数据。用yield关键字就行。我写过这样的代码:
def read_multiple_csv(files): for file in files:
with open(file, 'r') as f:
next(f) 跳过表头
for line in f:
yield line.strip().split(',')
这个生成器函数不会把数据存到列表里。它一次只返回一行。用的时候这样:
for row in read_multiple_csv(file_list): process(row)
内存压力几乎为零。不管你有10个100MB的文件还是100个10MB的文件,内存占用都一样少。
生成器表达式比列表推导式更省内存。列表推导式会生成一个完整列表。一万个元素就是一万个对象全在内存里。生成器表达式是惰性求值,用到一个才生成一个。你写代码时要主动选择生成器,别图方便用列表。两个例子区别很大:
内存杀手 data = [process(line) for line in f] 全部算完才返回列表
内存友好
data = (process(line) for line in f) 用到哪个算哪个,返回生成器
实战中还有一个坑。有人用生成器处理数据,但最后非要接个list()。这就白费功夫了。生成器全部展开成列表,内存又炸了。比如:
result = list(process(line) for line in f) 又全部加载到内存
你要真需要所有结果,建议分批处理。或者用数据库存中间结果。别指望一次性全放内存里。大文件处理就要接受一个事实:有些东西没法一次性搞定。
迭代器能无限延展。你可以写个生成器,一直从数据库流式读取数据。读到空记录为止。后台数据量多大都没关系。代码写起来跟处理普通列表一样方便。这就是生成器的威力。
文件大了不要怕。用到迭代器,一行一行读。用生成器,一个元素一个元素产。代码写清楚,内存稳如狗。下次遇到几个G的大文件,你就知道怎么干了。