大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
文件读写
这一课你将能做到
- 用
with open(...)正确读写文本文件,说清为什么 with 能自动关闭文件 - 用 pathlib 处理路径:拼接、判断存在、创建目录、批量列举文件
- 读懂并写出「逐行处理大文件」和「读写 JSON」这两个高频场景的代码
为什么要学
前面几课的数据都活在内存里,程序一关全没了。文件读写是脚本「记住东西」的第一种方式:批量处理日志、导出报表、保存配置、持久化爬虫结果,全靠它。而且「文件 + 循环 + 字符串」三件套正是 Python 脚本最经典的形态——第 12 课的综合练习直接用这一课的产出。
人话:open 就是找图书管理员借书(打开文件),read 是翻阅,write 是往里写,close 是还书。with 语句就是「自动还书提醒」——不管中途发生什么,出了作用域管理员一定把书收回去。
先建立图像
open() 的三种基本模式:
r → read 读(文件必须存在,光标在开头)
w → write 写(清空原内容!不存在则新建)
a → append 追加(保留原内容,写在末尾)
加 b 是二进制模式:rb / wb(图片、视频用)
文本模式强烈建议显式指定 encoding="utf-8"
with 语句结构:
with open("demo.txt", "w", encoding="utf-8") as f:
f.write("内容") # with 块内:文件处于打开状态
# with 块外:文件必定已自动关闭
跟着做
步骤 1:写入第一个文件
新建 files_demo.py,在脚本所在目录运行:
with open("demo.txt", "w", encoding="utf-8") as f:
f.write("第一行\n")
f.write("第二行\n")
f.writelines(["第三行\n", "第四行\n"]) # 接收字符串列表
print("写入完成")
运行后打开脚本目录里的 demo.txt,应有 4 行文本。注意 w 模式每次运行都会清空重写,想保留旧内容追加用 a 模式:
with open("demo.txt", "a", encoding="utf-8") as f:
f.write("追加的第五行\n")
输出(终端):
写入完成
步骤 2:读文件的三种方式
# 方式一:一次性读全部(小文件)
with open("demo.txt", "r", encoding="utf-8") as f:
content = f.read()
print(repr(content[:12])) # '第一行\n第二行\n第三' —— \n 也在里面
# 方式二:按行读成列表
with open("demo.txt", "r", encoding="utf-8") as f:
lines = f.readlines() # 每行末尾带着 \n
print(len(lines)) # 5
# 方式三:逐行遍历(推荐,内存友好,文件多大都不怕)
with open("demo.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip()) # strip 去掉行尾换行符
输出:
'第一行\n第二行\n第三'
5
第一行
第二行
第三行
第四行
追加的第五行
步骤 3:with 的意义——亲眼验证自动关闭
f = open("demo.txt", "r", encoding="utf-8")
print(f.closed) # False:文件开着
f.close()
print(f.closed) # True
# 手动 open 的问题:中间一旦抛异常,close 就执行不到了
# with 则保证「无论是否异常」都关闭:
with open("demo.txt", "r", encoding="utf-8") as f:
print(f.closed) # False
print(f.closed) # True —— 出了 with 块自动关了
输出:
False
True
False
True
规则:文件操作永远用 with,不裸写 open/close。
步骤 4:pathlib 现代路径操作
from pathlib import Path
p = Path("data") # 当前目录下的 data
print(p / "raw" / "2026.csv") # 用 / 拼路径,跨平台自动处理分隔符
# data/raw/2026.csv (Windows 下显示 data\raw\2026.csv)
print(Path.cwd()) # 当前工作目录
print(Path("demo.txt").exists()) # True(上一节刚建的)
print(Path("demo.txt").is_file()) # True
print(Path("demo.txt").suffix) # .txt 后缀
# 递归创建目录( parents=True 连父目录一起建)
Path("data/raw/logs").mkdir(parents=True, exist_ok=True) # 已存在不报错
# 列出目录内容
for f in Path(".").glob("*.py"): # 当前目录所有 .py
print(f.name)
# pathlib 也能直接读写(内部仍是 open,小文件场景简洁)
p2 = Path("quick.txt")
p2.write_text("pathlib 快捷写入", encoding="utf-8")
print(p2.read_text(encoding="utf-8"))
输出:
data/raw/2026.csv (Windows: data\raw\2026.csv)
<当前工作目录绝对路径>
True
True
.txt
files_demo.py
pathlib 快捷写入
pathlib 是 3.4+ 的官方推荐,替代老的 os.path 拼字符串写法——p / "sub" / "file.txt" 一行顶三行。
步骤 5:实战——逐行处理 + 统计
把第 7 课的字符串能力串起来:统计一个文本文件的行数和最长单词行。
from pathlib import Path
# 先准备一份样例文件
Path("notes.txt").write_text(
"python is powerful\n"
"files make scripts useful\n"
"practice every day\n",
encoding="utf-8",
)
with open("notes.txt", "r", encoding="utf-8") as f:
total_lines = 0
longest = ""
for line in f:
total_lines += 1
line = line.strip()
if len(line) > len(longest):
longest = line
print(f"共 {total_lines} 行")
print(f"最长的一行:{longest}")
输出:
共 3 行
最长的一行:files make scripts useful
步骤 6:JSON 读写——结构化数据的默认格式
import json
from pathlib import Path
users = [
{"name": "小明", "age": 18, "tags": ["python", "java"]},
{"name": "小红", "age": 20, "tags": ["go"]},
]
# 写:dump 到文件,ensure_ascii=False 让中文正常显示
with open("users.json", "w", encoding="utf-8") as f:
json.dump(users, f, ensure_ascii=False, indent=2)
# 读:从文件 load 回来,类型自动还原(列表/字典/数字)
with open("users.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(type(loaded)) # <class 'list'>
print(loaded[0]["tags"][0]) # python
print(loaded[1]["name"]) # 小红
# 字符串 ↔ JSON 的转换(API 场景常用)
s = json.dumps(users[0], ensure_ascii=False)
print(s) # {"name": "小明", "age": 18, ...}
back = json.loads(s)
print(back["age"]) # 18
输出:
<class 'list'>
python
小红
{"name": "小明", "age": 18, "tags": ["python", "java"]}
18
json 是标准库,不用装。json.load 读回来就是 Python 的 dict/list,和第 6 课练的嵌套结构完全对上。
讲清楚要点
- 永远 with open:with 依赖文件对象的上下文管理器协议,异常时也会关闭文件。裸 open 一旦异常就泄漏文件句柄,Windows 上还会导致文件被占用删不掉。
- encoding="utf-8" 必须显式写:不写时 Python 用系统默认编码——Windows 上常是 GBK,跨机器运行就 UnicodeDecodeError。UTF-8 是事实标准,永远显式。
- w 会清空文件:
w模式打开的瞬间原内容就没了,不是等 write 才清。只想加内容用a;读文件写r可以省略(默认)。 - 大文件用逐行遍历:
for line in f是惰性的,内存里永远只有一行;read()和readlines()会把全部内容塞进内存,处理几百 MB 日志时会卡爆。 - write 不自动换行:和 print 不同,
f.write("abc")不会补\n,多行内容要自己写换行符。 - pathlib 的
/是路径拼接运算符:Path("a") / "b" / "c.txt",自动处理/和\的平台差异。判断存在用 exists(),建目录用 mkdir(parents=True, exist_ok=True)。 - JSON 只支持基础类型:dict/list/str/int/float/bool/None 能转,tuple 会变 list,datetime 等对象要先转字符串(
isoformat())再存。
你可能会懵的地方
-
为什么我运行脚本时找不到刚才写的文件? 相对路径基于「运行命令时所在的目录」,不是脚本文件所在目录。在
python-lab目录外执行python lab/files_demo.py,文件就写到了python-lab外面。解决:cd到脚本目录再跑,或用Path(__file__).parent定位脚本目录。 -
read() 和 readlines() 选哪个? 小文件(几百 KB 内)随手用都行;日志、CSV 这类可能很大的文件一律逐行遍历
for line in f。原则:能惰性就不贪多。 -
文件不存在时报 FileNotFoundError 怎么优雅处理? 读之前
Path(p).exists()判断;或用 try/except 捕获 FileNotFoundError(第 11 课)。写模式下w/a会自动创建文件,不存在不用慌。 -
二进制文件(图片、PDF)怎么处理? 模式加 b:
with open("img.png", "rb") as f: data = f.read(),读到的是 bytes 不是 str。处理图片本身一般交给 Pillow 等库,脚本里常只做「复制、移动、上传」这类搬运。 -
追加写入后文件里全挤在一行?
a模式从文件末尾续写,如果原文件最后一行没换行符,新内容会接在同一行。追加前先确认结尾:内容前手动补\n或写入时每条都带。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
f = open(...) 手动 close | 异常时 close 不会执行 | with open(...) as f: |
| open 不写 encoding | Windows 默认 GBK,跨机乱码 | 显式 encoding="utf-8" |
用 w 模式「接着写」 | 每次打开清空原内容 | 追加用 a 模式 |
| 大文件用 read() 一次读完 | 内存爆炸 | for line in f 逐行处理 |
f.write("abc") 后想换行 | write 不自动补 \n | 写 "abc\n" |
字符串 + 拼 Windows 路径 | 分隔符平台差异、易错 | Path(a) / b / "c.txt" |
| json.dump 忘了 ensure_ascii=False | JSON 文件里中文变 \uXXXX | 加 ensure_ascii=False |
卡点急救
- UnicodeDecodeError:文件实际编码和指定编码不符。先确认来源编码(Excel 导出常是 GBK),读取时对应传
encoding="gbk";自己的文件统一存 UTF-8。 - PermissionError:文件被别的程序占用(Excel 开着 CSV)、或目标目录只读。关掉占用程序;确认写入路径有权限。
- 找不到写入的文件:相对路径相对「运行目录」而非「脚本目录」。用绝对路径或
Path(__file__).parent / "demo.txt"。 - 读出来每行末尾多了 \n:正常,文件本身含换行符。
line.strip()或line.rstrip("\n")处理。 - JSONDecodeError: Expecting value:JSON 文件格式坏(手工编辑弄坏的、或读的是空文件)。先
print(Path(p).read_text(encoding="utf-8"))目检内容。 - 写入后读不到最新内容:写文件的对象还没关闭(缓冲未刷盘)。确认 with 块已退出,或读之前 f.flush()。
面试常问
-
with 语句的原理是什么? 上下文管理器协议:对象实现
__enter__(进入时调用,返回值绑定给 as 变量)与__exit__(离开时调用,无论是否异常都会执行)。文件对象在__exit__里关闭自己。等价于 try/finally,但语义清晰。自定义类也可实现这两个方法支持 with。 -
如何读取一个 10GB 的日志文件? 不能 read() 全量载入。逐行遍历(
for line in f,惰性加载);或readline()控制;或按块读二进制(f.read(chunk_size))配合迭代。关键是内存中只保留一小部分。 -
文本模式和二进制模式的区别? 文本模式做两件额外的事:解码(bytes→str,按 encoding)与换行符转换(
\r\n↔\n)。二进制模式原样读写字节,不解码不转换。图片/网络数据用二进制,人类可读文本用文本模式并显式 encoding。 -
r、w、a、x 模式的区别? r 只读(不存在报错);w 只写(清空或新建);a 追加(不存在新建);x 新建写(已存在报 FileExistsError,防覆盖)。加
+可读写(如 r+)。另有 b 后缀进二进制。 -
pathlib 相比 os.path 的优势? 面向对象:路径是对象而非字符串,
/运算符拼接跨平台正确;方法内聚(exists、glob、mkdir、read_text 直接在对象上);与 f-string 配合表达清晰。新代码官方推荐 pathlib。
自测三问
w和a模式打开同一个已存在文件,行为差在哪?- 为什么处理大文件必须用
for line in f而不是f.read()?写出逐行统计行数的代码。 Path("a") / "b" / "c.txt"与os.path.join相比好在哪?mkdir 防报错的两个参数是什么?
小结一句话
文件操作三板斧:永远 with open、永远 encoding="utf-8"、大文件逐行读;再配上 pathlib 的 / 拼路径和 json 标准库,脚本就能真正把数据留在磁盘上了。
延伸
- 官方教程 — 读写文件
- pathlib 官方文档
- 下一课预告:模块与 pip——import 的机制、
__name__ == "__main__"的真相、虚拟环境与依赖管理。