
我接触Python最早写的工具就是从文件读写开始的。当时要统计十几个日志文件里某个关键词出现的次数手工做了快一上午最后用Python几十行搞定。后来不管是爬虫存数据、脚本导出报表还是做自动化处理几乎每个程序都绕不开文件读写。可以说文件读写是Python基础里最“实用”的一环但也是新手最容易出错的一环。这篇教程我不想只摆语法而是想把文件读写的前因后果、常见坑、以及实际项目里的用法一次讲透看完你就能直接拿去干活。1. 读写文件之前先把这三件事弄明白路径、模式、编码很多新手一上来就写open(test.txt)报错就懵了。文件读写出问题十个里有八个是在这三件事上栽跟头路径没找对、模式选错、编码不统一。这三件事搞不定后面读多少代码都没用。1.1 路径程序在哪文件在哪永远先搞清楚“你在哪”路径问题是文件读写里最基础也最阴间的问题。你以为你写的是open(data.txt)程序就去当前目录找文件但“当前目录”不一定是你代码文件所在的目录而是你运行Python命令时所在的目录。这个目录叫工作目录它决定了一个相对路径最终指向哪里。# 这样写文件会去哪里找 with open(data.txt, r, encodingutf-8) as f: print(f.read())如果我在/home/user/project目录下运行python script.py工作目录就是/home/user/project代码会去这里找data.txt。但如果我在项目上级目录运行python project/script.py工作目录就变成了/home/user程序就会去/home/user下面找data.txt找不到就报FileNotFoundError。这就是为什么我强烈建议只要涉及文件路径优先用绝对路径或者基于当前文件位置来拼路径而不是指望工作目录“恰好正确”。from pathlib import Path # 获取当前py文件所在目录再拼上目标文件名 base_dir Path(__file__).resolve().parent file_path base_dir / data.txt with open(file_path, r, encodingutf-8) as f: print(f.read())Path(__file__).resolve().parent的意思是“当前脚本的绝对路径取它所在的目录”。这样做的好处是不管你从哪里运行这个脚本只要脚本本身不挪位置文件路径就一定是稳定的。这是我处理所有Python文件读写项目的第一条铁律。另一个容易踩的坑是Windows路径里的反斜杠。Windows路径长这样C:\Users\admin\data\test.txt如果直接写在字符串里\U、\a这些会被当成转义字符直接报错或者路径不对。最稳妥的写法是字符串前面加r或者直接统一用正斜杠因为Windows也认正斜杠。# r 开头把反斜杠当普通字符 file_path rC:\Users\admin\data\test.txt # 或者直接换成正斜杠 file_path C:/Users/admin/data/test.txt1.2 模式开文件不是拿起来就读mode 决定你的操作边界open()函数第二个参数是模式新手往往只背过r和w实际用起来才发现选择比想象中多。模式拆开其实就两部分一个是操作类型读、写、追加一个是文件类型文本、二进制。模式含义文件不存在时文件存在时指针位置r只读文本报错正常读开头w只写文本创建清空内容开头a追加文本创建保留内容末尾r读写文本报错可读可写开头w写读文本创建清空内容开头a追加兼读文本创建保留内容末尾rb只读二进制报错正常读开头wb只写二进制创建清空内容开头ab追加二进制创建保留内容末尾注意看w模式只要文件存在打开瞬间内容就会被清空。我见过不少人在该用a的地方习惯性写了w结果之前的记录全部被冲掉连个后悔的余地都没有。所以要不要覆盖原文件这个决定一定要在open之前想清楚不是打开以后再纠结。还有一个容易忽略的细节open(filename)不写模式时默认是r也就是只读文本模式。这个默认值看似贴心但如果你企图往这个文件对象里写东西会直接得到io.UnsupportedOperation: not writable。与其靠猜不如每次显式写清楚模式代码可读性也更好。1.3 编码先约定好字符集中文不乱码的分水岭编码问题说白了就是“用什么规则把字符转成字节以及用什么规则把字节还原成字符”。同一个汉字用UTF-8存是一种字节序列用GBK存又是另一种字节序列。如果你写入的时候用了UTF-8读取的时候用GBK去解就会得到乱码或者直接报UnicodeDecodeError。Python 3里open()函数如果不指定encoding参数会使用系统默认编码。在Windows上这个默认值曾经是GBK在Linux和macOS上一般是UTF-8。同一个脚本在Windows上能正常读的中文文件拿到Linux上可能就崩了就是因为默认编码不一样。所以我的建议非常简单粗暴凡是文本文件open的时候永远带上encodingutf-8。这样至少你和你的队友、你的服务器之间有一个共同的约定。如果你要处理的是别人给你的Excel导出的CSV那大概率是GBK编码这时候老老实实写encodinggbk去读不要含糊。# 指定编码读写避免跨平台乱码 with open(notes.txt, w, encodingutf-8) as f: f.write(中文内容) with open(notes.txt, r, encodingutf-8) as f: text f.read()再多提一句写文件时如果遇到编码错误可以试试errorsignore参数它会跳过无法编码的字符。但这是“死马当活马医”的应急手段因为跳过字符等于静默丢数据排查起来更困难。能用编码解决的问题就不要用忽略来解决。2. 读文件的几种姿势read、readline、readlines 和迭代式读取读文件的方法很多但每种都有它的使用场景。选错方法不一定会报错但很可能在性能或代码可读性上吃亏。2.1 read()适合小文件的一次性读取read()不带参数时会把整个文件内容作为字符串返回。这个操作简单直接适合配置文件、小文本、模板文件这类体量不大的场景。with open(config.json, r, encodingutf-8) as f: data f.read() print(data)但如果你手上是一个几百MB的日志文件直接read()会把全部内容加载进内存。Python的字符串又是不可变对象加载大文件时不仅占用内存还有可能把程序直接拖垮。我最早处理日志时就干过这事程序跑了一半内存报警。从那以后我就养成了一个习惯不确定文件大小时不要用read()。read()也可以传一个数字参数表示最多读取多少个字符。这个能力在按块处理大文件时有用不过更常见的是后面要讲的迭代式读取。2.2 readline() 和 readlines()按行处理的两个选择readline()每次只读一行包括行尾的换行符。你可以放在循环里一行一行读但这样写比较啰嗦还得自己处理文件读完的情况。readlines()则会一次性把所有行读进来返回一个列表每一行是一个元素。看起来方便但它同样会把整个文件加载到内存里和read()的问题是一样样的。with open(data.txt, r, encodingutf-8) as f: lines f.readlines() for line in lines: print(line.strip())这个方法适合文件不大且你确实需要随机访问某一行或者需要多次遍历内容的场景。比如你要先统计总行数再处理每行数据用readlines()拿到的列表就很方便。2.3 for line in file最推荐的读法如果只是从头到尾按行处理Python最地道的写法是直接把文件对象当成可迭代对象放在for循环里with open(access.log, r, encodingutf-8) as f: for line in f: line line.strip() if line: process_line(line)文件对象实现了迭代协议for循环每次从文件里读一行并处理一行不会一次性把整个文件塞进内存。这就是处理大文件的核心招数。我在做日志分析、数据清洗的时候几GB的文件都是用这种方式跑的内存占用非常稳定。有人可能会问这和readlines()到底差在哪本质区别在于“懒加载”和“一次性加载”。for line in f是边读边处理readlines()是全部读完再处理。如果只是顺序扫描前者在内存占用上是压倒性优势。2.4 大文件逐行读不爆内存的补充技巧有时候一行本身就可能非常长比如十几MB的单行日志、由模型生成的超长文本等。这时就算用for line in f单行也会占掉大量内存。更稳的做法是设置固定大小按字节块去读。with open(huge_file.txt, r, encodingutf-8) as f: while True: chunk f.read(8192) if not chunk: break process_chunk(chunk)read(8192)一次读8192个字符处理完再读下一段内存占用被控制在常数级别。这种写法在处理超长单行或非结构文本时比for循环更稳。3. 写文件write、writelines、flush 以及追加模式的讲究写文件看起来比读文件简单但细节一点也不少。什么时候真正写入硬盘、是覆盖还是追加、一次性写很多内容时是write还是writelines这些问题我当年都踩过坑。3.1 write 和 writelines 的区别write()接收的是一个字符串写入后返回写入了多少个字符。writelines()接收的是一个可迭代对象比如列表或者生成器但它“不会”自动加换行符这和很多人的直觉正好相反。lines [第一行, 第二行, 第三行] with open(out.txt, w, encodingutf-8) as f: # 这样写不会换行结果就是“第一行第二行第三行”挤在一起 f.writelines(lines) # 每个元素后面必须自己加换行符 f.writelines(line \n for line in lines)如果只是写一行内容用write()就够如果想把一个列表的所有元素写进文件writelines()配合生成器表达式非常简洁。只是要记得补换行这是新手最容易忽略的。反过来如果你在循环里反复调用write()每次调用次数非常多也可以考虑改成攒一批再写减少系统调用的次数。3.2 覆盖 vs 追加a模式比w模式安全得多前面提到过w模式下文件内容会被清空。很多实际场景里用户并不想每次运行都覆盖旧文件而是把新数据追加到末尾。比如我做采集脚本时每次抓取新数据都希望追加到同一个csv文件里这时候就该用a模式。with open(records.csv, a, encodingutf-8, newline) as f: f.write(2025-05-01,北京,18\n)a模式还有一个隐藏特性不管你怎么移动文件指针写入操作始终发生在文件末尾这种“向末尾锁定”的行为在多进程写同一个文件时也比w模式更不容易出错。如果要兼顾“写入前先清空旧数据”和“文件不存在时能创建”那就用w这是两种模式各自的核心使用场景。千万不要把覆盖和追加的边界线画模糊了否则会付出数据丢失的代价。3.3 flush 和 close数据到底是什么时候落到硬盘上的这是个特别容易被忽视的问题。Python的文件写入是有缓冲的你调了f.write()内容先写进内存缓冲区不一定立刻就同步到硬盘。只有缓冲区满、调用flush()、调用close()或者程序正常结束时数据才会真正写到磁盘上。这意味着如果你的程序在写入后、正常关闭前崩溃了或者你直接复制文件、断点调试强制停止缓冲区里的数据可能就丢了。所以对重要数据写完关键内容后建议显式调用f.flush()。with open(important.txt, w, encodingutf-8) as f: f.write(这段必须立刻落盘) f.flush() # 强制把缓冲区内容写入磁盘 # 继续后面的逻辑这里多说一句flush()是“把缓冲区里已接收的内容交给操作系统”但操作系统可能也有一层缓存。如果要确保物理落盘可以用os.fsync(f.fileno())但正常业务场景中flush()已经足够用了。3.4 newline 参数和换行符的坑写文本文件时不同操作系统对换行的处理不一样。Windows用\r\nLinux和macOS用\n。Python在文本模式下默认会对换行符做转换在Windows上写\n会自动变成\r\n。大多数情况下这是好事但如果你在生成CSV文件然后还要拿给Excel打开这个转换反而会带来麻烦。解决方案是打开文件时指定newline这样Python就不会再做任何额外转换import csv with open(output.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([姓名, 城市, 年龄])encodingutf-8-sig又是另一个实用技巧它会在文件开头写入一个BOM头Excel用这个BOM来识别UTF-8文件不然打开中文CSV就是乱码。这个问题我当年处理过一次从那以后只要给Excel准备CSV我都默认用utf-8-sig。4. with 语句为什么重要资源释放、异常安全和多文件管理4.1 忘掉close()后果不一定立刻出现但迟早出事在Python里文件对象占用的是操作系统级别的资源。直接调用open()而不close()后果就是文件一直被进程占用。在Windows上这会导致别的程序无法删除或修改这个文件在Linux上过多未关闭的文件描述符会耗尽进程的资源上限程序最终报OSError: [Errno 24] Too many open files。最要命的是没有关闭文件时程序可能不会立刻报错直到文件数量积累到一定程度。我见过一个定时任务每天早上跑一次每次处理几百个文件但一直不关闭跑了快一个月后突然挂了排查半天才发现是文件描述符耗尽。4.2 with的底层原理上下文管理器with open(...) as f是Python推荐的标准写法。它的作用可以简单理解成无论with代码块里的代码正常执行完还是中途抛了异常文件都会自动关闭。with open(test.txt, r, encodingutf-8) as f: data f.read() # 执行到这里文件已经自动关闭了文件对象实现了上下文管理器协议with进入时会调用__enter__方法获取资源退出时会调用__exit__方法释放资源。整个过程不用你手动处理也没有遗漏的可能。有人图省事写f open(...)然后后面完全不管关闭这是我最不建议的写法。哪怕你觉得“脚本跑完就退出了系统会回收”在长时间运行的服务、定时任务或批量处理脚本里这种习惯早晚会变成线上事故。4.3 多个文件的with写法项目里经常需要同时操作多个文件比如把一个文件的内容处理后写入另一个文件。这时可以在一个with语句里并列打开多个文件with open(input.txt, r, encodingutf-8) as fin, \ open(output.txt, w, encodingutf-8) as fout: for line in fin: fout.write(line)用反斜杠续行是为了不让单行代码太长保持可读性。这个写法既保证读文件正常打开也保证写文件正常关闭。还有更复杂的动态文件数量场景可以用ExitStack去管理但相对少见这里先用双文件版本记下核心思路就足够了。5. 中文编码与二进制文件不加班也不乱码5.1 编码问题的根源在“写入和读取用同一套规则”很多人一遇到中文乱码就头大其实根源就一件事写入用了一种编码读取用了另一种。比如你用Python默认的GBK写入一个名为“笔记.txt”的文件后面在Linux上以UTF-8读取得到的就会是一堆乱码字符。要想根治记住一个原则写入端和读取端必须约定同一个编码。我的默认方案是UTF-8因为它在跨平台、跨语言场景下兼容性最好。只有面对Windows老软件、旧系统或别人给的GBK文件时才明确指定gbk读取。常见的报错UnicodeDecodeError: utf-8 codec cant decode byte...意思是当前文件里的字节序列用UTF-8规则解不开。这时候要么尝试gbk要么看一下文件的原始比特是哪种编码。如果手头没有编辑器可以判断可以先用open(filename, rb)读一下前几个字节结合十六进制看个大概。5.2 二进制文件的读写图片、压缩包、模型文件怎么处理处理图片、压缩包、exe这类文件时必须在模式里加上b也就是二进制模式。二进制模式下读出来的是字节对象bytes写进去的也必须是字节对象Python不会帮你做任何编码转换。# 复制图片 with open(source.png, rb) as fin: data fin.read() with open(copy.png, wb) as fout: fout.write(data)这段代码把一张图片完整读进内存再原样写出去。小文件这么做没毛病大文件我不建议直接read()全部加载还是按块读写更稳with open(source.zip, rb) as fin, open(copy.zip, wb) as fout: while True: chunk fin.read(65536) if not chunk: break fout.write(chunk)65536是64KB这个大小在磁盘读写和内存占用之间比较平衡。真正做文件复制时当然可以用shutil.copyfile但理解底层按块读写的逻辑对处理网络流、管道流都很有帮助。5.3 读取自带编码信息的文件用codecs还是直接用openPython官方推荐直接使用open(..., encoding...)它内部已经足够可靠。早期教程里常见的codecs.open()现在已经不再需要了除非你要处理编码自动检测这类特殊需求。判断文件编码可以用chardet或schema之类的第三方库但它们有判断出错的风险生产环境里能确认编码就尽量不要靠猜。6. 项目中最常见的数据落盘方案CSV 与 JSON 的读写实操文本文件的读写是基础但实际项目里大家更多地在读写结构化的数据格式。CSV和JSON是笔记本里最高频的两种很多教程只会给一句“用csv库”或者“用json库”很少讲清楚里面的坑和细节。6.1 CSV用csv库而不是手工拼字符串新手最常见的做法是自己拼字符串写CSV# 反面教材手工拼CSV with open(test.csv, w, encodingutf-8) as f: f.write(姓名,城市\n张三,北京\n)这个做法在数据很简单的时候能跑但一旦字段里出现了逗号、引号或换行符CSV格式就会直接破坏。正确的做法是用标准库的csv.writer和csv.reader。import csv rows [ [省份, 城市, 人口], [广东, 广州, 1500], [浙江, 杭州, 1200], ] with open(city.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerows(rows)writerows一次写入多行比在循环里反复writerow更高效可读性也更好。读取时用csv.readerwith open(city.csv, r, encodingutf-8-sig, newline) as f: reader csv.reader(f) for row in reader: print(row)如果CSV有表头而且你想按列名访问数据用csv.DictReader会舒服很多。它会自动把第一行当成字段名后面每行返回一个字典。with open(city.csv, r, encodingutf-8-sig, newline) as f: reader csv.DictReader(f) for row in reader: print(row[城市], row[人口])还有一点如果文件太大用pandas读CSV内存不够csv.reader这种逐行扫描的方式反而是最灵活的兜底方案。6.2 JSON读和写都别忘了ensure_ascii这个参数JSON格式在配置、接口对接、数据持久化中非常常见。Python的json库用起来很简单但写中文时有个特别坑的默认行为。import json data { name: 张三, city: 北京, tags: [python, 爬虫] } # 反面教材直接写 with open(data.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiTrue) # 默认就是True上面这段代码看起来没毛病但打开生成的JSON文件会发现中文全变成了\u5f20\u4e09这种转义序列。对程序来说它也能正常解析但人眼完全没法读。解决方法是设置ensure_asciiFalse这样中文会被原样写入。with open(data.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)加上indent2会让JSON文件按两层空格缩进输出排错、对比都方便很多。读取JSON就是对应的json.load直接用with open(data.json, r, encodingutf-8) as f: res json.load(f) print(res[name])读文件时还要小心一种情况文件内容非法JSON时json.load会抛JSONDecodeError。接到第三方数据时我建议用try...except包住解析过程至少把错误信息打出来而不是让整个脚本直接崩溃。7. 文件读写中我真的踩过的坑和排查思路这一节我想换个方式不做教程式的清单而是如实说说我在实际项目里遇到过的问题。你把这些当成“别人已经踩过的雷”多少能少走点弯路。7.1 文件找不到但路径看起来明明是对的有一次我写脚本处理同事给的报告代码里写的是open(rC:\Users\admin\Desktop\报告.txt)运行后一直报FileNotFoundError。我反复检查路径觉得没问题最后才发现文件名里藏了一个不可见字符。原来同事是在Word里复制文件名时把全角空格或者零宽字符也带进来了。这事情用肉眼看根本看不出来。我的排查方法是遇到路径报错又确认路径正确时先把路径打印出来再复制到文件管理器里搜索一遍看是不是真的存在。或者直接用Path(报告.txt).exists()确认一下文件是否存在再继续后面的逻辑。from pathlib import Path p Path(rC:\Users\admin\Desktop\报告.txt) print(p.exists()) print(p.resolve())7.2 编码错误UnicodeEncodeError和UnicodeDecodeError写CSV处理中文时我遇到过UnicodeEncodeError: gbk codec cant encode character原因是Windows默认编码是GBK而内容里有GBK无法表示的字符。解决方式就是全局指定UTF-8。Python 3.7开始还可以在代码最顶部加一行import sys sys.stdout.reconfigure(encodingutf-8)但文件的读写还是那句老话open的时候统统显式写上encodingutf-8没有例外。另一个是控制台输出乱码。很多人分不清“文件编码问题”和“终端显示编码问题”。有时候文件本身完全正常只是Windows控制台的代码页不认UTF-8字符所以显示乱码。这种情况可以先看文件的二进制内容用xxd或者十六进制编辑器确认字节是否正确。判断标准很简单如果文件字节是对的问题出在显示端如果文件字节就是错的问题出在写入端。7.3 flush和close的教训程序说完成了文件却是空的我做过一个批量导出脚本处理几千行数据后写入文件程序结束前会打印一句“导出完成”。结果有一次同事跑完脚本打开文件发现是空的。后来我才明白程序崩溃在缓冲区数据落盘之前或者操作方式直接把文件句柄弄丢了。从那以后对于重要的导出流程我会在写完后显式flush()和close()然后再打印完成提示。更稳妥的做法是把主要逻辑放进try...finally或with里确保资源正常释放。如果你发现“明明调了write但文件是空的”第一反应就该去看是否没有关闭文件、没有flush或者是否在with块之外访问了已经关闭的文件对象。7.4 循环里反复open文件资源泄漏的典型场景有一次写一个批量处理脚本循环里要处理1000个文件我图省事只写了f open(...)处理完就把文件变量扔了没有close。跑了几百个文件后程序报OSError: Too many open files文件句柄全部耗尽机器上其他进程也被拖累了。排查之后才意识到是没关闭文件的问题。如果你需要在循环里处理多个文件务必保证每个文件都走with语句而不是手动管理。用with之后即便处理逻辑中抛异常文件也能正常关闭。# 正确示范每个文件都用with管理 for file_name in file_list: with open(file_name, r, encodingutf-8) as f: # 处理逻辑 pass还有一种容易忽略的场景文件虽然用with打开了但你在处理过程中抛了异常导致下面的写入逻辑没执行。这不是文件关闭的问题而是业务流程设计的问题。我的经验是把“必须完成的善后工作”放在finally或者尽量简洁的分支里避免处理一半时被意外情况截断。7.5 追加模式a和读写模式r的迷惑行为有一次我想同时读和写同一个文件用了r。我发现写入的内容并没有出现在我期望的位置而是根据当时文件指针的位置来决定写哪里。r不会自动追加到末尾指针初始在开头所以如果你先读后写内容会写在读完之后的位置可能把原文件内容覆盖掉。如果不理解指针概念r非常容易制造诡异问题。我更推荐的做法是先读后写分开读用r写用a或新文件完成后再重命名替换。这样操作意图清晰也好排查。在实际业务里能在文件中间精准修改内容而不损坏文件结构的场景其实很少绝大多数需求是“读取出来→在内存里处理→完整写回”这比在文件内部改来改去可靠得多。我个人在实际操作中的体会是文件读写的语法并不多难的是养成“路径用绝对、编码写UTF-8、操作走with、重要内容手动flush”这几个习惯。这些习惯一旦建立你写出来的脚本就不会突然在别人电脑上报错也不会处理到一半丢数据。如果你现在刚开始学Python建议把这篇里的代码自己敲一遍尤其是用大文件跑一跑迭代式读取感受一下内存占用和运行速度的差别。文件读写这个基本功值得你花一个下午彻底弄透。