Python 生成器实战:用 yield 逐行处理大日志文件而不爆内存

举报
yd_70403235 发表于 2026/11/09 09:31:46 2026/11/09
【摘要】 一次性读入几百 MB 的日志文件很容易把内存撑爆。本文用一个可直接运行的小例子,对比 readlines 与生成器逐行读取的内存占用,演示 yield、生成器表达式与惰性过滤的组合写法。

背景

排查线上问题时,经常要从几百 MB 的日志文件里找出报错行。最直观的写法是 readlines() 一次性读入全部内容,再用列表推导过滤。文件不大时没有问题,文件一大,内存占用会跟着文件体积线性增长,轻则变慢,重则被系统杀掉进程。

生成器是 Python 处理这类流式数据的标准工具:它一次只产生一个值,用完即丢,内存占用基本恒定。

实现思路

核心是把“读一行、判断一行、产出一行”写成生成器函数,调用方按需消费,中间不产生大列表。

逐行读取并过滤

def error_lines(path):
    """逐行产出包含 ERROR 的日志行,内存占用与文件大小无关。"""
    with open(path, encoding="utf-8") as f:
        for line in f:
            if "ERROR" in line:
                yield line.rstrip("\n")

文件对象本身就是可迭代对象,for line in f 每次只读一行,配合 yield 后函数会在每次产出后暂停,调用方取走一行才继续。

用生成器表达式做惰性统计

不需要把结果存下来时,生成器表达式比列表推导更省内存:

error_count = sum(1 for line in error_lines("app.log"))

sum 一边消费一边累加,全程只保留一个计数器。

组合多个生成器做流水线

过滤、转换可以拆成多个小生成器串联,每个环节都是惰性的:

from itertools import islice

def timestamps(lines):
    for line in lines:
        yield " ".join(line.split(" ")[:2])

first_errors = list(islice(timestamps(error_lines("app.log")), 5))

只有最后的 list(...) 会把结果物化,而且这里只取了前 5 条。注意生成器不能像列表那样切片,取前 N 条要用 itertools.islice。

验证结果

本地用 Python 3.12 实测:先生成一个 20 万行、约 9.7 MB 的测试日志,其中混入 1000 行 ERROR。

import tracemalloc

tracemalloc.start()
count = sum(1 for _ in error_lines("test.log"))
current, peak = tracemalloc.get_traced_memory()
print(count, f"峰值内存 {peak / 1024:.1f} KB")
tracemalloc.stop()

实测输出:1000 峰值内存 22.2 KB。作为对照,readlines() 读入同一文件再过滤,计数同样是 1000,峰值内存约为 18.6 MB,与文件体积相当。

结论:生成器版本的峰值内存不随文件行数增长,只与单行长度有关;readlines() 版本的峰值内存与文件体积成正比。

注意事项

  • 生成器只能消费一次:sum(...) 用完后,同一个生成器对象不能再遍历,需要重新调用函数生成新的。
  • yield 之后的代码在下一次 next() 时才继续执行,打开的文件句柄要放在 with 块里,确保消费结束或中途退出时正确关闭。
  • 如果中途 break 退出循环,生成器会被垃圾回收关闭,未读完的文件句柄同样会被 with 块释放,不需要手动处理。
  • 需要随机访问或多次遍历时,生成器不合适,应改用列表或把结果写入临时文件。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。