带过不少学 Python 的新人,我观察到一个几乎规律性的现象:前两周大家都很顺利,print会了,条件判断会了,列表字典也熟了,一切看起来都挺好。等到第一次作业需要"把数据处理后保存成文件",或者"读一个 txt 里的名单挨个处理"的时候,大批人开始卡壳。不是不会写代码,而是对着open()、read()、write()这几个函数不知道从哪里下手,网上的教程又各讲各的,看得更晕。文件读写这个能力,本质上是从"在终端里自嗨"到"真正和数据打交道"的分界线。爬虫要存数据,办公自动化要碰 Excel,数据分析要先读文件再导出结果,日志处理要持续追加内容——这些应用场景里,文件读写都是地基一样的存在。这篇文章我就按自己多年的使用经验,把 Python 文件读写这件事从头到尾拆一遍,从open()的底细到路径编码的坑,从逐行读大文件到用 pandas 落地 Excel,一次讲透。
1. 文件读写是 Python 学习中绕不开的那道分水岭
1.1 从控制台输出到数据落盘:跨过这道坎才算真正入了门
很多人第一次接触程序输出,用的是print()。这玩意儿确实方便,程序跑完,结果在终端里印得整整齐齐。但你有没有想过一个问题:终端里的内容是"一次性的",关掉窗口之后什么都没留下。程序一旦结束,这些数据就跟没存在过一样。
文件读写解决的就是这个核心问题——持久化。把你折腾半天计算出来的结果、从网上抓下来的数据、用户填写的配置信息,实实在在写到磁盘上,让它们在下一次程序启动时还能被读到。这一点太重要了,以至于我可以直接说:不会文件读写,你写的 Python 程序基本就只能停留在"练习题"阶段,一碰到真实需求就会露怯。
我带过的新人里,有好几个都是卡在这一步差点放弃。他们当时的心态很有代表性:明明每个函数分开看都懂,一组合就乱。后来我发现,问题不出在智商上,是教程没把文件读写这件事讲透。大多数人以为文件读写就是"打开-读/写-关闭"三步,但实际操作中还要考虑文件存不存在、编码是不是 utf-8、写入的内容有没有被缓冲、路径到底怎么拼接。这些细节不看清楚,踩坑是必然的。
1.2 爬虫、办公自动化、数据分析:三大主力场景全绕不开它
先说爬虫。写爬虫的人几乎都会遇到同一个需求:把抓取到的网页内容或结构化数据存下来。你不可能每次运行爬虫都只把结果打印在屏幕上,而是要保存成文本文件或者 CSV,方便后续查看和分析。
再说办公自动化。这年头 Python 在办公场景里最热门的用法之一就是批量处理 Excel、Word、CSV。热搜词里就有"pandas 读写 excel 文件""python 写入 excel",可见这需求有多普遍。但凡要在程序里操作这些文件,第一步永远是"把文件打开读进来"。
最后是数据分析。数据分析的标准流程是:读数据、清洗、分析、可视化、导出结论。你从 pandas 里读 CSV、读 Excel,分析完再to_csv()或to_excel()落盘,这一整套流程的起点和终点都是文件读写。
你发现没有,这三个热门方向就像三根柱子,撑着 Python 应用的大半边天,而文件读写就是这三根柱子共同的地基。地基不牢,上面盖什么都悬。
2. open() 函数全面拆解:模式、编码、资源释放一次说透
2.1 打开模式对照:r、w、a 以及带 + 和不带 + 的本质区别
Python 里一切文件操作都从open()开始。这个函数看起来简单,两三个参数而已,但里面藏着不少细节,尤其是第一个参数后面跟着的模式字符串。
我先把最常用的模式列一张表,对应关系一目了然:
| 模式 | 含义 | 文件不存在时 | 文件存在时 | 文件指针位置 |
|---|---|---|---|---|
r | 只读 | 报错 | 正常打开 | 文件开头 |
w | 只写 | 创建新文件 | 清空原内容 | 文件开头 |
a | 追加写 | 创建新文件 | 保留原内容 | 文件末尾 |
r+ | 读写 | 报错 | 正常打开 | 文件开头 |
w+ | 写读 | 创建新文件 | 清空原内容 | 文件开头 |
a+ | 追加读 | 创建新文件 | 保留原内容 | 文件末尾 |
rb | 二进制只读 | 报错 | 正常打开 | 文件开头 |
wb | 二进制只写 | 创建新文件 | 清空原内容 | 文件开头 |
这张表里最需要刻进脑子里的有两个点。第一,w系模式会在打开文件的一瞬间把原文件内容清空,不管你之后有没有真正写入任何东西。新手最悲剧的操作就是:本来想改一个配置文件,用了w打开,代码还没跑到写入那一步就先报错了,结果原文件已经被清成了空文件。第二,+号的意思不是"追加",而是"在原有读或写的基础上,增加另一方向的操作"。r+表示"可读可写",a+表示"可追加可读",好多人以为a+里的+是"追加"的意思,这是个流传很广的误解。
2.2 编码参数:为什么 Python 3 默认编码还会遇到乱码
open()的第二个容易忽略但是极其关键的参数是encoding。Python 3 的字符串是 Unicode 体系,默认使用 UTF-8 编码保存文本文件,这本来是好事,但实际使用中乱码依然常见,原因在于:你打开的文件不一定是用 UTF-8 存的。
尤其在国内环境,很多老系统、旧软件、Windows 记事本默认保存文件用的还是 GBK 或 GB2312 编码。比如你拿到一个对方用 Windows 记事本另存的 txt 文件,直接用open('data.txt', encoding='utf-8')去读,大概率会抛UnicodeDecodeError。
我推荐的习惯是:只要能确定文件来源的编码,就显式写encoding='utf-8';不确定的时候,先尝试 utf-8,报错了再换 gbk 试。下面这个写法比较稳:
try: with open('data.txt', 'r', encoding='utf-8') as f: content = f.read() except UnicodeDecodeError: with open('data.txt', 'r', encoding='gbk') as f: content = f.read()顺便提一句newline参数。在 Windows 上写文本文件时,默认换行会被转成\r\n,读的时候又会被转回\n,这在某些对格式敏感的场景下会带来麻烦。如果需要精确控制换行行为,显式传newline=''或者按需传newline='\n'会安全很多。
2.3 with 上下文管理器:一句话解决忘记 close() 的隐患
文件操作有个经典问题:打开了文件之后忘了关闭。忘了close()会有什么后果?文件描述符泄漏、文件被占用导致其他程序无法访问、写入缓冲区的数据没有真正落到磁盘……这些问题在开发环境里可能不明显,一旦放到长期运行的脚本或服务里,就会变成诡异的 Bug。
with语句就是为这个问题准备的。它会在代码块结束之后自动调用close(),哪怕代码块内部抛了异常,文件也会被正确关闭。
# 不推荐的写法 f = open('note.txt', 'w', encoding='utf-8') f.write('hello') f.close() # 万一上面 write 抛异常,这里根本执行不到 # 推荐的写法 with open('note.txt', 'w', encoding='utf-8') as f: f.write('hello') # 缩进块结束,文件自动关闭,异常也能保证释放资源我见过有人纠结"用 with 是不是多此一举",我的回答是:这不是多此一举,这是用最简单的方式彻底消灭一类 bug。等你真正经历过"文件明明写入了但内容一直是空的"、"文件被占用删不掉"这类问题之后,你就知道手动管理生命周期有多不靠谱了。
3. 文本文件读取的几种常见姿势,按场景选择而不是背函数
3.1 read()、readline()、readlines() 三个直觉方法的差异
open()打开文件之后,下一步就是读取内容。Python 提供了几个名字上很好懂的方法,但它们的区别和应用场景搞清楚了,能省不少事。
read(size):不传参数时一次性读取整个文件内容,返回一个字符串;传了size就读取指定字节数。readline():读取一行内容,包含末尾的换行符;也可以传字节数,读取该行指定字节部分。readlines():一次性读取整个文件,按行分割后返回一个列表,每一行是列表中的一个元素。
这三个方法里,read()和readlines()都涉及"一次性读入全部内容",逻辑上最爽,但内存上最危险。我处理过一个接近 2GB 的日志文件,别人用readlines()直接跑,内存眼看着涨到 4GB 然后进程被系统干掉。文件越大,"一次读到底"的代价越明显。
相比之下,readline()每次只处理一行,内存占用恒定,适合逐行扫描的场景。但它的缺点是写法麻烦,要自己维护一个循环。
3.2 for 循环直接迭代文件对象:处理大文件的优雅方式
其实 Python 里最推荐的做法,既不是readlines(),也不是手动readline(),而是直接拿for循环去迭代文件对象本身。文件对象在 Python 中实现了迭代器协议,每次循环自动取出下一行,底层有自己的缓冲机制,既按行处理,又不会一次性把整个文件塞进内存,内存占用和代码优雅程度都能兼顾。
with open('big_log.txt', 'r', encoding='utf-8') as f: for line in f: # 每一行自动去掉末尾换行符再处理 line = line.strip() if 'ERROR' in line: print(line.strip())这段代码逐行扫描一个超大日志文件,只把包含ERROR的行打印出来。不管日志文件是几十 MB 还是几十 GB,内存占用基本恒定,因为同一时刻只保留当前这一行。这是我在处理服务器日志时最常用的套路,实测下来非常稳。
3.3 怎么选:从文件大小、数据形态、内存占用三个维度做决策
给一个可以直接抄作业的选择思路:
- 小文件(几 KB 到几 MB):怎么读都行,追求简单就
read()一把梭。 - 需要按行处理的大文件(几十 MB 以上):优先
for line in file迭代读取,内存和安全兼顾。 - 文件不大但需要随机访问某几行:
readlines()转列表后按下标取行,理解成本低。 - 底层二进制数据处理或网络流场景:用
read(size)分块读取,控制每次读入的量。
我把不同方案的对比整理成了一张表,方便你快速查阅:
| 方法 | 返回类型 | 内存占用 | 最适合场景 |
|---|---|---|---|
f.read() | 字符串 | 与文件大小成正比 | 小文件整体读取、全文搜索 |
f.readline() | 字符串 | 恒定 | 逐行处理但不想用 for 迭代的场景 |
f.readlines() | 列表 | 与文件大小成正比 | 文件不大且需要按行索引访问 |
for line in f | 迭代器 | 恒定 | 大文件逐行扫描、日志分析 |
4. 写入文件的讲究:覆盖、追加、缓冲与指针
4.1 write 与 writelines:写给新人看的基本习惯
读取聊完,轮到写入。写入端的基础操作是两个:write()和writelines()。
write()传入一个字符串,直接写入文件。注意它不会自动帮你加换行符,你需要显式写出\n:
with open('output.txt', 'w', encoding='utf-8') as f: f.write('第一行\n') f.write('第二行\n')writelines()接收一个可迭代对象,把每一项逐个写入文件。这里有个新手容易踩的坑:writelines()名字里带 "lines",但它同样不会自动在每行末尾加换行符。如果你传入的列表元素本身不带\n,写出来的文件会变成一大坨挤在一起的文本。
我自己的习惯是:要写多行内容时,用列表推导式把所有行拼成带换行符的字符串,然后一次write()写完,这样代码反而更清晰:
lines = ['第一行', '第二行', '第三行'] with open('output.txt', 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) f.write('\n') # 补上最后一个换行符,很多工具会要求文件以换行结尾4.2 flush()、缓冲机制与 close() 之间的微妙关系
有一类问题特别诡异:代码运行完了,程序也没报错,但打开文件一看,内容是空的或者不全。很多人以为是写入失败了,其实是因为缓冲。
Python 的文件写入并不是你写一个字符就立刻往磁盘上落一个字符,而是先写进内存缓冲区,攒到一定量再一次性刷到磁盘。这样做是为了性能,因为磁盘 I/O 远比内存操作慢,频繁刷盘会让程序慢得没法看。
那内容到底什么时候真正落到磁盘?三种时机:缓冲区满了自动刷、调用flush()主动刷、调用close()关闭文件时自动刷。所以如果你只调用了write()却没有关闭文件,或者程序中途异常退出,缓冲区里的内容可能就丢了。
当你需要"写入后立刻被别人读到"时,比如写日志后马上要 tail 查看,可以主动调一下flush():
with open('app.log', 'a', encoding='utf-8') as f: f.write('2025-06-01 10:00:00 用户登录成功\n') f.flush() # 强制写盘,确保日志立刻可见4.3 追加与覆盖的选择逻辑:日志用 a,配置更新用 w
写文件之前先问自己一个问题:这次写入,是想从零开始覆盖旧内容,还是保留旧内容继续追加?选错模式轻则数据覆盖,重则文件被清空。
日志、流水、埋点记录这类"只增不减"的场景,用追加模式a。好处是简单安全,每次打开文件指针自动定位到末尾,不会动已经写好的内容。注意a模式下seek()是动不了指针的,因为操作系统层面保证你只能从末尾追加。
配置文件的更新、程序每轮运行重新生成结果这一类场景,用w。它会自动清空旧内容,保证文件里只有本次运行的最新数据。但前面强调过,w的清空发生在打开那一刻,所以如果你还要读原文件内容做参考,就不要用w直接打开,先r读完,再w写入。
还有一个容易忽略的细节:当你用r+或w+读写同一文件时,读和写共用同一个指针。写入之后文件指针停在写入结束的位置,紧接着read()读出来的不是文件开头的内容,而是从指针位置继续往后读。这个特性坑过不少人,简单说就是"读写混合时,注意指针位置,必要时显式seek(0)回到开头"。
5. 路径处理与编码问题:两个最影响实战的隐性坑
5.1 路径拼接:告别硬编码字符串,用 pathlib 规范操作
文件读写逃不开路径。新手最直观的做法是写死一个完整路径:open('C:/Users/xxx/Desktop/data/note.txt')。这在当前机器上没问题,但一旦代码放到别的电脑、别的操作系统上,路径分隔符不一样,目录结构不一样,立刻崩给你看。
稍微进阶一点的人知道用os.path.join()来拼接路径,这样能适应不同操作系统的分隔符差异。但今天我更推荐直接上pathlib,它在语义化和跨平台方面做得更好。
from pathlib import Path # 不用纠结是 / 还是 \,直接用正斜杠,pathlib 会处理好 data_dir = Path("data") file_path = data_dir / "raw" / "users.txt" # 判断文件是否存在 if not file_path.exists(): print("文件不存在") # 直接打开,pathlib 对象可以直接传给 open with open(file_path, 'r', encoding='utf-8') as f: content = f.read()pathlib里还有一个我常用的技巧:Path.glob()可以批量匹配目录下符合条件的文件,配合文件读写能实现很多自动化需求。比如把某个目录下所有.txt文件统一改编码或合并内容,几条循环就搞定。
5.2 编码乱码排查链路:从 UnicodeDecodeError 到修复的完整思路
编码问题是真的烦,但排查思路其实是固定的。遇到UnicodeDecodeError或者读出来是乱码时,我一般按这个顺序排查:
- 先用文本编辑器(比如 VS Code、Notepad++)打开文件看一眼右下角的编码提示,确认原文件的编码格式。
- 确认后,在
open()里显式传入对应的encoding。 - 如果文件是 UTF-8 但带着 BOM(UTF-8-BOM),
utf-8可能读不出或首字符带\ufeff,可以改用utf-8-sig。 - 如果实在判断不出来,用
chardet库自动检测编码,只做参考,别盲信。 - 注意"乱码"和"打不开"是两回事。能打开但内容是乱码,大概率是读取时用了错误的编码;打不开抛
UnicodeDecodeError,通常是文件内容里混入了当前编码无法解码的字节。
还有一种特殊情况:同一个文件里混着多种编码的内容。这种情况基本无解,我建议让数据源头统一格式,或者写入时就一并用 utf-8 规范化。在项目的开始阶段就约定"所有文本文件统一 UTF-8",能省掉后面无数个加班的晚上。
6. 进阶实战:csv 模块与 pandas 读写 Excel 的选型
6.1 csv 模块:轻量级表格数据的标准方案
表格数据是文件读写里最常碰到的类型。最基本的 CSV 读写不需要装任何第三方库,Python 自带的csv模块就够用。
import csv # 读取 CSV with open('users.csv', 'r', encoding='utf-8-sig', newline='') as f: reader = csv.reader(f) for row in reader: print(row) # 每行是一个列表注意这里的newline=''参数,很多教程里没提。在 Windows 上如果缺了它,写入 CSV 时每行后面会多出一个空行,因为 csv 模块自己管理换行,而默认的换行转换机制又插了一脚,两边叠加就多出空行了。我当年写 CSV 时遇到这个诡异现象,排查了半天才发现是 newline 参数的问题。
如果需要按列名访问数据,csv.DictReader和csv.DictWriter更方便,它们把每一行映射成字典,列名可以是文件头部那一行。
with open('users.csv', 'r', encoding='utf-8-sig', newline='') as f: reader = csv.DictReader(f) for row in reader: print(row['name'], row['age'])6.2 pandas 读写 Excel:办公自动化里的高频动作
CSV 毕竟存不了复杂格式,很多人实际工作里更多是在处理 Excel 文件。"pandas 读写 excel 文件"能成为热搜词,不是没原因的——这几乎是办公自动化的核心技能之一。
pandas 读 Excel 只需要一行代码,前提是装好了pandas和openpyxl(或者xlrd):
import pandas as pd # 读取 Excel,指定 sheet 名 df = pd.read_excel('销售数据.xlsx', sheet_name='Sheet1') print(df.head()) # 处理完后写回新的 Excel 文件 df.to_excel('销售数据_处理后.xlsx', index=False, sheet_name='Sheet1')关于to_excel,我提醒三个细节。第一,index=False默认不要,否则 pandas 会把行号写进 Excel 第一列,别人打开你的文件会莫名其妙多一列。第二,多个 DataFrame 可以写到同一个 Excel 的不同 sheet,用pd.ExcelWriter的上下文管理器。第三,pandas 写 Excel 依赖 openpyxl,记得提前装好,报ModuleNotFoundError是因为缺依赖而不是代码写错了。
6.3 表格数据读写的选型建议:什么时候 csv、什么时候 pandas
很多新手在选择工具时会纠结。我给一个务实的判断标准:
- 数据量不大,格式简单,或者需要兼容老系统,用
csv模块。它轻量、无依赖、加载快,对简单的表格数据完全够用。 - 需要做数据分析、数据清洗、按列筛选计算,或者要操作多 sheet 的 Excel 文件,直接上 pandas。
- 只需要把大量数据快速写出去、追求性能,可以考虑
csv模块手写或者pandas,两者性能差异在数据量极大时才明显。 - 要保留 Excel 的格式、公式、图表,pandas 做不到,考虑
openpyxl直接操作单元格样式。
一句话总结:读写只是手段,以后要对数据做的事才决定选什么工具。
7. 二进制模式与我的踩坑清单
7.1 二进制模式:处理图片、序列化对象的正确打开方式
文本模式处理的是字符串,二进制模式处理的是字节。图片、音频、视频、压缩包、pickle 序列化文件,这一类东西都必须用rb或wb模式操作。如果你用r模式去读一张图片,Python 会尝试按文本解码,结果通常是抛UnicodeDecodeError。
二进制读写的代码结构和文本模式几乎一样,只是读写的数据类型从str变成了bytes:
# 复制图片文件 with open('source.jpg', 'rb') as src: with open('copy.jpg', 'wb') as dst: dst.write(src.read())上面这种src.read()一次读完的方式只适合小文件。大文件复制时,我习惯分块读,避免整个文件都进内存:
with open('big_video.mp4', 'rb') as src: with open('big_video_copy.mp4', 'wb') as dst: while chunk := src.read(1024 * 1024): # 每次读 1MB dst.write(chunk)Python 对象要持久化存储,最标准的方案是pickle模块。它能把列表、字典、自定义对象序列化成字节,之后用pickle.load()原样读回来:
import pickle data = {'name': '张三', 'scores': [88, 92, 95]} with open('data.pkl', 'wb') as f: pickle.dump(data, f) with open('data.pkl', 'rb') as f: restored = pickle.load(f) print(restored)7.2 五个经典坑的报错信息与修复方式:逐一复盘
最后这部分是我这些年见过、踩过、帮别人擦过屁股的高频问题,逐条列出来给你当避雷针。
坑一:FileNotFoundError文件不存在
用r模式打开一个不存在的文件就会报这个错。修复方式:先判断文件是否存在,或者用try捕获异常。如果是创建新文件,直接用w或a模式,它们会在文件不存在时自动创建。
坑二:写进去的内容丢了或没更新
大概率是缓冲没刷。如果你用write()之后没close()就强行结束程序、或者程序中途崩了,缓冲区里的数据会丢。修复方式:用with管理自动关闭,或者按需flush()。
坑三:UnicodeDecodeError/ 读出来是乱码
原因就是前面说的编码不匹配。修复方式:确认源文件编码,显式声明encoding。读出乱码优先检查是不是编码声明错了,别急着怀疑代码逻辑。
坑四:w模式把原来的文件清空了
打开模式选错。写之前要读原文件的话,先用r读完,再考虑w。重要数据文件处理前建议备份,或者先用追加模式a测试性写入。
坑五:CSV 写入后每行之间多了空行
Windows 下open()没传newline=''。修复方式极简单:写入 CSV 时加newline='',这个细节我前文强调过,遇到的人都会深深记住。
文件读写看似基础,实际上藏了不少磨人的细节。我个人这些年最大的体会是:与其把各种方法背得滚瓜烂熟,不如养成三个习惯——所有open()都配with,所有文本操作都显式声明encoding,所有大文件都逐行或分块处理。这三个习惯护体,文件读写这块的雷,你至少已经避掉九成了。