跳到主要内容

文件读写

文件读写让程序可以把数据保存到磁盘,并在下次运行时继续使用。常见场景包括读取配置、处理日志、保存用户数据,以及复制图片等。

一次完整的文件操作通常只有三步:打开文件、读写数据、关闭文件。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;
  • 重要文件先写临时文件再替换,并针对可恢复的文件错误进行异常处理。