首页 / 资讯中心 / 文章详情

Python文件读写三种方式:read、readline与迭代器的内存模型与选型

Python文件读写三种方式:read、readline与迭代器的内存模型与选型 ★ FEATURED ARTICLE
简介武汉大学计算机学院本科实验设计报告《三种文件读写方式的比较》围绕操作系统中的文件I/O核心议题面向学习Windows原理、系统编程或文件系统机制的学生和开发者旨在通过无缓冲、有缓冲与异步三种方式的实测对比为不同场景下的文件读写方式选择提供依据。报告完整涵盖实验目的、意义、设计原理、实验方案、实验展望及源代码附录重点介绍了基于GetTickCount函数的毫秒级计时、对同一source.txt文件多次调用读写函数的性能测试方法并给出异步方式速度最快、有缓冲方式居中、无缓冲方式最慢的清晰结论实验展望进一步探讨了扩大数据量、增加调用次数后异步方式的性能优势。资源为单一docx文档共1个文件压缩包约208KB结构完整、内容详实既适合作为操作系统课程实验报告范例也可帮助开发者理解同步/异步I/O机制的差异。该资料已有581人学习下载是学习文件读写性能分析不可多得的参考材料。1. 三种文件读写方式从“能跑”到“敢跑大文件”的分水岭拿到《三种文件读写方式比较》这个题目先别急着翻文档。做过几年文件处理的人都知道read()、readline()、for line in f 这三条路表面只是“读法不同”实际决定了一个脚本在 10MB 文件上跑得欢还是会在 2GB 日志文件面前直接卡死。这里的关键不是语法而是内存模型一次读入、逐行显式读、还是交给迭代器惰性取值。本文将三种方式的内存代价、选型标准、关键参数和常见的翻车现场铺开讲给出一套可以直接照抄的读写方案。适合刚上手 Python 文件处理的人确认细节也适合写过一段时间但被大文件坑过、想弄清楚背后原理的开发者。看完至少能回答一个问题手头这个文件到底该用哪种读法。2. 三种读写方式的内存模型与性能底牌read、readline 与惰性迭代三种方式的本质差异是内存获取策略的差异。read 系一口气把内容全搬进内存readline 每次只搬一行for line in f 则是站在迭代器的肩膀上用缓冲区的机制边取边扔。理解这个差异比背 API 重要得多。2.1 一次性读入read 与 readlines 的适用边界read() 和 readlines() 都属于“一口气把内容搬进内存”。区别只在于read() 返回一个包含整个文件内容的字符串readlines() 返回以行分隔的字符串列表。两者同样会把文件全部读进内存差别是 readlines() 还要为每一行建立独立的字符串对象多出一层列表和对象的开销。在实际中我一般会先用交互环境快速验证两者的返回类型差异with open(demo.txt, r, encodingutf-8) as f: content f.read() line_list f.readlines() # 注意这行拿到的通常是空列表注意上面这段代码在业务上是不对的第一次 read() 之后文件指针已经到了末尾readlines() 只能拿到空列表。写成这样只是为了在交互环境里确认返回类型真正的脚本里二选一不要连续调用。正确的打开方式是这样的with open(demo.txt, r, encodingutf-8) as f: data f.read() # data 是整个文件的字符串适合做全文正则、全文替换、整体解析 with open(demo.txt, r, encodingutf-8) as f: lines f.readlines() # lines 是每个元素为一行含换行符的列表适合按行索引访问open() 的第一个参数是路径r 表示只读encoding 指定解码方式。如果文件是 GBK 编码encoding 应设为 gbk否则会报 UnicodeDecodeError。readlines() 会把每行的 \n 或 \r\n 保留在字符串末尾需要去掉换行符时用 line.rstrip(\n)不要用 strip()因为 strip() 会连行首的空格一并去掉污染数据。什么时候选一次性读入当文件大小远小于可用内存、且你需要对全文做跨行处理时。例如整个配置文件只有几十 KB解析 JSON、XML或者要统计全文某个关键词出现次数read() 是最短路径。readlines() 的好处是保留行的顺序和索引适合小文件内要随机按行号取内容的场景。两者都不适合超过几百 MB 的文件——此时列表对象造成的开销会比文件本身的字节数大好几倍。同样是 1GB 的文件read() 吃 1GB 多内存readlines() 可能吃 4GB 甚至更多内存压力远超你预期。2.2 逐行读入readline 的循环代价readline() 一次只读一行返回的字符串包含换行符读到文件末尾时返回空字符串。它需要配合循环使用with open(demo.txt, r, encodingutf-8) as f: while True: line f.readline() if not line: break process(line) # 对单行做处理替换为你的业务逻辑这里的 process(line) 是占位函数你需要替换为实际业务逻辑。readline() 每次都从当前文件指针位置读到下一个换行符为止返回后指针落在下一行开头。空行返回 \n不是空字符串只有读到文件末尾时才会返回 所以 if not line 判断的是“是否读到了 EOF”而不是“是否是空行”。这一点初看反直觉但它是 readline 循环里最基础的判断。参数方面readline(size) 可以传一个可选的 size 参数表示最多读取 size 个字节。若行长度超过 size这一行会被截断读取剩余部分留在缓冲区直到下一次 readline 再取。这个特性在解析超长行时有用例如不想一次读一整行进内存可以按 1KB 一段切着读但多数普通文件处理用不上。readline 的代价在于 Python 层的循环每行都执行一次函数调用处理百万行的文件时解释器调用开销明显高于后面的迭代器方案。它的价值在于“手动控制读取节奏”的场景比如两个文件按行交替读取合并成新记录with open(a.txt, r, encodingutf-8) as fa, open(b.txt, r, encodingutf-8) as fb: line_a, line_b fa.readline(), fb.readline() while line_a and line_b: merged f{line_a.rstrip()}|{line_b.rstrip()} # 处理合并结果比如写成一行 CSV line_a, line_b fa.readline(), fb.readline()这段代码用 readline 因为需要同时维护两个文件的读取进度for 循环在这种“多文件联读”场景反而别扭。两个文件的行数不一致时while 条件会在较短的读完后退出不会报错这也是 readline 的容错优势。另外readline 无法做读一半回退想回到上一步只能自己保存历史这限制了它的灵活性。2.3 迭代器与惰性求值为什么 for line in f 是默认首选把文件对象本身当作可迭代对象来用是最推荐的默认方案with open(demo.txt, r, encodingutf-8) as f: for line in f: process(line) # 逐行处理行内含换行符这段代码的核心机制是文件对象实现了迭代协议每次 for 循环取下一行时底层用的是带缓冲的读取并不把整个文件载入内存。也就是说它和 readline 的效果类似但循环发生在 C 层迭代器的 next 调用比显式 readline() 快得多。对绝大多数“逐行处理”的需求这就是默认答案。迭代器方案的另一层优势是它“用完即弃”每行的字符串在处理完之后只要没有引用就会被垃圾回收内存曲线非常平缓。做 500MB 大日志的逐行过滤进程的常驻内存可以稳定压在 30MB 以内。这一点是 readlines 完全做不到的。如果硬要给 for line in f 挑毛病那就是它和 readline 一样不支持按字节限流也无法在循环中方便地获知“当前是第几行”——真要行号用 enumerate(f) 即可多余的开销可以忽略。到这里三种方式的边界已经清楚了整读适合小文件全文处理readline 适合节奏需要自己控制的场景for 迭代器是逐行处理时性能和内存的平衡点。接下来要解决的是“怎么选”的问题——文件多大算大哪些信号说明该换方式了3. 按文件规模选型判别标准与最小可复现代码选型不能凭感觉。我习惯先做一个快速的规模判断再决定读法。判断的三个信号一是文件体积二是对内容的使用方式全文操作还是逐行操作三是内存预算。其中文件体积是最直观的维度。3.1 先看文件大小再选读法三行代码定方向用 os.path.getsize 获取文件字节数再按经验阈值切分策略是最省事的做法import os size os.path.getsize(data.txt) if size 50 * 1024 * 1024: strategy whole # 小于 50MB整读没压力 elif size 1024 * 1024 * 1024: strategy line # 50MB 到 1GB走逐行迭代 else: strategy chunk # 超过 1GB分块读这段代码把选型逻辑集中在一个地方。50MB 和 1GB 是经验值不是硬性规定如果你的机器内存只有 256MB阈值就要往下调如果服务器内存 256GB直接整读 2GB 文件也不是不行只是不值得。strategy 的值可以打印出来也可以作为后续分支的入口。注意这里算的是字节数1GB 要写成 1024 * 1024 * 1024不是 1000 * 1000 * 1000避免和磁盘厂商的换算方式混淆。3.2 小文件与中等文件readlines 到 for 迭代的切换点小于 50MB 的文件怎么写都不会有太大问题优先考虑代码可读性。我一般这样处理import json with open(config.json, r, encodingutf-8) as f: config json.load(f) # 内部会调用 read()不需要手动读 with open(data.csv, r, encodingutf-8) as f: rows f.readlines() # 需要随机访问第 N 行时用这个json.load() 内部本身就是一次 read()所以不需要再手动调 read。readlines() 最典型的误用是把大文件整读进来但在小文件场景里它是合理的因为它能让你直接访问第 n 行rows[99]。注意这里的 rows 每行都带换行符对比数据前记得清理。中等文件50MB 到 1GB这个区间整读已经不保险了尤其当你的进程还有其他大对象占用内存时。逐行处理是更稳的选择对于结构化的文本最常见的做法是 for 迭代器配合按行解析import csv with open(big_data.csv, r, encodingutf-8-sig, newline) as f: reader csv.reader(f) for row in reader: # row 是一个列表对应一行中按逗号切好的字段 total sum(float(row[i]) for i in range(1, len(row)))csv.reader 接收一个文件对象内部就是按行迭代所以你不会在内存里看到整个文件。newline 是 Python 官方推荐的写法避免 csv 模块读含换行符的字段时出问题。encodingutf-8-sig 会去掉 Excel 导出文件开头可能存在的 BOM 头避免第一行第一个字段出现 \ufeff 前缀。如果你处理的是 GBK 编码的 CSV把 encoding 换成 gbk 即可。3.3 大文件与超长行分块读取的生成器写法1GB 以上的文件普通逐行迭代仍然有效但要注意两点每行内容本身可能很长以及 GC 对逐行字符串对象的回收压力。更稳妥的做法是分块读取控制每次进入内存的数据量def read_in_chunks(file_path, chunk_size1024 * 1024): with open(file_path, r, encodingutf-8) as f: while True: chunk f.read(chunk_size) if not chunk: break yield chunk这是一个生成器每次产出 1MB 的文本块。chunk_size 的单位是字节1024 * 1024 就是 1MB。你可以先按块把文本切好再在块内按行拆分也可以把 chunk_size 调小到 64KB 来降低单次内存峰值。这个函数的优点是它不关心行边界适合做字符级的大文本分析缺点是如果你需要按行处理边界行会被切碎需要自己拼接。实际项目里我通常先用 for line in f 跑一遍发现性能不够再换成这个分块版本。注意分块读取不保证按行分割。如果业务严格要求按行处理优先用 for line in f不要偷懒直接套分块。如果文件是 gzip 压缩的把 open 换成 gzip.open其他逻辑完全一致import gzip with gzip.open(big.log.gz, rt, encodingutf-8) as f: for line in f: process(line) # 解压是流式的不会一次性膨胀到内存gzip.open 的第二个参数 rt 表示文本模式读取缺省是二进制读出来的是 bytes直接做字符串处理会报错。gzip 文件最忌讳的是先 gunzip 成原始文件再处理白白占一份磁盘空间流式解压才是正解。选型的标准我整理成下面这张表文件规模读法内存模型典型场景50MBread() / readlines()全文入内存配置、小报表、全文正则50MB~1GBfor line in f缓冲 单行入内存日志清洗、CSV 处理1GB分块 read(chunk_size)可控块内存大日志统计、流式解析4. 读写参数与性能调优用什么参数、调哪里、效果如何选定了读法之后真正拉开性能差距的是 open() 和读写循环里的参数。不少人把精力花在纠结 read 和 readline 哪个快却忽略了一个事实open() 的缓冲区大小、文本模式的换行符处理、以及写入时的 flush 策略每一项对实际运行时间的影响都远超读法选择本身。4.1 buffer size 与块大小默认值就该相信吗open() 默认使用系统级缓冲通常 8KB。对小文件来说这个值已经够用对大数据量的循环写入默认缓冲反而是瓶颈。with open(large.log, w, encodingutf-8, buffering64 * 1024) as f: for record in records: f.write(record)buffering 参数的单位是字节。设置为 64KB 时写入数据会先在内存攒到 64KB 再真正落一次盘磁盘 I/O 次数减少写日志类任务能明显提速。但 buffering 不是越大越好它只影响用户态的缓冲大小操作系统和磁盘层还有自己的缓存逻辑缓冲调太大程序异常退出时丢失的数据量也更大。我一般把日志写入的 buffering 设在 64KB 到 256KB 之间再大收益很小。分块读取时的 chunk_size 同理chunk_size 太小比如 4KBPython 层调用次数暴增CPU 反而跑满太大比如 32MB内存占用失去分块意义。经验值是 1MB 到 8MB兼顾调用次数和内存峰值。4.2 编码与换行符文本模式的隐藏参数open() 在文本模式下默认的换行符处理是“万能”的读取时把 \r\n、\r 统一转成 \n写入时把 \n 转成当前平台的换行符。这个特性在 Windows 上经常制造事故你写的是一个带 \n 的字符串落盘却变成 \r\n文件在 Linux 上再读回来行尾就带了 \r。with open(out.txt, w, encodingutf-8, newline\n) as f: f.write(line1\nline2\n)newline\n 表示写入时不转换直接保留 \n这样写出的文件在跨平台交换时行为一致。读取时也可以设 newline表示不做任何换行转换把原始换行符原样给你适用于需要自己精确解析行边界的场景。如果你的数据源来自老旧的 Windows 系统建议打开文件时先确认行尾是 \r\n 还是 \n再决定要不要交给默认转换处理。编码这块很多人栽在 encoding 缺省值上。Python 3 在多数 Linux 环境默认 UTF-8但在 Windows 中文版默认是 GBK。同一个脚本在两边跑读同一个 UTF-8 文件结果可能完全不同。with open(data.txt, r, encodingutf-8, errorsreplace) as f: for line in f: passerrorsreplace 的作用是遇到无效字节时用替换字符代替而不是抛异常。这在处理脏数据时很有用但对数据质量要求高、需要准确定位失败位置的任务应该用默认的 strict 模式让它在出错时直接抛出 UnicodeDecodeError你能及时发现问题。两个参数没有绝对好坏区别是“先跑起来”还是“先暴露错误”。4.3 写入缓存与 flush数据丢在崩溃前的最后一刻写入模式也有缓冲。默认 buffering-1 使用系统默认缓冲数据先存在内存缓冲区满或文件关闭时才落盘。这在程序正常运行期间没问题但一旦进程被 kill 或者断电缓冲区里的数据就没了。with open(result.txt, w, encodingutf-8, buffering1) as f: for chunk in result_stream: f.write(chunk) f.flush()buffering1 是行缓冲每写一个换行符就 flush 一次。显式 flush() 表示立即把缓冲内容交给操作系统。这个做法的代价是性能明显下降每行一次系统调用写百万行文件时差距是数量级的。所以我的习惯是不重要的中间产物用大缓冲写完自然落盘需要保证“即使崩溃也不丢太多”的关键结果分批 flush比如每 1000 行 flush 一次而不是每行都 flush。with open(critical.txt, w, encodingutf-8, buffering64 * 1024) as f: for i, row in enumerate(data): f.write(str(row) \n) if i % 1000 0: f.flush()enumerate 提供行号取模判断 1000 行时 flush 一次内存峰值和崩溃丢失的窗口都处于可控状态。如果处理的是极端重要的结果flush 之后还可以加 os.fsync(f.fileno())强制操作系统把脏页写进磁盘代价是每次 fsync 都可能让写性能降一个量级所以只在结果写入的关键节点用不要在循环里滥发。5. 文件读写的五个常见翻车现场从内存爆炸到换行符玄学无论读法选得多么合理实际运行中总有几个固定坑位等着你。下面五条基本覆盖了我在各种项目里见过的文件读写事故每条按现象、原因、解决展开。5.1 现象readlines 一次性读入大日志内存直接爆掉现象一个 1.2GB 的日志文件用 readlines() 读入后进程内存冲到 5GB然后被系统 OOM 杀掉。原因readlines() 返回每一行的独立字符串列表每个字符串对象有固定对象头开销再加上列表指针、行尾字符的临时拷贝内存占用能达到文件体积的 4 到 5 倍。1.2GB 的文件表现出 5GB 内存占用是正常的不是泄漏是清单式读取的原罪。解决换成 for line in f用迭代器逐行处理。如果确实需要随机访问用 mmap 或先把文件按块切分索引不要贪图 readlines 的语法简单。5.2 现象for 迭代与 readline 混用导致行丢失现象脚本先用了 for line in f 处理前三行然后改用 f.readline() 继续读结果第一个 readline 返回的不是第四行而是第五行甚至更后面的内容。原因文件对象的迭代器内部有自己的缓冲。for 循环从缓冲区里取了数据之后文件指针的真实位置和 Python 层维护的位置可能已经不一致再调 readline 会跳过部分缓冲内容。这种混用行为属于文档中没有明确承诺的实现细节不同 Python 版本表现还不一样踩过一次就明白什么叫玄学。解决在一个文件对象上全程只用一种读法。要“先跳过 N 行再逐行处理”用 itertools.islice(f, N, None) 拿到切片迭代器不要跳完再 readline。5.3 现象Windows 下 \r\n 与 \n 的换行符玄学现象在 Windows 上写的脚本把文件逐行处理后重新写出结果行尾都变成 \r\n把同样的脚本放到 Linux 上读同一个文件每行末尾多出一个 \r字符串匹配全部失败。原因文本模式默认开启换行符转换。读取时把 \r\n 转成 \n写入时把 \n 转回 \r\n。在跨平台场景下这个特性制造了不对称的行为。解决需要精确控制行尾时统一加 newline 读取、newline\n 写入。如果只是想去掉所有 \r在行处理时对每一行做 rstrip(\r\n)再按统一格式写。5.4 现象UnicodeDecodeError 反复出现第一行就崩现象一个 CSV 文件在本地打开正常脚本一读就报 UnicodeDecodeError排查发现数据源是从老系统导出的 GBK 编码。原因Python 3 的 open() 在没指定 encoding 时使用区域设置的默认编码。不同机器、不同操作系统的默认编码不一样同一个脚本换个环境跑法就变。这不是“中文文件就该用 utf-8”能解释的问题。解决所有打开文本文件的地方显式指定 encoding。不确定编码时先用二进制模式读前几百字节做探测with open(unknown.csv, rb) as f: raw f.read(512) for encoding in (utf-8, gbk, latin1): try: raw.decode(encoding) print(f匹配编码: {encoding}) break except UnicodeDecodeError: continue二进制模式读出来的 bytes 逐个候选编码尝试 decode哪个不抛异常就用哪个。这段代码只适合做初步判断不建议直接用于生产环境的自动识别因为有些编码对特定字节序列都能“恰好解码成功”选错会出乱码。5.5 现象写入后数据不落盘程序崩溃丢数据现象程序把一大段处理结果写进文件日志打印“写入完成”随后进程被杀打开文件发现内容缺失或只有旧数据。原因write() 只把数据写进了用户态的缓冲区落盘发生在缓冲满、close、flush 或显式 fsync 时。很多框架在退出时会调用 flush但强制 kill 不会走这个流程。解决性能容忍的情况下关键节点调用 flush()必要再加 os.fsync(f.fileno())。两者区别flush 是把数据从 Python 缓冲区交给操作系统fsync 是强制操作系统把脏页写进磁盘。对极端重要的文件两步都要做但只在关键节点用不要在循环里滥用。6. 进阶用生成器把三种方式组合成一套通用读取管线前面几章的方案各自独立实战中我习惯把它们组装成一个可复用的读取管线对外暴露统一的 with 语义对内根据文件大小自动选择读法。这样业务代码不用关心文件多大、用哪种读法也方便在读取环节统一挂编码处理、行尾清理、异常上报。from contextlib import contextmanager contextmanager def smart_open(file_path, moder, encodingutf-8, newlineNone): f open(file_path, mode, encodingencoding, newlinenewline) try: yield f finally: f.close()这个包装的核心不是 open 本身而是把“保证关闭”的逻辑固定下来。使用方写 with smart_open(...) as f: 就和原生 open 一样但可以在中间插入统一的后处理逻辑。再挂一个按需选读法的迭代函数def iter_lines(file_path, encodingutf-8, fallback_encodings(gbk, latin1)): try: with open(file_path, r, encodingencoding) as f: for line in f: yield line.rstrip(\r\n) except UnicodeDecodeError: with open(file_path, r, encodingfallback_encodings[0]) as f: for line in f: yield line.rstrip(\r\n)这段代码把编码降级逻辑收进生成器内部。首次解码失败时自动用 fallback 重新读一遍业务方只拿干净的行。注意 rstrip(\r\n) 去掉了行尾换行但保留了行首空格比 strip() 更保守。如果业务对空行敏感可以在这里加一个 if line: yield line把空行过滤掉。这个管线的好处是文件读法、编码策略、行尾策略被固定在一个地方业务代码只对“行”做处理。遇到超大文件要切分块时再把 iter_lines 换成第 3 章的分块生成器接口不变业务代码一行都不用改。最后说一个被坑出来的习惯任何涉及文件读写的脚本上线前我都会用一个“假大文件”跑一遍——用生成器写一个 200MB 的临时测试文件观察两个指标内存峰值是否平缓、处理完的文件是否能在另一个操作系统上正常读回。这两步只花五分钟却救过我太多次。跨平台文件读写这件事很多坑不是语法层面的而是运行环境默认值造成的显式声明永远比依赖默认值可靠。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站