文件读写
文件读写让程序可以把数据保存到磁盘,并在下次运行时继续使用。常见场景包括读取配置、处理日志、保存用户数据,以及复制图片等。
一次完整的文件操作通常只有三步:打开文件、读写数据、关闭文件。Python 推荐使用 with 自动关闭文件:
with open("notes.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 使用
open()和pathlib.Path打开文件; - 根据需求选择读取、写入、追加和二进制模式;
- 正确处理文本编码、换行符和大文件;
- 使用文件指针随机访问文件;
- 处理常见文件异常,并避免数据丢失。
文件路径
路径用来说明文件在哪里,可以分为相对路径和绝对路径。
from pathlib import Path
relative_path = Path("data") / "notes.txt"
absolute_path = Path("/Users/alice/data/notes.txt")
- 相对路径从程序的当前工作目录开始查找,如
data/notes.txt; - 绝对路径从文件系统根目录开始,完整指向某个文件;
Path("data") / "notes.txt"会使用当前操作系统正确的路径分隔符,比手动拼接字符串更可靠。
当前工作目录不一定是 Python 文件所在目录,可以这样确认:
from pathlib import Path
print(Path.cwd())
如果文件路径应当相对于当前 Python 文件,而不是启动命令的位置,可以使用 __file__:
from pathlib import Path
base_dir = Path(__file__).resolve().parent
config_path = base_dir / "config" / "settings.txt"
在交互式解释器和某些 Notebook 环境中可能没有 __file__,此时应明确指定基准目录或使用 Path.cwd()。
使用 open() 打开文件
open() 的常用形式如下:
file = open(file_path, mode="r", encoding=None)
它返回一个文件对象。最常用的参数是:
| 参数 | 作用 |
|---|---|
file | 文件路径,可以是字符串或 Path 对象 |
mode | 打开模式,决定读取、写入、追加以及文本或二进制操作 |
encoding | 文本编码,处理文本文件时通常明确写成 "utf-8" |
newline | 控制文本模式下换行符的读取和写入方式 |
buffering | 控制缓冲策略,一般保留默认值即可 |
直接调用 open() 后必须关闭文件:
file = open("notes.txt", "r", encoding="utf-8")
try:
content = file.read()
finally:
file.close()
关闭文件会释放操作系统资源,并把仍在缓冲区中的数据提交出去。手动关闭容易遗漏,所以实际代码应优先使用 with。
使用 with 自动关闭文件
文件对象是上下文管理器。离开 with 代码块时,即使中途发生异常,文件也会被关闭:
with open("notes.txt", "r", encoding="utf-8") as file:
content = file.read()
print(file.closed) # True
文件关闭后不能继续读写,但已经读取到内存中的 content 仍然可以使用。
同时处理多个文件时,可以写在同一个 with 中:
with (
open("source.txt", "r", encoding="utf-8") as source,
open("target.txt", "w", encoding="utf-8") as target,
):
for line in source:
target.write(line)
文件打开模式
模式可以组合使用,例如 "rb" 表示以二进制方式读取。
| 模式 | 含义 | 文件不存在时 | 文件存在时 |
|---|---|---|---|
"r" | 只读,默认模式 | 抛出 FileNotFoundError | 从开头读取 |
"w" | 只写 | 创建文件 | 立即清空原内容 |
"a" | 追加写入 | 创建文件 | 从末尾追加,不清空 |
"x" | 独占创建并写入 | 创建文件 | 抛出 FileExistsError |
"t" | 文本模式,默认值 | — | 读写 str |
"b" | 二进制模式 | — | 读写 bytes |
"+" | 更新模式 | 取决于主模式 | 同时允许读取和写入 |
常见组合包括 "r+"、"w+"、"a+"、"rb" 和 "wb"。注意:
"w+"仍然会在打开时清空文件;"a+"的写入通常仍发生在文件末尾,即使移动过文件指针;"+"不代表追加,它只表示同时可读和可写;- 文本模式不能和二进制模式同时使用。
不确定是否应该覆盖原文件时,不要使用 "w";如果只允许创建新文件,使用 "x" 更安全。
读取文本文件
一次读取全部内容
read() 返回从当前位置到文件末尾的字符串:
with open("notes.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)
也可以限制最多读取多少个字符:
with open("notes.txt", encoding="utf-8") as file:
preview = file.read(100)
read() 适合能够安全放入内存的小文件。对大型日志或数据文件,不要一次全部读取。
一次读取一行
readline() 每次读取一行,通常会保留行尾的换行符;到达文件末尾时返回空字符串 "":
with open("notes.txt", encoding="utf-8") as file:
first_line = file.readline()
second_line = file.readline()
print(first_line.rstrip("\n"))
print(second_line.rstrip("\n"))
只想去掉换行符时,使用 rstrip("\r\n") 或 rstrip("\n")。直接调用 strip() 还会删除行首和行尾的空格,可能改变原始数据。
逐行遍历
文件对象本身可迭代。逐行遍历不会一次把整个文件加载到内存,是处理大文本文件的首选方式:
with open("access.log", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
print(line_number, line.rstrip("\r\n"))
把所有行读成列表
readlines() 返回字符串列表,每个元素通常对应一行:
with open("notes.txt", encoding="utf-8") as file:
lines = file.readlines()
它也会一次占用与文件大小相近的内存。只有确实需要随机访问、排序或多次处理所有行时再使用。
写入文本文件
使用 write()
write() 接收字符串,返回实际写入的字符数,并且不会自动添加换行符:
with open("notes.txt", "w", encoding="utf-8") as file:
count = file.write("第一行\n")
file.write("第二行\n")
print(count) # 4:“第一行”三个字符加一个换行符
写入的值必须是字符串。数字等其他类型需要先转换:
score = 95
with open("score.txt", "w", encoding="utf-8") as file:
file.write(str(score))
也可以使用 print(),它会自动添加换行符,并能方便地格式化多个值:
with open("report.txt", "w", encoding="utf-8") as file:
print("姓名", "成绩", sep=",", file=file)
print("小林", 95, sep=",", file=file)
使用 writelines()
writelines() 可以写入一组字符串,但同样不会自动添加换行符:
lines = ["Python\n", "Java\n", "Go\n"]
with open("languages.txt", "w", encoding="utf-8") as file:
file.writelines(lines)
如果原始字符串没有换行符,可以在写入时补充:
languages = ["Python", "Java", "Go"]
with open("languages.txt", "w", encoding="utf-8") as file:
file.writelines(f"{language}\n" for language in languages)
追加内容
追加模式 "a" 会保留原内容,并把新数据写到末尾:
from datetime import datetime
with open("app.log", "a", encoding="utf-8") as file:
file.write(f"{datetime.now():%Y-%m-%d %H:%M:%S} 程序启动\n")
"a" 不会自动确保原文件以换行符结尾。如果格式要求每条记录独占一行,需要由程序维护换行规则。
文本编码
计算机在磁盘中保存的是字节,文本模式会自动完成两次转换:
- 读取时:字节按照
encoding解码为str; - 写入时:
str按照encoding编码为字节。
推荐明确使用 UTF-8:
with open("message.txt", "w", encoding="utf-8") as file:
file.write("你好,Python!")
如果读取时使用了错误编码,可能抛出 UnicodeDecodeError,也可能得到乱码。应该确认文件的真实编码,而不是盲目忽略错误:
with open("legacy.txt", encoding="gb18030") as file:
content = file.read()
errors 参数可以指定编码错误的处理方式:
with open("unknown.txt", encoding="utf-8", errors="replace") as file:
content = file.read()
errors="strict":默认行为,遇到非法数据就抛出异常;errors="replace":使用替代字符标记无法解码的数据;errors="ignore":直接丢弃错误数据,可能造成无提示的数据损失,应谨慎使用。
换行符
不同系统常用的换行符不同:Unix/Linux 和现代 macOS 通常使用 \n,Windows 通常使用 \r\n。文本模式默认会进行通用换行处理,普通文本通常无需手动适配。
需要精确控制时可以使用 newline:
with open("unix.txt", "w", encoding="utf-8", newline="\n") as file:
file.write("第一行\n第二行\n")
处理 CSV 文件时,通常按 csv 模块的要求使用 newline="",避免出现额外空行:
import csv
with open("scores.csv", "w", encoding="utf-8", newline="") as file:
writer = csv.writer(file)
writer.writerow(["姓名", "成绩"])
writer.writerow(["小林", 95])
读取和写入二进制文件
图片、音频、压缩包等不是文本,应使用 "rb" 或 "wb"。二进制模式读写 bytes,不使用 encoding 和 newline:
with open("photo.jpg", "rb") as source:
data = source.read()
with open("photo-copy.jpg", "wb") as target:
target.write(data)
复制大文件时应分块处理,避免占用过多内存:
chunk_size = 1024 * 1024 # 每次读取 1 MiB
with (
open("video.mp4", "rb") as source,
open("video-copy.mp4", "wb") as target,
):
while chunk := source.read(chunk_size):
target.write(chunk)
实际项目只需要复制文件时,也可以使用标准库 shutil.copyfile(),不必自行实现循环。
文件指针:tell() 和 seek()
文件对象会记录当前读写位置,称为文件指针。每次读写后,指针都会向后移动:
with open("data.bin", "rb") as file:
print(file.tell()) # 0
file.read(4)
print(file.tell()) # 4
seek(offset, whence) 可以移动指针:
whence=0:从文件开头计算,默认值;whence=1:从当前位置计算;whence=2:从文件末尾计算。
with open("data.bin", "rb") as file:
file.seek(10) # 移动到从开头算起的第 10 个字节
block = file.read(4)
file.seek(-4, 2) # 移动到文件末尾前 4 个字节
tail = file.read()
二进制模式按字节定位,最适合随机访问。文本模式存在编码和换行转换,除 seek(0) 等受支持位置外,不应随意按字符数计算偏移量;需要返回之前的位置时,应保存 tell() 的结果再传给 seek()。
缓冲、flush() 和落盘
文件写入通常先进入缓冲区,再由 Python 和操作系统批量处理,以提高性能。关闭文件时会自动刷新 Python 缓冲区。需要在文件保持打开时让其他程序尽快看到数据,可以调用:
with open("progress.log", "a", encoding="utf-8") as file:
file.write("任务开始\n")
file.flush()
flush() 只把 Python 的缓冲数据交给操作系统,不保证数据已经物理写入磁盘。对事务日志等有严格持久化要求的场景,还需要结合 os.fsync(),并评估性能与文件系统行为。普通学习和业务代码通常不需要手动处理。
使用 pathlib 简化常见操作
Path.read_text() 和 Path.write_text() 适合一次性读写小文本文件,它们会自动打开并关闭文件:
from pathlib import Path
path = Path("notes.txt")
path.write_text("学习 Python 文件读写\n", encoding="utf-8")
content = path.read_text(encoding="utf-8")
二进制文件可以使用 read_bytes() 和 write_bytes():
from pathlib import Path
source = Path("icon.png")
Path("icon-copy.png").write_bytes(source.read_bytes())
这些方法会一次处理全部内容,不适合大文件。需要逐行或分块时,使用 Path.open():
from pathlib import Path
path = Path("access.log")
with path.open(encoding="utf-8") as file:
for line in file:
print(line.rstrip("\r\n"))
创建父目录可以使用:
from pathlib import Path
output_dir = Path("output") / "reports"
output_dir.mkdir(parents=True, exist_ok=True)
open() 可以创建文件,但不会自动创建缺失的父目录。
异常处理
文件操作常见异常包括:
| 异常 | 常见原因 |
|---|---|
FileNotFoundError | 读取的文件或父目录不存在 |
FileExistsError | 使用 "x" 创建已存在的文件 |
PermissionError | 当前用户没有访问权限 |
IsADirectoryError | 把目录当作文件打开 |
NotADirectoryError | 路径中本应是目录的部分实际不是目录 |
UnicodeDecodeError | 文本内容与指定编码不匹配 |
OSError | 磁盘空间不足、设备错误等其他操作系统问题 |
只捕获能够处理的具体异常:
from pathlib import Path
def load_notes(path: Path) -> str | None:
try:
return path.read_text(encoding="utf-8")
except FileNotFoundError:
print(f"文件不存在:{path}")
except PermissionError:
print(f"没有读取权限:{path}")
except UnicodeDecodeError:
print(f"文件不是有效的 UTF-8 文本:{path}")
return None
不要先用 exists() 检查再假定文件一定可用:文件可能在检查后被删除或替换。真正打开文件时仍应处理异常。exists() 更适合界面提示,而不能替代异常处理。
安全地更新文件
用 "w" 打开原文件后,如果程序在写到一半时失败,原内容可能已经丢失。重要文件可以先写入同一目录下的临时文件,成功后再用 replace() 替换:
from pathlib import Path
from tempfile import NamedTemporaryFile
def save_text(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with NamedTemporaryFile(
mode="w",
encoding="utf-8",
dir=path.parent,
delete=False,
) as temporary_file:
temporary_file.write(content)
temporary_path = Path(temporary_file.name)
temporary_path.replace(path)
把临时文件放在目标文件的同一目录,通常可以避免跨文件系统移动,并让替换操作具有更可靠的原子性。不过,权限保留、异常后的临时文件清理、断电持久性和多进程并发写入仍需根据实际系统单独设计。
常见错误与最佳实践
用 "w" 误删原内容
"w" 在打开文件时就会截断原内容。需要保留内容时使用 "a",需要拒绝覆盖时使用 "x",重要更新则先写临时文件再替换。
忘记指定编码
文本模式默认编码由运行环境决定,同一份代码在不同系统上可能表现不同。可控的文本文件应明确使用 encoding="utf-8"。
把大文件一次读入内存
对大文本逐行遍历,对大二进制文件分块读取。只有文件大小明确且可控时才使用 read()、readlines()、read_text() 或 read_bytes() 一次加载全部内容。
混淆字符串和字节
文本模式读写 str,二进制模式读写 bytes。二者需要显式转换:
text = "你好"
data = text.encode("utf-8")
restored = data.decode("utf-8")
依赖不明确的当前工作目录
相对路径从当前工作目录解析,而不是自动相对于源码文件。对固定资源,应通过明确的基准目录构造路径;对用户提供的路径,应清楚说明它相对于哪里。
捕获所有异常后静默忽略
except Exception: pass 会隐藏权限错误、磁盘错误和程序缺陷。只捕获能恢复的异常,并提供清晰提示或记录日志;不能处理的异常应继续向上抛出。
综合示例:筛选日志中的错误行
下面的程序逐行读取日志,只把包含 ERROR 的记录写入新文件。它不会一次加载整个文件,并会创建缺失的输出目录:
from pathlib import Path
def export_errors(source_path: Path, target_path: Path) -> int:
target_path.parent.mkdir(parents=True, exist_ok=True)
matched_count = 0
with (
source_path.open("r", encoding="utf-8") as source,
target_path.open("w", encoding="utf-8") as target,
):
for line_number, line in enumerate(source, start=1):
if "ERROR" in line:
target.write(f"{line_number}: {line}")
matched_count += 1
return matched_count
try:
count = export_errors(
Path("logs/app.log"),
Path("output/errors.log"),
)
except FileNotFoundError as error:
print(f"找不到输入文件:{error.filename}")
except PermissionError as error:
print(f"没有文件访问权限:{error.filename}")
except UnicodeDecodeError:
print("输入文件不是有效的 UTF-8 文本")
else:
print(f"已导出 {count} 条错误记录")
这个示例体现了文件处理的核心原则:使用 Path 构造路径、用 with 管理资源、明确编码、流式处理数据,并只捕获能够向用户解释的异常。
小结
- 使用
with open(...)或Path.open(),让文件始终能够正确关闭; "r"用于读取,"w"会清空后写入,"a"用于追加,"x"用于安全创建;- 文本文件读写
str并应明确编码,二进制文件读写bytes; - 小文件可以整体读取,大文件应逐行或分块处理;
write()和writelines()不会自动添加换行符;- 相对路径取决于当前工作目录,跨平台路径优先使用
pathlib; - 重要文件先写临时文件再替换,并针对可恢复的文件错误进行异常处理。