用sum(1 for _ in f)统计行数最简单但不适用于gb级文件;需注意文件末尾无换行符会导致少计一行,可通过xxd验证,修复方式包括补空行或在分块扫描后判断buf是否以b'\n'结尾并手动加1。

用 sum(1 for _ in f) 最简单但别在 GB 级文件上试
小文件(for line in f 本质是按行缓冲,内存友好。但大文件会慢——不是因为逻辑错,而是磁盘 I/O 和 Python 解析换行符的开销叠加。
常见错误现象:open('huge.log').readlines() 直接 OOM;len(f.readlines()) 同样扛不住。
- 真实场景:日志归档、ETL 前校验、CI 中快速探查数据规模
- 参数差异:
encoding必须匹配文件实际编码,否则遇到非法字节会抛UnicodeDecodeError - 性能影响:纯文本下,每行平均 100 字节时,1GB 文件约 1000 万行,
sum(1 for _ in f)在机械盘上可能耗 8–15 秒
grep -c '^' 是 Linux/macOS 下最快的实际解法
绕过 Python 解析层,让系统工具干它最擅长的事——流式扫描换行符。实测比纯 Python 快 3–8 倍,且内存恒定 ≈ 4KB。
- 使用场景:服务器批量处理、Docker 容器内轻量统计、CI 脚本中嵌入
- 注意点:
grep -c '^'统计的是“以行首开始的行”,空行也算;若文件末尾无换行符,最后一行仍被计入(POSIX 兼容行为) - 兼容性:Windows 默认无
grep,需装 WSL、Git Bash 或用findstr /n "^" file | find /c ":"(但后者慢且对 Unicode 不稳) - 示例:
import subprocess; n = int(subprocess.run(['grep', '-c', '^', 'data.csv'], capture_output=True, text=True).stdout.strip())
二进制分块扫描 + 手动计 \n 字节,真正可控的大文件方案
当必须跨平台、不能依赖 shell、又得扛住 10GB+ 文件时,就该自己扫字节流。核心思路:按块读(如 8MB),统计每个块里 \n 的个数,最后加 1(因首行前无换行符)。
- 容易踩的坑:
rb模式读取,绝不用rt——避免解码失败打断流程;块边界处的\n不会漏,因我们只数字节,不解析行 - 为什么不是简单
f.read().count(b'\n')?——会把整个文件加载进内存,和初衷背道而驰 - 关键细节:文件开头是否为空?末尾是否有
\n?答案是——不影响总数。只要文件非空,行数 =\n个数 + 1;空文件返回 0 - 示例片段:
def count_lines(path):<br> n = 0<br> with open(path, 'rb') as f:<br> while True:<br> buf = f.read(8*1024*1024)<br> if not buf:<br> break<br> n += buf.count(b'\n')<br> return n + (1 if n == 0 and os.path.getsize(path) > 0 else 0)
别忽略文件末尾无换行符的边界情况
POSIX 标准里,文本文件最后一行应以 \n 结束,但现实里大量生成器(尤其 Windows 工具、某些 CSV 导出)会漏掉。此时所有基于 \n 计数的方案都会少算一行。
立即学习“Python免费学习笔记(深入)”;
- 验证方法:
xxd -ps -c 1 yourfile | tail -5看最后几个字节是不是0a(即\n) - 修复建议:如果业务允许,用
echo >> file补一个空行;若不能改源文件,就在分块扫描后追加判断:if os.path.getsize(path) > 0 and not buf.endswith(b'\n'),再加 1 - 性能代价:这个判断只发生在最后一次 read,几乎无开销;但很多人写的时候忘了 seek 到末尾确认,直接假设“有或没有”,结果在关键数据上差一行










