
写Python到现在我遇到过不少初学者问我“学完语法之后该练什么”我的答案里永远有一个选项——文件操作。不是说文件操作有多难而是它太实用了爬虫爬下来的数据要存成文件处理Excel表格要读写文件程序运行的日志要记录到文件就连配置文件也是文件。可以说只要你的程序不是纯黑盒早晚都要跟文件打交道。这篇文章就是一份Python文件操作的快速复习笔记覆盖路径、编码、打开模式、读写方式、上下文管理器再到目录操作和综合实战适合已经学过基本语法、想系统梳理文件操作知识点的读者也适合放在手边随时查阅。1. 文件操作是Python里最被低估的基础功很多人觉得文件操作不就是open()加read()吗有什么好学的。但真正写项目的时候你会发现文件操作里藏着一堆细节编码不对打不开、路径写错找不到文件、忘记关闭导致数据没写进去、大文件一次性读入内存直接卡死。这些问题单独拿出来都不难但放在一起就成了新手劝退组合拳。文件操作的基本流程其实就三步打开文件、读写内容、关闭文件。Python内置的open()函数负责第一步和第三步的桥梁read()、write()这些方法负责第二步。举个最简单的例子# 读取一个文本文件 f open(test.txt, r, encodingutf-8) content f.read() print(content) f.close()这段代码没有任何问题但它不是最佳写法。原因后面我会详细讲先记住一个结论open()出来的文件对象用完必须关闭。不关闭的后果在Windows上特别明显——文件会被锁住你后续想删除、重命名、覆盖它都会报权限错误。在Linux/macOS上表现没那么激烈但反复打开不关闭文件描述符会泄漏程序跑久了就会报Too many open files。文件操作涉及的知识点其实是一个完整链路路径定位到文件、指定编码解码内容、选择合适模式打开、用对方法读写、最后正确关闭。任何一个环节出问题程序都会报错或者产生错误结果。所以我建议你把文件操作当成一个整体来学而不是零散地记几个方法。2. 写在open()之前路径、编码、模式这三个基础决定了成败open()不是魔法它能不能成功打开文件取决于你给它的三个参数文件路径、打开模式、编码方式。很多人一上来就写open(test.txt)结果报错FileNotFoundError其实就是路径没搞对。2.1 路径字符串拼接不是好方案Python里表示路径有两种方式普通字符串和pathlib.Path对象。早期教程喜欢用字符串拼接比如base_dir /home/user/project data_file base_dir /data/ test.txt这种写法在Windows下会踩坑因为Windows的路径分隔符是反斜杠\Linux/macOS是正斜杠/。你要是直接写C:\Users\test.txtPython会把\U、\t当成转义字符结果路径直接错了。从Python 3.4开始官方推荐的方案是pathlib库它把路径当作对象来操作跨平台且代码可读性高from pathlib import Path # 构建路径 base_dir Path(/home/user/project) data_file base_dir / data / test.txt # 用 / 连接路径 # 判断是否存在 print(data_file.exists()) # 获取文件名、父目录 print(data_file.name) # test.txt print(data_file.stem) # test print(data_file.suffix) # .txt print(data_file.parent) # /home/user/project/data用Path对象有个额外好处你不需要在代码里写死斜杠方向pathlib会自动处理当前操作系统的路径分隔符规范。比如在Windows上data_file的字符串表示会自动变成C:\home\user\project\data\test.txt这种反斜杠格式。2.2 编码UTF-8是默认选择但Windows有坑编码问题是Python文件操作里最容易让新手崩溃的事情。简单理解编码就是字符和字节之间互相转换的规则。同一个“中”字在UTF-8里占3个字节在GBK里占2个字节你在读取文件的时候如果用了错误的编码要么得到乱码要么直接报UnicodeDecodeError。Python 3的open()函数在文本模式下默认编码是locale.getpreferredencoding()这个值在不同系统上不一样。Linux/macOS一般默认UTF-8Windows的中文系统默认是GBK。这就是同一个脚本在Windows上报编码错误、在Linux上运行正常的原因。我的建议很直接写代码的时候open()里显式指定encodingutf-8不要依赖默认值。如果你要处理的文件是GBK编码比如很多Windows下生成的旧文本文件、CSV文件那就指定encodinggbk。读取时不确定编码可以用chardet库来检测但那是另外的话题了。# 显式指定编码避免环境差异 with open(test.txt, r, encodingutf-8) as f: content f.read()写入的时候同理。特别是写文件时如果忘记指定编码Python会用默认编码去写结果换台机器用UTF-8读取就乱码了。2.3 模式r、w、a、x选错了会清空文件open()的第二个参数是打开模式。最常用的就是下面这几个我整理了一个表格模式含义文件不存在时文件存在时文件指针位置r只读默认报错安全读取开头w只写创建清空内容开头a追加写创建保留内容末尾x独占创建写创建报错开头rb/wb二进制读/写同r/w同r/w同r/wr读写报错安全打开开头w读写创建清空内容开头a读和追加写创建保留内容末尾最容易出事故的是w模式。很多人想写文件随手写了open(data.txt, w)结果把原来的内容全部清空了。如果你要往已有文件里追加内容一定要用a模式如果你不确定文件存在且不想覆盖用x模式最安全——文件已存在会直接报FileExistsError不会误删数据。还有个细节模式字符串里加了b就表示二进制模式此时读写的内容是bytes类型而不是str。图片、音频、视频、压缩包这些文件必须以二进制模式打开否则会报TypeError或者数据损坏。3. 读文件不是只会read()三种读法各有用武之地read()是最直观的读文件方法但不一定是最好的。选哪种读法核心看文件大小和你要怎么处理内容。3.1 read()小文件一口气读完with open(small.txt, r, encodingutf-8) as f: content f.read()read()不传参数时会把整个文件读进内存返回一个字符串。文件小比如几MB以内的时候完全没问题代码简洁、处理方便。但你得有个概念如果文件是5GBread()就会尝试把5GB塞进内存结果通常是直接OOM内存溢出。所以我只在处理小文件时用read()大文件绝对不用。3.2 readline()逐行读处理一行丢一行with open(data.txt, r, encodingutf-8) as f: line f.readline() while line: # 处理每一行内容 print(line.strip()) line f.readline()readline()每次读一行所以内存占用固定为一行的大小。上面的写法用while循环判断当readline()读到文件末尾返回空字符串时循环结束。这个方法的缺点是代码有点啰嗦而且如果你忘了处理行尾的换行符\n拼接数据时容易出意外。3.3 迭代器方式最推荐没有之一上面两种写法Python官方其实都不太推荐日常使用。因为文件对象本身就是一个迭代器你直接用for循环遍历代码最简洁内存占用也最省with open(data.txt, r, encodingutf-8) as f: for line in f: # 每读到一行就处理一行 print(line.strip())这个写法背后就是Python在内部帮你调用readline()每次只缓存一行数据所以处理大文件也不怕内存爆炸。我做日志分析、爬虫数据清洗全是这个写法稳定又高效。3.4 readlines()慎用除非你知道文件不大with open(data.txt, r, encodingutf-8) as f: lines f.readlines() # 返回每行组成的列表readlines()把整个文件的所有行读进内存组成一个列表。好处是你可以通过索引访问任意一行比如lines[100]。坏处和read()一样——文件大了内存扛不住。而且如果你只是需要逐行处理这个写法完全没必要白白浪费内存。文件读取时还有一个概念容易被忽略——文件指针。文件对象内部维护着一个指针指向下一个要读的位置。read()读完全部内容后指针在文件末尾再调用read()返回空字符串。seek(0)可以把指针移回开头重新读tell()可以查看当前指针位置。做断点续读、跳读时这两个方法特别有用。4. 写文件覆盖、追加、flush这些细节决定了数据能不能持久化写文件比读文件更容易踩坑因为很多问题不会立刻暴露等到程序崩溃你才发现数据没写进去。4.1 write()和writelines()写入的是字符串不是数字with open(output.txt, w, encodingutf-8) as f: f.write(hello\n) f.write(world\n)write()一次写入一个字符串。如果你想写入的不是字符串比如数字、列表、字典必须先转成str或者用json序列化。直接写数字会报TypeError: write() argument must be str, not int。# 错误示范 with open(output.txt, w) as f: f.write(123) # TypeError # 正确示范 with open(output.txt, w, encodingutf-8) as f: f.write(str(123)) # 存数字 f.write(str([1, 2, 3])) # 存列表但存进去的是字符串表示 # 结构化存储建议用json import json data {name: 张三, age: 30} with open(data.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)4.2 覆盖和追加一个字符的差别后果天壤之别w是覆盖写入文件原本的内容会被全部清空从空文件开始写。a是追加写入原有内容保留新内容从文件末尾开始写入不会破坏已有数据。这个区别看起来简单但实际项目中真的会有人写错。我之前处理一批日志本来应该用a往旧日志里追加结果脚本里写成了w跑完才发现历史日志全没了。所以我自己定的规矩是除非你确定要清空重建文件否则一律用a模式或者先用exists()判断一下文件状态再决定模式。4.3 缓冲区、flush()和close()数据不是立刻落盘的很多人以为write()之后数据马上就写到硬盘了其实不是。Python的文件写入有缓冲区机制write()先把数据写到内存缓冲区等缓冲区满了、或者调用close()、或者显式调用flush()才会真正写入硬盘。这样做的原因是减少磁盘IO频次提升性能。这个机制带来一个隐患如果程序在write()之后、close()之前异常退出了缓冲区里的数据可能还没写入硬盘直接丢失。更常见的情况是你往文件里写了几行日志程序没正常结束最后打开文件发现是空的或者少了几行。看这个例子# 写完后立刻查看文件可能啥也没有 f open(test.txt, w, encodingutf-8) f.write(hello) # 此时数据还在缓冲区 # 没有close程序继续运行如果你此时用别的程序打开test.txt文件是空的 f.flush() # 强制把缓冲区数据写入硬盘 # 这下其他程序就能看到内容了 f.close()所以有两个实用建议写完每个逻辑块后如果你需要立刻查看文件内容验证调用一次flush()。最好还是养成用with的习惯让Python自动管理关闭时机后面详细讲。4.4 文件编码在写场景下同样重要写入UTF-8编码的文件时记得在open()里指定encodingutf-8。跨平台运行脚本时如果不指定编码在Windows中文系统上写出来的文件是GBK编码拿到Linux上用UTF-8打开就是乱码。如果你希望写出来的文件是纯ASCII内容直接在open()里指定encodingascii写入非ASCII字符时立刻报错防止脏数据。5. with open()是标配上下文管理器解决90%的关闭问题前面我反复提到with现在系统说说为什么它是文件操作的标配写法。先看传统写法的问题f open(test.txt, r, encodingutf-8) content f.read() f.close()这段代码在正常情况下没问题。但如果f.read()抛了异常比如文件编码不对、磁盘出错f.close()这行就不会执行文件句柄泄漏。文件句柄是系统资源不释放的话次数多了程序会崩溃。而且你还要记得在每个分支都要close()代码稍微复杂一点就很容易漏。with的设计就是为了解决这个问题——不管代码块正常执行完还是抛异常它都会在退出时自动调用close()with open(test.txt, r, encodingutf-8) as f: content f.read() # 到这里文件已经自动关闭了很多人把这个行为叫“上下文管理器”。简单理解with为你构建了一个安全的作用域进入时打开资源退出时自动释放资源。不止文件数据库连接、网络连接、锁这些资源都可以用with管理原理是一样的。5.1 同时打开多个文件一个with搞定有时候你需要同时读写两个文件比如把source.txt的内容复制到target.txt。可以嵌套写也可以把多个open()写在一个with后面# 写法一嵌套 with open(source.txt, r, encodingutf-8) as src: with open(target.txt, w, encodingutf-8) as dst: dst.write(src.read()) # 写法二并列Python 3.1支持 with open(source.txt, r, encodingutf-8) as src, \ open(target.txt, w, encodingutf-8) as dst: dst.write(src.read())第二种写法更简洁两个文件都能在适当的时候自动关闭。5.2 自定义上下文管理器不只是文件能用with背后的机制就是协议——类里实现__enter__()和__exit__()两个特殊方法对象就变成了可上下文管理的资源。你也可以给自己的业务对象实现这个协议处理“用完必须清理”的逻辑。不过在日常开发中你更多是用现成的上下文管理器比如pathlib.Path的对象就自带open()方法可以和with结合使用from pathlib import Path p Path(test.txt) with p.open(r, encodingutf-8) as f: content f.read()效果和open(test.txt, r)一样但路径处理更灵活。6. 目录操作从os到pathlib遍历、筛选、批量处理一把梭文件操作不只针对单个文件很多时候你要处理的是一个目录下的全部文件。比如批量重命名图片、遍历项目里的所有Python文件、把多个日志合并成一个。Python里有两个方案传统os模块和现代pathlib。6.1 列出目录内容listdir vs iterdiros.listdir()返回目录下所有文件和子目录的名称列表但不包含路径信息你得自己拼接完整路径。pathlib的iterdir()则直接返回Path对象更方便import os from pathlib import Path # os模块写法 files os.listdir(.) for name in files: full_path os.path.join(., name) if os.path.isfile(full_path): print(full_path) # pathlib写法 p Path(.) for item in p.iterdir(): if item.is_file(): print(item)pathlib的路径对象直接有is_file()、is_dir()、suffix这些属性省了各种判断函数代码读起来也顺。6.2 递归遍历目录walk vs rglob如果目录下面还有子目录你需要递归遍历。os.walk()是传统方案它会递归所有子目录返回(当前目录路径, 子目录列表, 文件列表)三元组。pathlib则提供了更优雅的rglob()方法from pathlib import Path # 找出项目里所有Python文件 p Path(/path/to/project) for py_file in p.rglob(*.py): print(py_file)rglob(*.py)会递归匹配所有.py文件返回Path对象生成器。这个需求以前用os.walk()要写好几行现在一行搞定。6.3 批量重命名一个实战示例目录操作最常见的场景是批量重命名。假设你在整理照片想把IMG_001.jpg这种命名改成2024-01-01_001.jpg这种格式。用pathlib可以这样写from pathlib import Path photo_dir Path(/path/to/photos) for img in photo_dir.glob(IMG_*.jpg): # 把旧前缀替换成日期前缀 new_name f2024-01-01_{img.stem.split(_)[1]}.jpg img.rename(photo_dir / new_name) print(f已重命名: {img.name} - {new_name})这个例子说明了一个关键点直接用pathlib的rename()方法参数是目标路径的Path对象比os.rename()需要手动拼接字符串更不容易出错。6.4 glob模式匹配文件筛选利器pathlib的glob()和rglob()都支持通配符匹配*匹配任意字符串?匹配单个字符还可以用[abc]匹配字符集合。这在筛选文件时特别好用from pathlib import Path p Path(/tmp/data) # 所有以 .log 结尾的文件 logs list(p.glob(*.log)) # 所有以 access_log 开头、以 .txt 结尾的文件 access_txt list(p.glob(access_log_*.txt)) # 2023年或2024年的数据文件 data_23_24 list(p.glob(data_202[34]_*.csv))7. 文件操作避坑指南这几个坑我踩过之后才彻底明白这一节我总结几个自己实际踩过的坑每个都真实发生过写出来帮你少走点弯路。7.1 写入后立刻读取读到的是空内容场景我写一个数据处理脚本先向文件写入结果然后立刻用另一个函数读取这个文件结果读到空字符串。排查了半天发现是缓冲区问题——写入的数据还在缓冲区没有落盘读取的时候自然啥也读不到。解决方案写入完成后调用flush()或者用with把写入操作完整结束后再读取。如果你要在同一个进程里先写后读也可以考虑用r模式打开文件或者写完后重新open()读取。7.2 文件编码报错UnicodeDecodeError场景在Windows上生成的一个CSV文件用我Linux开发环境上的Python脚本读取直接报UnicodeDecodeError: utf-8 codec cant decode byte 0xd6 in position 0。解决方案这个错误就是编码不匹配。我先用chardet探测大概编码发现是GBKimport chardet with open(data.csv, rb) as f: raw f.read(10000) # 读取前1万字节来检测 result chardet.detect(raw) print(result) # {encoding: GB2312, confidence: 0.99}然后指定正确编码读取with open(data.csv, r, encodinggbk) as f: content f.read()后来我在写脚本时凡是需要处理来源不固定的文本文件都会在前面加入编码探测逻辑避免这个问题。7.3 路径里带空格或者中文字符导致找不到文件场景一个用户上传的文件路径是C:\Users\张三\My Documents\新建文件夹\report.txt脚本里用字符串拼接路径结果报错说找不到文件。解决方案这个问题通常是路径拼接不正确导致的比如用了拼接而漏掉了分隔符或者转义字符没有被正确处理。使用pathlib的/运算符可以有效规避这个问题from pathlib import Path base Path(C:/Users/张三) file_path base / My Documents / 新建文件夹 / report.txt注意即使路径包含空格、中文字符Path对象也能正确处理不需要手动转义。7.4 大文件读取导致内存不足场景处理一个8GB的日志文件直接用read()程序跑了十几分钟后内存爆掉进程被系统杀掉。解决方案换成迭代器逐行读取内存占用从GB级别降到几MBwith open(huge.log, r, encodingutf-8) as f: for line in f: process_line(line)如果需要处理超大文件的某个特定部分可以用seek()定位到指定偏移量再读避免从头遍历。7.5 文件操作抛出PermissionError场景在Windows上运行脚本读取一个Excel文件报PermissionError: [Errno 13] Permission denied。原因就是那个文件正被另一个程序比如Excel独占打开Python没办法同时读取。解决方案先关闭占用文件的程序再运行脚本。另外如果脚本本身之前打开过文件但没关闭也会导致后续操作失败。用with管理文件对象能从根上避免这个问题。8. 实战按日期拆分日志文件的完整脚本理论聊得差不多了来一个综合实战。假设你有一个大日志文件app.log里面的内容按行记录每行以日期开头2024-01-01 10:23:45 INFO 用户登录 2024-01-01 10:25:12 DEBUG 数据库写入成功 2024-01-02 09:31:08 ERROR 连接超时 2024-01-02 11:02:55 INFO 任务完成 2024-01-03 08:15:33 WARNING 磁盘空间不足需求是把这个大文件按日期拆分成多个小文件log_2024-01-01.txt、log_2024-01-02.txt以此类推。用文件操作的知识我们可以这样实现from pathlib import Path def split_log_by_day(log_path): log_file Path(log_path) if not log_file.exists(): print(f日志文件不存在: {log_file}) return current_date None output_file None try: with log_file.open(r, encodingutf-8) as f: for line in f: # 假设每行前10个字符是日期 YYYY-MM-DD date_str line[:10].strip() if not date_str: continue # 日期发生变化时切换输出文件 if date_str ! current_date: # 关闭之前的输出文件 if output_file: output_file.close() current_date date_str output_filename Path(flog_{date_str}.txt) # 使用追加模式防止重复运行时覆盖已有内容 output_file output_filename.open(a, encodingutf-8) # 写入当前行 output_file.write(line) finally: # 确保最后一个输出文件也关闭了 if output_file: output_file.close() if __name__ __main__: split_log_by_day(app.log)这个脚本有几个值得注意的设计点用pathlib处理路径避免字符串拼接和平台差异问题。用迭代器方式逐行读取大日志内存开销小日志文件几个GB也能处理。日期变化时关闭上一个文件、打开新文件注意文件关闭时机。输出文件用追加模式a而不是覆盖模式w这样即使脚本重复运行也不会丢失之前拆出来的内容。用try/finally确保所有打开的文件最终都能关闭。跑完这个脚本原本一个大文件就变成了按日期划分的多个小文件后续按日期分析日志就方便多了。同样的思路可以扩展到按小时拆分、按IP拆分、按错误级别拆分核心逻辑都是“逐行读取 按条件切换输出文件”。如果你想把多个小日志合并成一个大文件反向操作也很简单from pathlib import Path def merge_logs(output_path, log_files): with Path(output_path).open(w, encodingutf-8) as out: for log_file in log_files: with Path(log_file).open(r, encodingutf-8) as inf: for line in inf: out.write(line) # 用法 merge_logs(merged.log, [log_2024-01-01.txt, log_2024-01-02.txt])9. 文件操作进阶方向JSON序列化、CSV处理、临时文件到这里基础的文件读写已经覆盖完了。但实际开发里你常常不是直接读写纯文本而是读写特定格式的数据。我简单提几个高频场景作为进阶方向。9.1 JSON文件的读写项目配置、API响应的标准格式JSON是Python程序之间、前后端之间交换数据最常用的格式。json模块配合文件操作两行代码就能搞定import json from pathlib import Path # 写入JSON data { name: 文件操作复习, tags: [python, 文件, 复习], version: 1.0 } with Path(config.json).open(w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) # 读取JSON with Path(config.json).open(r, encodingutf-8) as f: loaded json.load(f) print(loaded[name]) # 文件操作复习几个注意点ensure_asciiFalse保证中文正常显示而不是变成\uXXXXindent2让JSON有缩进方便人阅读读取时如果JSON格式不对会抛JSONDecodeError建议用try/except包裹。9.2 CSV处理数据分析和脚本自动化绕不开的格式CSV可以用csv模块处理比手动按逗号拆分靠谱得多因为CSV字段里可能包含逗号、引号、换行只有专用模块能正确解析import csv from pathlib import Path # 写入CSV with Path(data.csv).open(w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerow([日期, 访问量, 来源]) writer.writerow([2024-01-01, 1024, 搜索引擎]) # 读取CSV with Path(data.csv).open(r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: print(row[日期], row[访问量])注意写入CSV时newline这个参数如果省略在Windows上可能每行之间多一个空行。9.3 临时文件程序运行过程中的存储如果你需要一个临时文件存放中间结果跑完自动删除可以用tempfile模块import tempfile from pathlib import Path with tempfile.NamedTemporaryFile(prefixmyapp_, suffix.tmp, deleteTrue) as temp: temp_path Path(temp.name) temp.write(btemporary data) # 在临时文件上做操作 print(f临时文件位置: {temp_path}) # 离开with后临时文件自动删除这在处理超大文件、需要临时存储中间结果的场景下特别实用。比如你从网络下载一个大文件先存到临时文件校验完成后再移动到正式目录避免下载一半的文件污染正式目录。9.4 二进制文件读写图片、音频、压缩包前面提到处理非文本文件必须使用二进制模式。比如复制一张图片from pathlib import Path def copy_binary(src_path, dst_path): src Path(src_path) dst Path(dst_path) # 分块复制避免大文件占满内存 with src.open(rb) as src_file, dst.open(wb) as dst_file: while chunk : src_file.read(8192): # 每次读8KB dst_file.write(chunk) copy_binary(photo.jpg, photo_copy.jpg)这个例子里用到了海象表达式:和分块读取二进制文件不管多大都不会撑爆内存。如果你只是简单复制文件直接shutil.copy()更省事但理解分块读写的原理对以后开发大文件处理工具很有帮助。文件操作这块内容学起来不难但真正做到不出错、性能好需要在实际项目里反复打磨。我见过很多人在项目里写文件操作代码能跑但一遇到异常就崩、一处理大文件就卡、一跨平台就乱码。能把路径、编码、模式、生命周期管理这几个点想清楚文件操作这块基本就稳了。我个人在写文件处理代码时的习惯是路径统一用pathlib打开文件统一用with open()编码统一显式指定utf-8大文件统一用迭代器逐行或分块处理。这几个习惯帮我规避掉了绝大部分文件操作相关的生产事故。如果你还没有自己的固定写法建议从今天开始把这套流程内化成肌肉记忆。