不能直接用len(open(file).readlines()),因其将整个文件加载到内存,导致oom风险且内存占用达2–3倍;应改用二进制分块读取统计b'\n',或mmap零拷贝优化。

为什么不能直接用 len(open(file).readlines())
这行代码看似简洁,但会把整个文件一次性加载进内存——遇到几百MB甚至GB级日志时,Python进程可能直接被系统OOM kill。更糟的是,readlines() 还要为每行额外分配字符串对象和换行符副本,实际内存占用常是文件体积的2–3倍。
用 buffering 参数控制底层IO块大小
buffering 并不改变“是否打开文件”的事实(所有文件操作都需先调用 open()),但它能避免全量读入。关键是把文件当二进制流处理,只统计换行符 b'\n',跳过解码、字符串分割等开销:
def count_lines_fast(filepath):
count = 0
with open(filepath, 'rb', buffering=8192) as f:
while True:
chunk = f.read(8192)
if not chunk:
break
count += chunk.count(b'\n')
return count
-
buffering=8192让系统以8KB为单位预读,减少系统调用次数;值太小(如1)会频繁陷入内核,太大(如1MB)可能浪费内存 - 必须用
'rb'模式:避免文本模式下的换行符归一化(如Windows的\r\n被转成\n),确保count(b'\n')结果准确 - 最后一行若无换行符,此方法会漏计——需额外判断
f.tell() == os.path.getsize(filepath)且末字节非b'\n'
比 buffering 更快的方案:用 mmap 零拷贝统计
当文件极大(>1GB)且机器有足够虚拟内存时,mmap 能绕过Python缓冲区,直接在内存映射页中扫描 b'\n':
import mmap
def count_lines_mmap(filepath):
with open(filepath, 'rb') as f:
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
return mm.read().count(b'\n')
- 注意:
mm.read()仍会加载全部内容到用户空间——正确写法是用mm.find(b'\n', start)循环查找,但实现略复杂 - 实际测试中,对10GB纯文本,
mmap + find比分块读取快15–20%,但内存驻留页更多,可能影响其他进程 - Windows下大文件
mmap可能触发PermissionError,需加try/except回退到分块读取
真实场景要注意的边界情况
线上日志或CSV导出文件常含非标准换行,比如空行、Windows换行符、甚至二进制乱码:
- 若文件含
\r\n,上面所有方法都只计1次——这通常是期望行为(逻辑行数),无需额外处理 - 若文件末尾缺失换行符,且业务要求“最后一行必须计入”,得在循环结束后检查
chunk是否非空且末字节不是b'\n' - UTF-16/UTF-32编码文件不能用
b'\n'直接扫——此时必须按行解码,buffering优势消失,老实用for line in f:
真正省资源的关键不在“是否打开文件”,而在避免构造每行字符串对象。只要不调用 readlines() 或 splitlines(),哪怕 open() 了,内存也只涨几KB。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











