文件读写看着简单,坑都藏在编码、换行和路径分隔符里;这篇把它们一次讲透,并告诉你为什么
pathlib应当取代os.path。
你将学到
open()的encoding与newline到底在控制什么,以及为什么必须显式指定- 读大文件的正确姿势:逐行迭代 vs 分块读取
os.path和pathlib.Path的对比,以及为什么新代码应当用后者- 路径拼接、遍历、
glob/rglob通配,文件和目录的增删改查 tempfile临时文件,文本与二进制的区别,以及 JSON 文件的读写with上下文管理器在文件 I/O 里的再次应用
前置知识
需要理解 with 语句和上下文管理器,见 上一篇:代码质量与工具链 之前的 04-上下文管理器与with。
打开文件的三个参数
open(file, mode, encoding, newline) 里,最容易被忽略的是 encoding 和 newline。
# ❌ 不指定 encoding:用了操作系统的默认编码
# Windows 默认 GBK,Linux/macOS 默认 UTF-8,同一份代码换个机器就乱码
with open("data.txt", "r") as f:
text = f.read()
# ✅ 永远显式指定 encoding="utf-8"
with open("data.txt", "r", encoding="utf-8") as f:
text = f.read()
encoding="utf-8" 还有个小技巧:用 "utf-8-sig" 可以自动吃掉 Windows 记事本写文件时加的 BOM(\ufeff),处理别人给的 CSV 时很实用。
newline 控制换行符的翻译。默认 newline=None 时,读的时候会把 \r\n、\r、\n 统一翻译成 \n;写的时候把 \n 翻译成当前平台的换行符。这大概率是你想要的。但有两个例外:
# 处理 csv 时希望保留原始换行,交给 csv 模块自己处理
with open("data.csv", "r", encoding="utf-8", newline="") as f:
...
# 写文本时固定用 \n(比如生成给 Linux 用的脚本),别被平台翻译
with open("run.sh", "w", encoding="utf-8", newline="\n") as f:
f.write("#!/bin/bash\necho hi\n")
模式字符串速记:r 读、w 写(清空)、a 追加、x 独占创建(已存在就报错)、b 二进制、+ 读写。文本模式别和 b 混用,open(..., "rb") 时不能传 encoding。
读大文件:别 readlines
一个 5GB 的日志文件,f.readlines() 会把它整个塞进内存——直接 OOM。
# ❌ 一次性全读进内存
with open("huge.log", encoding="utf-8") as f:
for line in f.readlines(): # readlines 先把所有行加载成 list
process(line)
# ✅ 文件对象本身就可迭代,逐行流式读取,内存占用恒定
with open("huge.log", encoding="utf-8") as f:
for line in f:
process(line)
如果一行本身就超长(比如没有换行的单行 JSON),逐行也不好使,得按块读:
# ✅ 按固定大小分块读,适合二进制或超长行
def read_in_chunks(path, size=8192):
with open(path, "rb") as f:
while chunk := f.read(size): # 海象运算符,读到空 bytes 就结束
yield chunk
for chunk in read_in_chunks("big.bin"):
# 这里 chunk 是 bytes,交给解析逻辑
consume(chunk)
os.path vs pathlib.Path
os.path 是一堆操作字符串的函数,pathlib 是把路径当成对象。后者可读性、可组合性全面胜出,Python 3.6+ 就应该用它。
import os
from pathlib import Path
# os.path 风格:函数套函数,返回的都是 str
base = "/tmp/data"
p = os.path.join(base, "2026", "report.csv")
parent = os.path.dirname(p)
name = os.path.basename(p)
ext = os.path.splitext(name)[1]
# pathlib 风格:用 / 拼路径,属性直接取
p = Path("/tmp/data") / "2026" / "report.csv" # 输出: /tmp/data/2026/report.csv
p.parent # 输出: /tmp/data/2026
p.name # 输出: report.csv
p.stem # 输出: report
p.suffix # 输出: .csv
p.exists() # 输出: False(还没创建)
/ 运算符会让很多从别的语言转过来的人愣一下,但它真的比 os.path.join 舒服。Path 还能直接读写文件:
p = Path("notes.txt")
p.write_text("hello\n", encoding="utf-8") # 等价于 open 写
text = p.read_text(encoding="utf-8") # 等价于 open 读
跨平台是 pathlib 的另一大优势:Windows 上 Path("a") / "b" 会得到 a\b,在 Linux 上得到 a/b,你不用手写 os.sep。
遍历与通配
glob 用 *(单层)和 **(递归任意层)匹配路径,比手写 os.walk 舒服得多。
from pathlib import Path
root = Path("project")
# 当前目录下所有 .py 文件
for p in root.glob("*.py"):
print(p)
# 递归所有层级的 .py(rglob 等价于 glob("**/*.py"))
for p in root.rglob("*.py"):
print(p)
# 多条件:遍历所有图片
for p in root.rglob("*"):
if p.suffix.lower() in {".jpg", ".png", ".gif"}:
print(p)
需要完整目录树遍历时用 os.walk,或者 Path.rglob("*"):
import os
for dirpath, dirnames, filenames in os.walk("project"):
# dirnames 可以原地修改,用来剪枝(比如跳过 .git)
dirnames[:] = [d for d in dirnames if d != ".git"]
for name in filenames:
print(os.path.join(dirpath, name))
增删改查
from pathlib import Path
import shutil
p = Path("out/report")
p.mkdir(parents=True, exist_ok=True) # 递归建目录,已存在也不报错
p.is_dir() # 输出: True
p.is_file() # 输出: False
f = p / "a.txt"
f.touch(exist_ok=True) # 创建空文件
# 改名 / 移动
f.rename(p / "b.txt")
# 复制(含元数据)
shutil.copy2(p / "b.txt", p / "c.txt")
# 复制整棵目录树
shutil.copytree("out/report", "out/backup", dirs_exist_ok=True)
# 删除文件 / 空目录 / 整棵树
(p / "c.txt").unlink(missing_ok=True)
shutil.rmtree("out/backup", ignore_errors=True)
Path 上还有 iterdir() 列目录、resolve() 转绝对路径、stat() 拿文件信息。resolve() 在处理 .. 和符号链接时特别有用。
临时文件 tempfile
写测试、做中间缓存时,别用 "tmp.txt" 这种会污染工作目录的名字,用 tempfile。
import tempfile
from pathlib import Path
# 一次性临时文件:关闭即自动删除
with tempfile.NamedTemporaryFile("w", suffix=".txt", encoding="utf-8",
delete=True) as tf:
tf.write("临时内容")
tf.flush() # 让其他进程能读到
print(Path(tf.name).read_text(encoding="utf-8")) # 输出: 临时内容
# with 块结束后文件已被删除
# 临时目录
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "x.txt"
p.write_text("hi", encoding="utf-8")
# 出了 with 块,整个目录连同内容一起删除
文本 vs 二进制
文本模式(默认)会做编码解码和换行翻译,返回 str;二进制模式("rb" / "wb")返回 bytes,不做任何翻译。
# 文本:读回来是 str
with open("t.txt", "w", encoding="utf-8") as f:
f.write("你好")
# 二进制:读回来是 bytes,适合图片、压缩包、序列化数据
with open("t.png", "rb") as f:
header = f.read(8)
print(header[:4]) # 输出: b'\x89PNG'
判断一个文件是不是文本靠猜,但可以用 errors 参数兜底:open(p, encoding="utf-8", errors="replace") 会把非法字节替换成 �,而不是抛 UnicodeDecodeError。
JSON 文件读写
json 模块只管 str 和对象之间的转换,落盘仍然要配合 open。
import json
from pathlib import Path
data = {"name": "Alice", "score": 95, "tags": ["a", "b"]}
# 写:ensure_ascii=False 才能正常显示中文
Path("u.json").write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
# 更省事的写法:dump 直接写文件对象
with open("u.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# 读:load 直接从文件对象解析
with open("u.json", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded["tags"]) # 输出: ['a', 'b']
ensure_ascii=True(默认)会把中文转成 \uXXXX,看着丑,但纯 ASCII 兼容性最好。给人类看就 False。
with 的再次应用
open 返回的文件对象本身就是上下文管理器,with 保证异常时也能关闭。多个文件可以一行搞定:
# ✅ 同时打开输入输出,任何一边出错都会正确关闭
with open("in.txt", encoding="utf-8") as fin, \
open("out.txt", "w", encoding="utf-8") as fout:
for line in fin:
fout.write(line.rstrip() + "\n")
Python 3.10+ 还能用括号分组,可读性更好:
# Python 3.10+
with (
open("in.txt", encoding="utf-8") as fin,
open("out.txt", "w", encoding="utf-8") as fout,
):
fout.writelines(fin)
常见坑
# ❌ 不指定 encoding,靠平台默认值
open("f.txt") # 换个系统就乱码
# ✅ 显式 utf-8
open("f.txt", encoding="utf-8") # 到哪都一样
# ❌ 手动拼字符串拼路径,Windows 上分隔符错
path = "data" + "\\" + "a.csv" # 只在 Windows 对
# ✅ 用 pathlib 的 /
from pathlib import Path
path = Path("data") / "a.csv" # 哪都对
# ❌ 读大文件用 readlines / read(),内存爆炸
lines = open("huge.log").readlines()
# ✅ 逐行迭代,或用 read_in_chunks 分块
for line in open("huge.log", encoding="utf-8"):
...
# ❌ 忘了关闭文件句柄,靠 GC 不稳定
f = open("f.txt", "w")
f.write("x")
# ✅ 用 with,作用域结束自动关闭
with open("f.txt", "w", encoding="utf-8") as f:
f.write("x")
小结
- 永远显式写
encoding="utf-8";需要保留原始换行或处理 CSV 时加newline=""。 - 大文件不要
readlines();文件对象本身可迭代,超长行用read(size)分块。 - 新代码一律用
pathlib.Path;/拼路径、.parent/.suffix取片段、glob/rglob做匹配。 - 建目录用
mkdir(parents=True, exist_ok=True),删树用shutil.rmtree(..., ignore_errors=True)。 - 临时文件用
tempfile,别污染工作目录;文本模式返回str,二进制模式返回bytes,两者别混。 - JSON 落盘:
ensure_ascii=False保留中文,indent=2方便人看。 with是文件 I/O 的标准姿势,多文件可用括号分组一次打开。
延伸阅读
- 官方文档:
pathlib、os.path、tempfile、shutil - 下一篇会讲 正则表达式,处理文本时和文件读写是绝配
上一篇:代码质量与工具链 · 下一篇:正则表达式
文章回复
0 条公开回复