1. 为什么说文件操作是Python进阶绕不开的一关先聊一个很现实的问题Python初学者最容易产生“我好像会了但又啥都干不了”的感觉。学完了变量、循环、函数、列表、字典写出来的脚本却只能在内存里打转数据一关程序就没了。打破这个瓶颈的关键一步就是文件操作。文件操作本质上解决的是“数据的持久化”问题。程序运行时的数据都在内存里进程一结束就烟消云散。文件操作就是把内存中的数据按一定格式写入磁盘或者反过来把磁盘上的数据读进内存让程序能够跨越时间、跨越进程地复用数据。你在网上看到的各种自动化脚本、数据处理工具、网站爬虫、日志分析系统底层都离不开文件读写。甚至你每天用Word、Excel、记事本本质上也在做文件操作只是被图形界面包装了。这篇文章面向两类人一类是刚把Python基础语法学完、正准备向实战过渡的零基础学习者另一类是已经会用open()和write()但在编码、二进制、结构化数据、异常处理上吃过亏的进阶使用者。我会从文件打开与关闭的基本原理讲起一直讲到CSV、JSON、二进制数据、大文件分段处理、路径库pathlib每一部分都用真实场景说明“为什么这么做”而不是只给一段能跑的代码。把这篇文章吃透你对“程序和数据进行交互”这件事的理解会上一个台阶。2. 入门核心先搞懂open()里藏着的那几个细节2.1 文件对象和“打开”这个动作到底发生了什么很多新手第一次看到这么一段代码f open(test.txt, r) data f.read() f.close()代码能跑但脑子里对open()的认知是模糊的。我得先把这个动作讲透。open()是Python内建函数作用是向操作系统发起一个系统调用请求打开指定路径的文件。操作系统返回一个称为“文件描述符”的整数标识给PythonPython把这个描述符包装成一个文件对象也就是我们拿到的f。之后我们调f.read()、f.write()其实都是通过这个文件对象去跟操作系统交互不是Python直接操作磁盘。很多教科书只会让你记住“用完要close()”但不讲原因。原因有两层第一文件对象占用操作系统资源比如文件描述符如果不关闭打开的文件一多就会报“Too many open files”第二写入数据时Python解释器为了性能会把数据先放到缓冲区里不一定会立刻刷到磁盘。只有调用close()、flush()或者程序正常退出时缓冲区的内容才会真正落盘。如果你写完后不关闭文件数据可能还躺在内存缓冲里程序崩溃就全丢了。我不建议用裸的open()加close()因为它有个致命弱点如果在read()或write()过程中抛出异常close()那行根本执行不到文件就一直开着。可靠的写法后面马上讲但先要理解open()本身。2.2 模式字符串r、w、a、rb、wb这些组合到底怎么选open()的第二个参数是模式字符串很多人直接背表格其实换个角度理解很简单模式由两部分组成一部分决定“打开文件的目的是读还是写”另一部分决定“文件内容按什么形式对待”。先看读取类r只读文件必须存在否则抛出FileNotFoundError。光标从文件开头开始。rb以二进制方式只读适用于图片、音频、视频、压缩包等非文本文件。r可读可写文件必须存在写入会从文件开头覆盖不会截断文件。rb二进制方式的读写。再看写入类w只写文件存在则清空原有内容不存在则新建。这是最常用的写入模式也是最“危险”的因为一个疏忽就可能把原有数据抹掉。wb二进制方式只写。w可写可读同样会清空原文件。wb二进制方式的写读。最后是追加类a追加写入文件不存在会新建。写入的内容追加到文件末尾。核心特点是光标永远在文件末尾哪怕你用seek()去移动光标写入时依然会跳到末尾。ab二进制追加。a可追加可读但读取时要注意光标位置默认在末尾想读内容得先seek(0)。ab二进制追加读写。我自己见过不少新手在用w模式处理日志文件时把历史记录清空了就是因为没意识到w模式会截断文件。如果你要保留原有内容哪怕是只在末尾加几行也一定要用a模式。操作前先想清楚这个文件我要不要保留原内容如果答案是要那模式里绝不能出现w。2.3 编码参数为什么读中文会乱码很多零基础学习者第一次遇到乱码问题都是因为没有指定encoding参数。open()如果不指定编码Python会用一个默认编码通常是系统区域设置决定的编码。在中文Windows上可能是GBK在绝大多数Linux服务器上可能是UTF-8。这就导致了一个很微妙的问题同样的代码在你自己电脑上跑得好好的传到服务器上读同一个文件结果乱码了。我的建议很简单凡是打开文本文件永远显式指定encoding参数。比如with open(data.txt, r, encodingutf-8) as f: content f.read()写文件时同理with open(output.txt, w, encodingutf-8) as f: f.write(你好Python)那如何确定一个文件原本是什么编码呢最笨也最可靠的方法是直接用编辑器或命令行工具查看。如果文件里有中文用UTF-8打开正常那就是UTF-8如果用GBK打开正常那就是GBK。如果你需要处理来源不明的文件可以先用Python尝试解码raw open(unknown.txt, rb).read() for enc in [utf-8, gbk, gb2312, latin-1]: try: text raw.decode(enc) print(f可以用 {enc} 解码成功) break except UnicodeDecodeError: continue这里有个细节latin-1永远能解码成功因为它把所有字节都映射成了字符所以它只能作为最后的兜底选项不代表文件真的是latin-1。判断编码是件跨学科的事没有绝对的银弹遇到重要文件建议先备份再处理。3. 从零基础到进阶一套能直接抄的读写套路3.1 用with open()来代替手动close()现在正式给出我强烈推荐的标准写法with open(test.txt, r, encodingutf-8) as f: content f.read()with语句在这里的作用是上下文管理。文件对象实现了__enter__和__exit__方法with会在进入代码块时调用__enter__获得文件对象在代码块执行完或者抛出异常时自动调用__exit__来关闭文件。这意味着无论代码中途发生什么文件都会被正确关闭不需要你手动写close()也从根本上避免了文件描述符泄漏和缓冲区数据丢失的问题。这套写法唯一要留意的是with代码块结束后文件对象虽然还能被引用但句柄已经关闭再调用read()会报ValueError: I/O operation on closed file。所以如果你需要在with之外使用读到的数据请在with内部把数据保存到变量里。3.2 读文件的三种姿势read、readline、readlines拿到文件对象后怎么读才算合适这取决于文件大小和你需要的结构。read()不带参数时一次读完整个文件把内容作为一个字符串返回。这种方式最简单但只适合小文件。假设你现在要处理一个2GB的文本文件用read()第一次读就把所有数据全塞进内存机器直接开始卡顿甚至OOM。这不是Python不够高效而是任何语言都在内存规模上有边界。readline()每次读一行包括行尾的换行符。用一个while循环可以逐行处理内存占用很小但需要自己控制循环结束条件。with open(data.txt, r, encodingutf-8) as f: while True: line f.readline() if not line: break print(line.strip())readlines()一次性把所有行读进来返回一个列表每一行是列表里的一个元素。对需要随机访问行号的场景很方便但如果文件很大它会和read()一样吃内存。真正处理大文件的推荐做法是直接把文件对象本身放进for循环with open(data.txt, r, encodingutf-8) as f: for line in f: process(line)这样做的好处是底层按行迭代一次只把一行读到内存而且代码非常简洁。文件对象实现了迭代器协议for循环会按行自动迭代不需要你去调用readline。这是我最常用的读文件方式没有之一。3.3 写文件的细节write和writelines别用反了写文件时有两个容易混淆的函数。write()接收一个字符串把字符串原样写入。writelines()接收一个可迭代对象比如列表但它并不是在每行之间自动添加换行符。很多新手以为writelines([a, b, c])会输出三行但实际上输出的是abc连在一起。如果你想让列表里的每个元素占一行需要自己加换行符lines [第一行, 第二行, 第三行] with open(out.txt, w, encodingutf-8) as f: f.writelines([line \n for line in lines])这里还有一个新手很容易踩的坑换行符在不同操作系统上不一样。Windows用\r\nLinux和macOS用\n。如果你在Windows上写了带\r\n的文件然后拿到Linux上处理一行结尾可能会多出一个\r字符。Python的默认行为是开启universal newlines模式读文件时会把\r\n统一转换成\n写文件时会把\n转换成当前平台的默认行结束符。但如果你用二进制模式rb或wb这个转换不会发生。在处理跨平台文本文件时记住这个差异会帮你省不少排查时间。3.4 追加写入和文件指针的移动写入模式中a模式和w模式的区别体现在是否保留原内容。但a模式还有一个特性经常被忽略——即使你调用了seek(0)写入时还是会写到文件末尾。这是a模式的刻意设计目的就是保护追加操作的原子性你不用去管光标在哪追加就是追加。如果你确实想在文件中间某个位置插入内容需要用到seek()。seek(offset, whence)中的whence有三个取值0表示从文件开头计算偏移1表示从当前位置2表示从文件末尾。比如seek(0, 2)就是把光标移到文件末尾通常配合读取末尾内容或确定文件大小使用。但我要提醒一句在文本模式下seek()的行为并不总是精确的因为文件内部可能有编码转换。比如UTF-8编码的中文字符占3个字节文本模式下说seek(3)未必能跳到一个字符边界。如果要做精确的字节级操作请切换到二进制模式。这算是我踩过的一个不大不小的坑。4. 进阶必修编码问题和路径陷阱一个都不能放过4.1 UnicodeDecodeError和UnicodeEncodeError的完整解法文本处理最痛的体验就是编码异常。常见的报错分两类读的时候报UnicodeDecodeError写的时候报UnicodeEncodeError。原因几乎都是同一个当前解码/编码用的字符集和文件内容不匹配。解决方案有三个层面。第一个层面是确定文件真实编码用第2.3节里的尝试解码法。第二个层面是读写时显式指定同一个编码比如全程UTF-8。第三个层面是遇到个别坏字节时给open()传入errors参数with open(dirty.txt, r, encodingutf-8, errorsignore) as f: content f.read()errorsignore会静默丢弃无法解码的字节适合读取强健性优先的场景。errorsreplace会把无法解码的位置替换成替字符通常是适合需要保留可读性的场景。errorsstrict是默认模式遇到坏数据直接报错适合需要严格校验的场景。我的建议是在开发阶段用strict尽早暴露编码问题到生产环境处理脏数据时再考虑ignore或replace。千万别因为图省事一律ignore否则数据被悄悄丢弃了你都不知道。4.2 路径拼接为什么不要用字符串加号文件操作的另一大坑是路径。新手最喜欢这么写path data/ filename .txt在特定环境里这能跑但有个问题Windows路径分隔符是反斜杠\Linux/macOS是正斜杠/。如果你在代码里写死正斜杠Windows其实也能识别但一旦涉及到系统API或者某些特殊工具反斜杠引起的问题会让人抓狂。更重要的是用字符串拼接路径时处理上级目录、绝对路径、路径存在性判断时都需要自己写逻辑很容易出错。正确的思路是使用标准库os.path或pathlib。我强烈推荐pathlib它是Python 3.4起引入的面向对象路径库用法非常直观from pathlib import Path base_dir Path(data) path base_dir / sub / file.txt这里的/运算符被重载了用来拼接路径不管在哪个系统上都能得到正确的分隔符。pathlib还能直接判断文件是否存在if path.exists(): print(path.read_text(encodingutf-8))甚至可以直接读写文本path.write_text(你好, encodingutf-8) text path.read_text(encodingutf-8)这些方法内部都自动管理了文件打开和关闭比手写with更简洁。对那些配合目录树遍历的场景pathlib提供的glob()、rglob()能让你一行代码遍历匹配所有文件for p in Path(data).rglob(*.txt): print(p.resolve())我是在处理一个多层目录的文件整理任务时彻底倒向pathlib的。当时要统计每个子目录下图片文件的数量用os.path和os.listdir写了好几层嵌套后来改成pathlib的rglob代码量直接减半可读性也高了很多。对新手来说从一开始就用pathlib能够少走很多弯路。4.3 文件不存在和权限不足的常见报错读写文件最常见的两个异常一个是FileNotFoundError一个是PermissionError。前者通常是因为文件路径拼错或者文件还没生成后者通常是想往一个只读文件里写内容或者是没有对应目录的写权限。处理异常的方式不是层层if判断而是用try-except把可能有问题的部分包起来from pathlib import Path path Path(data.txt) try: text path.read_text(encodingutf-8) except FileNotFoundError: print(文件不存在先创建再读) except PermissionError: print(没有访问权限)另一个实用技巧在写文件前确保父目录存在然后用open的w模式创建文件。很多人用open直接写一个不存在的路径时会遇到FileNotFoundError原因往往不是文件不存在而是路径里的目录不存在。确保目录存在的写法from pathlib import Path path Path(output/2025/result.txt) path.parent.mkdir(parentsTrue, exist_okTrue) path.write_text(内容, encodingutf-8)mkdir(parentsTrue, exist_okTrue)的意思是递归创建所有缺失的父目录如果目录已经存在不报错。这是处理多级目录写入最稳的姿势没有之一。5. 再进一步二进制文件与结构化数据的读写5.1 二进制模式下读写到底在读写什么文本模式和二进制模式的本质区别在于后者不做任何编码转换。你在二进制模式下读到的是一串bytes对象里面的每一个元素都是字节值0到255。这种模式下没有“字符”的概念也没有行结束符的转换。用二进制模式的主要场景包括图片缩放、文件复制、加密压缩、解析自定义协议数据包、读取Excel底层文件等。比如最简单高效的文件复制不需要关心内容是什么with open(source.jpg, rb) as src: data src.read() with open(copy.jpg, wb) as dst: dst.write(data)但把整个文件一次性读进内存再写出去对大文件来说非常浪费。更好的做法是用固定大小的块循环复制CHUNK_SIZE 1024 * 1024 with open(source.iso, rb) as src, open(copy.iso, wb) as dst: while True: chunk src.read(CHUNK_SIZE) if not chunk: break dst.write(chunk)这里每次读到1MB数据写完再读下一段。无论原文件多大内存占用都稳定在1MB左右。这段代码是处理大文件复制、传输任务的地基版本。5.2 CSV文件的正确打开方式别再用readlines手撕处理表格数据时最容易犯的错就是用手动split字符串的方式解析CSV。CSV的全称是Comma-Separated Values但真实世界的CSV远比“逗号分隔”复杂字段内可能包含逗号、引号字段本身可能被引号包裹引号内还有转义的引号。自己用split(,)解析遇到稍微复杂一点的数据就会裂开。正确姿势是使用标准库csv模块。读CSVimport csv with open(data.csv, r, encodingutf-8, newline) as f: reader csv.reader(f) header next(reader) for row in reader: print(row)注意三个细节。一是encoding要显式指定二是newline要写上。如果不写newline在Windows上可能每行之间多出一个空行这是Python文档和csv模块长期存在的交互坑。第三个细节是csv.reader会自动处理引号包裹的字段你不需要自己做字符串清理。写CSV时同样要处理换行问题import csv rows [ [张三, 25, 工程师], [李四, 30, 设计师], ] with open(output.csv, w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerow([姓名, 年龄, 职业]) writer.writerows(rows)如果你需要按字典读写可以用csv.DictReader和csv.DictWriter以列名为键操作起来更直观。遇到中文乱码问题可以在写文件时用一个加上UTF-8 BOM的编码encodingutf-8-sig。这样Excel打开生成的CSV时能正确识别中文。这是被无数人验证过的Windows兼容技巧。5.3 JSON跨语言数据交换的最常用格式JSON是Web开发和配置文件场景里最频繁出现的格式。Python的json模块使用起来很简单import json data { name: 张三, age: 25, skills: [Python, SQL], } text json.dumps(data, ensure_asciiFalse, indent2) print(text)重点在ensure_ascii这个参数。默认情况下json.dumps会把所有非ASCII字符转成\uXXXX形式。这不会导致数据错误但可读性很差而且跟UTF-8环境下的其他JSON工具生成的格式不一致。设置ensure_asciiFalse后中文会按原始字符输出。搭配indent2缩进生成的文件就有很好的可读性。写文件时最稳妥的方式是这样的组合with open(config.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2)读文件with open(config.json, r, encodingutf-8) as f: data json.load(f)这里的json.dump和json.dumps区别是dump直接写入文件对象dumps返回字符串。load和loads同理。如果你在爬虫、接口开发或者写自动化脚本这套读写模式基本够覆盖百分之九十的场景。5.4 pickle把任意Python对象存下来有些时候你需要的不是跨语言格式而是把Python对象原样保存下次运行时直接还原。pickle模块就是干这个的。可以把列表、字典、自定义类的实例都序列化到文件里import pickle data {result: [1, 2, 3], valid: True} with open(data.pkl, wb) as f: pickle.dump(data, f) with open(data.pkl, rb) as f: loaded pickle.load(f) print(loaded)pickle的优点是方便缺点是只能Python用而且存在安全性问题——永远不要用pickle.load去读取来源不可信的文件因为恶意构造的pickle内容可以在反序列化时执行任意代码。如果你只是自己缓存中间结果pickle很合适如果你要和外部系统交换数据还是用JSON更稳妥。6. 实战演练日志文件处理与临时文件管理6.1 从零实现一个按天滚动的日志写入器日志文件是文件操作最典型的应用场景之一。初级做法是每次打开文件用a模式追加一行。但生产环境里日志文件如果一直不切割会越涨越大最后单文件几个GB查看、备份都痛苦。下面我给一个按天滚动切分的入门实现思路。核心逻辑是写入之前判断今天的日期如果日期变了就关闭昨天的文件用当前日期作为文件名的一部分新建文件。import os from datetime import datetime class DailyLogger: def __init__(self, log_dir): self.log_dir log_dir os.makedirs(log_dir, exist_okTrue) self.current_date None self.f None def _ensure_file(self): today datetime.now().strftime(%Y-%m-%d) if self.current_date ! today: if self.f: self.f.close() path os.path.join(self.log_dir, fapp-{today}.log) self.f open(path, a, encodingutf-8) self.current_date today def log(self, message): self._ensure_file() timestamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) self.f.write(f[{timestamp}] {message}\n) self.f.flush() def close(self): if self.f: self.f.close() self.f None注意里面的flush()调用。日志系统有个特殊要求程序突然崩溃时日志尽量不丢。如果只靠write()数据可能还在缓冲区里。flush()会强制把缓冲区数据写到磁盘。虽然频繁flush会带来一点性能开销但日志场景里可靠性优先。这就是一个可用的mini日志框架地基。你可以在上面扩展文件大小切割、日志级别过滤、历史日志清理等能力。很多公司的日志处理工具库底层逻辑也和这个相似。6.2 大文件行数统计一次循环搞定别全读进内存有时候你需要快速知道一个大文件有多少行。最直接的想法是with open(big.txt, r, encodingutf-8) as f: lines f.readlines() print(len(lines))但如果你处理的是几十GB的日志文件readlines()直接内存爆炸。正确做法是逐行遍历并计数count 0 with open(big.txt, r, encodingutf-8) as f: for line in f: count 1 print(count)这段代码的内存占用不会随着文件变大而增加无论文件5MB还是5GB都能稳定跑完。如果你想更快还有一种基于缓冲区的计数方式用二进制模式读取固定大小的块统计块里的换行符数量def count_lines_fast(path): CHUNK_SIZE 1024 * 1024 count 0 with open(path, rb) as f: while True: chunk f.read(CHUNK_SIZE) if not chunk: break count chunk.count(b\n) return count这种方式的优势是省去了按行迭代时的字符解码和split开销适合对极致性能有要求的场景。代价是没法对每行内容做业务处理只能机械统计换行符个数。两者怎么选取决于你的实际需求。6.3 临时文件与自动清理tempfile模块在多个程序之间传递中间数据时你可能需要一个用完就删的临时文件。标准库的tempfile模块专门解决这个问题。最常见的用法import tempfile with tempfile.TemporaryFile(wt, encodingutf-8) as f: f.write(临时数据) f.seek(0) data f.read() print(data)TemporaryFile创建的文件在文件系统里没有名字至少不对外暴露关闭后自动删除。这种方式适合数据只在同一进程生命周期内需要中转的场景。如果你需要给其他进程传递这个临时文件的路径使用NamedTemporaryFileimport tempfile with tempfile.NamedTemporaryFile(deleteTrue, encodingutf-8) as f: f.write(数据) f.flush() print(f.name) # 这里能拿到临时文件路径deleteTrue的含义是关闭后自动删除。如果设置deleteFalse关闭后文件保留但你需要自己负责后续清理。临时文件虽然小但如果没人清理日积月累也会占满目录。能交给tempfile自动管理的就别自己造轮子。我自己在写数据处理管道的中间缓存时这个模块省了不少心程序跑完不会有残留文件干净利落。7. 一线排查文件操作最常见的坑和对应解法7.1 写入后立即读取为什么读出来是空的新手经常写这种代码with open(test.txt, w) as f: f.write(hello) with open(test.txt, r) as f: print(f.read()) # 有时能读到有时读不到这个场景大多数时候能读到因为with语句退出时文件已经关闭。但如果在同一个文件对象里先写后读就会遇到空内容with open(test.txt, w) as f: f.write(hello) print(f.read()) # 空原因在于写入后文件指针停在末尾read()从末尾开始读自然读不到东西。解决方法是写入后执行f.seek(0)再把指针移回开头再读。这是我见过的高频问题之一原理说穿了非常简单但不知道的人能卡一下午。7.2 文件被占用程序一运行就PermissionError在Windows系统上如果一个文件已经被Excel或者其他程序打开Python尝试写入时就会报PermissionError。这类问题的难度不在于代码而在于找到是谁占用了文件。常规排查思路是先关闭其他程序里打开的文件再重新运行脚本。如果找不到占用者可以重启资源管理器或者重启电脑。如果程序需要持续写入一个可能被其他程序打开的文件架构上就要考虑使用不同文件的方案或者用更底层的系统API处理文件锁。这一点说出来有点“笨”但确实是实操中最常见的情形。我在处理一个自动生成报表的定时任务时就遇到过Excel没关导致脚本报权限错误的情况。从那以后我写文件之前都会先确保目标文件没有被外部程序打开并且在脚本里做了友好的异常提示。7.3 明明用with了为什么数据还没落盘with语句保证的是文件对象被关闭但不保证数据立刻写入磁盘的物理介质上。操作系统的文件系统有自己的缓存机制close()时数据从Python缓冲区进入系统缓存系统会在合适的时机再写入磁盘。如果程序突然断电数据可能还在系统缓存里没有真正落盘。想要强制写入物理磁盘需要调用os.fsync()with open(important.txt, w, encodingutf-8) as f: f.write(关键数据) f.flush() os.fsync(f.fileno())fsync()会通知操作系统把文件数据强制刷到硬盘。注意这个操作比较慢不适合每条日志都调用。但对那些“必须持久化”的关键数据比如交易记录、配置变更值得付出这个性能代价。7.4 文件操作异常速查表为了方便你对照排查我把文件操作里的常见现象、原因和解法整理成一个速查表现象可能原因标准解法FileNotFoundError文件路径写错或目录不存在用pathlib检查路径先创建父目录PermissionError文件被其他程序占用或没有写权限关闭占用程序检查目录权限UnicodeDecodeError文件编码和encoding参数不匹配尝试不同编码或使用errorsreplaceUnicodeEncodeError写入字符超出目标编码范围统一使用utf-8设置ensure_asciiFalse写入内容为空写后未把指针移回开头读之前调用seek(0)读中文乱码未显式指定encoding打开文件时固定encodingutf-8读CSV多空行未设置newlineopen时加上newline路径拼接错Windows/Unix分隔符混用改用pathlib.Path文件大小异常二进制与文本模式混淆明确文件类型选对模式大文件内存溢出一次read()或readlines()全部读入改用for逐行迭代或分块读取这张表是我在实战中反复遇到过的问题集合。遇到异常时先对号入座能省一大半排查时间。8. 最后分享一点我长期使用的文件操作习惯说几点实操心得都是我在项目里每天在用的习惯。第一所有open()调用永远显式传encoding。我宁可在每个地方多打几个字也不要让程序在不同机器上行为不一致。UTF-8在今天是绝对主流新项目统一用UTF-8历史遗留的GBK文件单独处理。第二路径处理永远用pathlib不手写字符串拼接。Path对象之间用/连接目录创建用mkdir(parentsTrue, exist_okTrue)判断存在用exists()。这些API不仅简洁更重要的是能让代码在Windows、Linux、macOS上行为一致。第三文件读写的生命周期管理永远交给with语句。这是一个不讲条件的铁律。如果代码里有裸open我会立刻审查是否有可能泄漏资源。代码评审时我第一眼看的就是with和encoding。第四写入重要数据时先写临时文件再原子替换。这个技巧尤其适合配置文件先把内容写到一个临时文件确认写成功后再通过os.replace()把临时文件替换为目标文件。这样即使写入过程中程序崩溃原始文件也不会被写坏。下面是一个简短示例import os from pathlib import Path target Path(config.json) tmp target.with_suffix(.tmp) tmp.write_text(json.dumps(data, ensure_asciiFalse, indent2), encodingutf-8) tmp.replace(target) # 原子替换replace()在同一个文件系统内是原子操作不会出现“写到一半”的损坏状态。这个技巧救过我多次尤其是程序被信号中断或者磁盘空间不足的时候。最后想特别提醒一个很多人栽过的跟头文件操作里的“追加模式”不等于“不会出错”。a模式只保证写入位置在末尾但如果你在多个进程同时打开同一个文件并向其中写日志还是可能出现内容交错。如果需要多进程安全的日志写入建议用标准库logging模块它自带文件锁和多进程的兼容处理。手动open写日志的脚本在单进程任务里完全没问题但到了多进程环境请转用成熟方案。文件操作看起来是Python最基础的能力但越往深走越发现它连接着编码、路径、内存、并发、操作系统协议这些底层概念。把这一层地基打牢后面学数据处理、网络编程、自动化运维都会顺很多。希望这篇文章能陪你把这条路走扎实。