27. 文件的读取与写入
到目前为止,我们程序中的所有数据都是临时的——存储在变量中,程序结束时就会消失。要创建能够在多次运行之间记住信息的程序,我们就需要处理文件(files)。文件让我们可以把数据保存到磁盘上,并在之后读回来,从配置设置到用户数据存储都离不开它。
在本章中,你将学习如何在 Python 中读取和写入文本文件。我们会先理解文件路径以及 Python 如何定位文件,然后依次介绍打开、读取、写入以及正确关闭文件。你还会学习不同的文件模式(text vs binary)、文本编码(encoding),以及如何处理文件操作过程中常见的错误。
27.1) 文件路径与当前工作目录
在我们开始处理文件之前,需要先理解 Python 如何在你电脑的文件系统中定位它们。
27.1.1) 理解文件路径
文件路径(file path)是文件在你电脑上的地址。它告诉 Python 到底去哪里找文件。路径分为两种:
绝对路径(absolute path)指定从文件系统根目录开始的完整位置:
- Windows:
C:\Users\Alice\Documents\data.txt - macOS/Linux:
/home/alice/documents/data.txt
相对路径(relative path)指定相对于当前工作目录的位置:
data.txt(当前目录中的文件)reports/sales.txt(子目录中的文件)../config.txt(父目录中的文件)
当前工作目录(current working directory, CWD)是你使用相对路径时,Python 查找文件的文件夹。当你运行一个 Python 脚本时,CWD 是你执行命令时所在的目录,不一定是脚本文件所在的位置。
例如:
# Directory structure:
/home/alice/
└── projects/
└── script.py
# Running from the projects folder:
$ cd /home/alice/projects
$ python script.py
# CWD is /home/alice/projects
# Running from the parent folder:
$ cd /home/alice
$ python projects/script.py
# CWD is /home/alice (not where script.py is!)你可以这样查看当前工作目录:
import os
# 获取当前工作目录
current_dir = os.getcwd()
print(f"Current directory: {current_dir}")Output:
Current directory: /home/alice/projects/file_demoos.getcwd() 函数会返回当前工作目录的绝对路径。这对于理解你使用相对路径时 Python 会去哪里找文件很有用。
理解当前工作目录很重要,因为它决定了当你使用像 "data.txt" 这样的相对路径时,Python 会到哪里去查找。如果你从 /home/alice/projects/ 运行脚本并打开 "data.txt",Python 会查找 /home/alice/projects/data.txt。
27.1.2) 高效使用相对路径
处理文件时,相对路径往往比绝对路径更方便,因为它能让你的代码具备可移植性(portable)——无论项目文件夹在不同电脑上的位置如何变化,代码都能工作。
下面是常见的相对路径模式:
# 当前工作目录中的文件
filename = "student_grades.txt"
# 子目录中的文件(当前目录内的 data 文件夹)
filename = "data/student_grades.txt"
# 父目录中的文件
filename = "../shared_data.txt"在本章的示例中,我们主要使用像 "data.txt" 这样简单的文件名。这意味着:
- 数据文件应当位于你运行 Python 命令的同一个目录中
- 如果你的脚本在
/home/alice/projects/,并且你在该目录运行python script.py,那么 Python 会在/home/alice/projects/中查找data.txt
这种做法能让示例保持清晰,把重点放在文件操作而不是路径导航上。如果你遇到 FileNotFoundError,可以用 os.getcwd() 来检查 Python 正在从哪里查找文件。
27.1.3) 不同操作系统的路径分隔符
不同操作系统使用不同的字符来分隔路径中的目录:
- Windows 使用反斜杠:
data\reports\sales.txt - macOS 和 Linux 使用正斜杠:
data/reports/sales.txt
当你在代码中使用正斜杠时,Python 会自动处理——它在所有操作系统上都能工作:
# 这在 Windows、macOS 和 Linux 上都能工作
filename = "data/reports/sales.txt"Python 会把正斜杠转换为适合你操作系统的分隔符。
27.2) 打开与关闭文件
要处理一个文件,我们必须先打开(open)它,这会在我们的程序与磁盘上的文件之间建立连接。当我们完成后,必须关闭(close)它,以释放系统资源并确保所有数据都正确保存。
27.2.1) open() 函数
open() 函数会创建一个文件对象(file object),用于表示与文件的连接。最简单的用法是提供文件名:
# 打开文件用于读取(默认模式)
file = open("message.txt")这会以当前目录中的 message.txt 文件为目标打开文件。open() 函数返回一个文件对象,我们将其存入变量 file。该对象提供读取或写入文件的方法。
不过,这种基础用法有一个关键问题:如果在打开文件之后发生错误,文件可能永远不会被关闭。我们来看看为什么关闭文件很重要。
27.2.2) 为什么关闭文件很重要
当你打开一个文件时,操作系统会分配资源来维持这个连接。如果你不关闭文件:
- 数据可能不会被保存:写文件时,数据通常会先缓存在内存中,只有在关闭文件时才会写入磁盘
- 系统资源被浪费:每个打开的文件都会消耗内存和文件句柄
- 其他程序可能被阻塞:某些系统会阻止其他程序访问已经打开的文件
要关闭文件,调用它的 close() 方法:
file = open("message.txt")
# ... 使用文件进行操作 ...
file.close() # 释放资源并确保数据已保存27.2.3) 手动关闭的问题
手动关闭文件容易出错。如果在打开和关闭之间发生异常(exception),close() 调用可能永远不会执行:
file = open("data.txt")
result = process_data(file) # 如果这里抛出异常...
file.close() # ...这一行就永远不会运行!这是一个非常常见的问题,因此 Python 提供了更好的解决方案:with 语句,我们会在 27.4 节学习。现在请先理解:手动打开和关闭文件需要格外小心,确保 close() 总会被调用。
27.2.4) 检查文件是否处于打开状态
文件对象有一个 closed 属性,用来告诉你文件是否已关闭:
file = open("data.txt")
print(file.closed) # Output: False
file.close()
print(file.closed) # Output: True一旦文件关闭,尝试读取或写入它就会引发错误:
file = open("data.txt")
file.close()
# This raises ValueError: I/O operation on closed file
content = file.read()错误信息清楚地表明了问题:你正在对一个已经关闭的文件执行 I/O(输入/输出)操作。
27.3) 理解文件模式(r、w、a、文本 vs 二进制)与编码
打开文件时,你可以指定一个模式(mode),它决定允许哪些操作,以及文件应如何处理。理解模式对正确处理文件至关重要。
27.3.1) 文本模式 vs 二进制模式
文件可以以两种基本模式打开:
文本模式(text mode)(默认)把文件视为文本内容。Python 会自动:
- 无论平台如何,都把行结束符转换为
\n - 处理文本编码(在字节与字符串之间转换)
- 允许读取和写入字符串
二进制模式(binary mode)把文件视为原始字节。Python 会:
- 读取和写入 bytes 对象,而不是字符串
- 不做任何转换或解释
- 用于图片、音频、视频等非文本文件
本章我们将聚焦文本模式,这是你最常用的模式。二进制模式通过在模式字符串中添加 'b' 来表示(如 'rb' 或 'wb'),但在处理文本文件时不需要它。
27.3.2) 三种主要文件模式
Python 提供三种用于打开文本文件的主要模式:
读取模式('r') - 只读打开文件:
file = open("data.txt", "r") # 或者直接 open("data.txt")- 文件必须已存在,否则 Python 会抛出
FileNotFoundError - 你可以读取文件,但不能写入
- 如果你不指定模式,这是默认模式
写入模式('w') - 以写入方式打开文件:
file = open("output.txt", "w")- 如果文件不存在会创建
- 如果文件已存在,会清空所有已有内容
- 你可以写入文件,但不能读取
- 当你想创建新文件或完全替换已有文件时使用
追加模式('a') - 以追加方式打开文件:
file = open("log.txt", "a")- 如果文件不存在会创建
- 保留已有内容,把新内容添加到末尾
- 你可以写入文件,但不能读取
- 当你想在不丢失现有内容的情况下向文件追加内容时使用
下面比较这些模式对一个已存在文件的影响:
# 假设 data.txt 包含:"Hello\nWorld\n"
# 读取模式 - 内容不变
file = open("data.txt", "r")
file.close()
# 文件仍然包含:"Hello\nWorld\n"
# 写入模式 - 内容被清空
file = open("data.txt", "w")
file.write("New content\n")
file.close()
# 文件现在包含:"New content\n"
# 追加模式 - 保留内容,添加新内容
file = open("data.txt", "a")
file.write("Added line\n")
file.close()
# 文件现在包含:"New content\nAdded line\n"27.3.3) 理解文本编码
当你处理文本文件时,Python 需要知道如何在磁盘上存储的字节与程序中的字符串字符之间进行转换。这个转换过程称为编码(encoding)。
最常见的编码是 UTF-8,它能表示任何语言的任意字符。它是 Python 3 的默认编码,也是现代文本文件的标准。
# 显式指定 UTF-8 编码(虽然它通常就是默认值)
file = open("data.txt", "r", encoding="utf-8")编码为什么重要?考虑一个包含带重音字符名字的文本文件:
# 写入包含特殊字符的文件
file = open("names.txt", "w", encoding="utf-8")
file.write("José\n")
file.write("François\n")
file.write("Müller\n")
file.close()
# 再读回来
file = open("names.txt", "r", encoding="utf-8")
content = file.read()
file.close()
print(content)Output:
José
François
Müller如果你用错误的编码打开文件,可能会看到乱码,或者直接得到错误。除非你有必须使用其他编码的明确原因,否则新文件一律使用 UTF-8。
27.3.4) 其他模式变体
Python 还提供可与主要模式组合使用的额外模式字符:
加号模式(plus modes)允许同时读写:
'r+'- 读写(文件必须存在)'w+'- 写读(会清空已有内容)'a+'- 追加读(保留已有内容)
对初学者来说,将读取与写入分开、分别打开文件比使用加号模式更清晰。本章我们将坚持使用简单模式('r'、'w'、'a')。
27.4) 使用 with 自动管理文件
with 语句提供了一种更整洁、更安全的方式来处理文件。它会在你完成后自动关闭文件,即使发生错误也一样。
27.4.1) with 语句语法
下面是使用 with 打开文件的方法:
with open("data.txt", "r") as file:
content = file.read()
print(content)
# 文件在这里会自动关闭这段语法包含多个部分:
with- 启动上下文管理器(context manager)的关键字open("data.txt", "r")- 打开文件as file- 创建一个变量引用该文件对象:- 开始缩进代码块- 缩进代码块 - 处理文件的代码
- 代码块结束后 - 文件会自动关闭
关键收益:Python 保证 with 块结束时文件一定会被关闭,不管块是如何结束的(正常结束、通过 return 结束,或由于异常结束)。
27.4.2) 为什么 with 比手动关闭更好
对比这两种方式:
# 手动关闭 - 有风险
file = open("data.txt", "r")
content = file.read()
result = process(content) # 如果这里抛出异常...
file.close() # ...这一行不会运行
# 使用 with - 安全
with open("data.txt", "r") as file:
content = file.read()
result = process(content) # 即使这里抛出异常...
# ...文件仍会自动关闭with 语句使用了 Python 的上下文管理器协议(context manager protocol)(我们会在第 28 章详细探讨)。目前,你可以把它理解为一个保证:“无论发生什么,等你用完后我都会清理这个资源。”
27.4.3) 同时处理多个文件
你可以在一个 with 语句中打开多个文件:
with open("input.txt", "r") as infile, open("output.txt", "w") as outfile:
content = infile.read()
outfile.write(content.upper())
# 两个文件都在这里自动关闭当你需要同时从一个文件读取并写入到另一个文件时,这很有用。即使发生错误,也能保证两个文件都被正确关闭。
27.4.4) 文件对象只在 with 代码块内有效
一旦 with 代码块结束,文件就会关闭,你无法再使用该文件对象:
with open("data.txt", "r") as file:
content = file.read()
print("Inside with block:", file.closed) # Output: Inside with block: False
print("Outside with block:", file.closed) # Output: Outside with block: True
# This raises ValueError: I/O operation on closed file
more_content = file.read()这种行为是刻意设计的——它能防止你意外使用已关闭的文件。如果你需要在 with 代码块外使用文件内容,请在代码块结束前把内容保存到变量中(比如上面的 content)。
从本章接下来的内容开始,我们将对所有文件操作使用 with。这是推荐方式,也是你在自己代码中应该使用的方法。
27.5) 读取文本文件
现在我们已经理解了如何用 with 安全地打开文件,让我们来探索读取文本文件内容的不同方式。
27.5.1) 使用 read() 读取整个文件
read() 方法会把整个文件内容作为一个字符串读取出来:
with open("message.txt", "r") as file:
content = file.read()
print(content)如果 message.txt 包含:
Welcome to Python!
This is a text file.
It has multiple lines.Output:
Welcome to Python!
This is a text file.
It has multiple lines.read() 方法会包含文件中的所有换行符(\n)。当你打印该字符串时,Python 会因为这些换行符而把每行显示在单独的一行上。
你也可以通过向 read() 传入一个数字来读取指定数量的字符:
with open("message.txt", "r") as file:
first_ten = file.read(10) # 读取前 10 个字符
print(f"First 10 characters: '{first_ten}'")Output:
First 10 characters: 'Welcome to'读取整个文件很简单,对小文件也很适用。但对大型文件(以 MB 或 GB 计),一次性读取全部内容可能会占用过多内存。对于这种情况,按行读取更高效。
27.5.2) 使用 readline() 逐行读取
readline() 方法会从文件中读取一行,包括行末的换行符:
with open("message.txt", "r") as file:
line1 = file.readline()
line2 = file.readline()
line3 = file.readline()
print(f"Line 1: {line1}")
print(f"Line 2: {line2}")
print(f"Line 3: {line3}")Output:
Line 1: Welcome to Python!
Line 2: This is a text file.
Line 3: It has multiple lines.
注意输出里多出来的空行。因为从文件读取的每一行末尾都有 \n,而 print() 又会再添加一个换行。为了避免这一点,可以使用 rstrip() 方法去除末尾空白字符:
with open("message.txt", "r") as file:
line1 = file.readline().rstrip()
line2 = file.readline().rstrip()
print(f"Line 1: {line1}")
print(f"Line 2: {line2}")Output:
Line 1: Welcome to Python!
Line 2: This is a text file.当 readline() 到达文件末尾时,它会返回空字符串 ""。这让你能够检测是否已经没有更多行可读:
with open("message.txt", "r") as file:
while True:
line = file.readline()
if line == "": # 文件结束
break
print(line.rstrip())不过,还有一种更符合 Python 风格的逐行读取方式。
27.5.3) 使用 for 循环迭代读取每一行
文件对象是可迭代的(iterable),这意味着你可以直接用 for 循环对它进行遍历。这是逐行读取文件最常见、也最符合 Python 风格(Pythonic)的方式:
with open("message.txt", "r") as file:
for line in file:
print(line.rstrip())Output:
Welcome to Python!
This is a text file.
It has multiple lines.这种方式的优点是:
- 更简洁:不需要
readline()或检查空字符串 - 更高效:Python 按块读取文件,而不是一次把整个文件加载到内存
- 更符合 Python 风格:使用迭代,这是 Python 的核心概念
循环的每一次迭代都会读取文件的下一行。当没有更多行时,循环会自动结束。
27.5.4) 使用 readlines() 把所有行读入列表
readlines() 方法会读取文件中的所有行,并以字符串列表形式返回:
with open("message.txt", "r") as file:
lines = file.readlines()
print(f"Number of lines: {len(lines)}")
for i, line in enumerate(lines, start=1):
print(f"Line {i}: {line.rstrip()}")Output:
Number of lines: 3
Line 1: Welcome to Python!
Line 2: This is a text file.
Line 3: It has multiple lines.列表中的每个元素都是一行对应的字符串,并包含该行的换行符。这个方法适用于你需要:
- 通过索引访问某一行:
lines[0]、lines[1]等 - 多次处理这些行
- 在处理前就知道总行数
不过,和 read() 一样,readlines() 会把整个文件加载到内存中。对于大文件,用 for 循环迭代读取会更节省内存。
27.6) 写入与追加文本文件
写入文件和读取同样重要。Python 提供了简单的方法来创建新文件或修改现有文件。
27.6.1) 使用 write() 写入文件
要写入文件,请以写入模式('w')打开,并使用 write() 方法:
with open("output.txt", "w") as file:
file.write("Hello, World!\n")
file.write("This is a new file.\n")运行这段代码后,output.txt 内容为:
Hello, World!
This is a new file.关于 write() 的要点:
- 它向文件写入一个字符串
- 它不会自动添加换行符——你必须自己包含
\n - 它返回写入的字符数(尽管我们通常会忽略这个返回值)
- 如果文件已存在,写入模式会在写入前清空所有已有内容
我们看看对一个已有文件进行写入会发生什么:
# 首先,创建一个包含一些内容的文件
with open("demo.txt", "w") as file:
file.write("Original content\n")
# 现在再次以写入模式打开它
with open("demo.txt", "w") as file:
file.write("New content\n")
# 读取文件看看它包含什么
with open("demo.txt", "r") as file:
print(file.read())Output:
New content原始内容消失了。写入模式总是从空文件开始,无论是在创建新文件还是覆盖已有文件。
27.6.2) 写入多行
你可以多次调用 write() 来写入多行:
with open("shopping_list.txt", "w") as file:
file.write("Apples\n")
file.write("Bananas\n")
file.write("Oranges\n")27.6.3) 写入集合中的数据
一个常见任务是把列表或其他集合中的数据写入文件:
students = ["Alice", "Bob", "Carol", "David"]
with open("students.txt", "w") as file:
for student in students:
file.write(student + "\n")这会创建 students.txt,内容为:
Alice
Bob
Carol
David27.6.4) 追加写入文件
当你想在不清空已有内容的情况下把内容添加到现有文件末尾时,使用追加模式('a'):
# 创建一个包含初始内容的文件
with open("log.txt", "w") as file:
file.write("Program started\n")
# 之后,追加更多内容
with open("log.txt", "a") as file:
file.write("Processing data\n")
file.write("Processing complete\n")
# 读取文件查看所有内容
with open("log.txt", "r") as file:
print(file.read())Output:
Program started
Processing data
Processing complete追加模式非常适合日志文件,因为你希望持续记录事件。每次以追加模式打开文件,新内容都会添加到末尾,从而保留之前的全部内容。
27.7) 处理常见文件 I/O 错误
文件操作可能由于很多原因失败:文件不存在、你没有访问权限、磁盘满了,或者文件正被其他程序打开。学会优雅地处理这些错误能让你的程序更健壮、更友好。
27.7.1) FileNotFoundError:文件不存在时
最常见的文件错误发生在你尝试读取一个不存在的文件时:
# WARNING: This will raise FileNotFoundError if data.txt doesn't exist
with open("data.txt", "r") as file:
content = file.read()如果 data.txt 不存在,Python 会抛出:
FileNotFoundError: [Errno 2] No such file or directory: 'data.txt'要优雅地处理它,请使用 try-except 代码块(正如我们在第 25 章学过的):
try:
with open("data.txt", "r") as file:
content = file.read()
print(content)
except FileNotFoundError:
print("Error: The file 'data.txt' was not found.")
print("Please check the filename and try again.")Output (if file doesn't exist):
Error: The file 'data.txt' was not found.
Please check the filename and try again.这种方式能防止程序崩溃,并向用户提供有帮助的信息。
27.7.2) PermissionError:无法访问文件时
有时你没有权限读取或写入某个文件:
try:
with open("/root/protected.txt", "r") as file:
content = file.read()
except PermissionError:
print("Error: You don't have permission to access this file.")权限错误可能发生在以下情况:
- 文件属于其他用户
- 文件位于受保护的系统目录
- 文件被标记为只读,而你尝试写入
- 在 Windows 上,文件正被另一个程序打开
27.7.3) IsADirectoryError:把目录当成文件打开时
如果你不小心尝试打开一个目录而不是文件:
try:
with open("my_folder", "r") as file:
content = file.read()
except IsADirectoryError:
print("Error: 'my_folder' is a directory, not a file.")当文件和目录名称相似,或你在路径中忘记包含文件名时,就可能发生这种情况。
27.7.4) UnicodeDecodeError:编码不匹配时
如果你用错误的编码读取文件,可能会得到 UnicodeDecodeError:
try:
with open("data.txt", "r", encoding="utf-8") as file:
content = file.read()
except UnicodeDecodeError:
print("Error: The file encoding doesn't match UTF-8.")
print("The file might use a different encoding.")当文件包含不是有效 UTF-8 的字节时就会出现这个错误。如果你遇到它,文件可能:
- 使用了不同编码(如 Latin-1 或 Windows-1252)
- 是一个二进制文件,但你尝试把它当文本读取
- 已损坏
27.7.5) 处理多种错误类型
你可以在一个 try-except 结构中捕获多种错误类型:
filename = input("Enter filename: ")
try:
with open(filename, "r") as file:
content = file.read()
print(content)
except FileNotFoundError:
print(f"Error: '{filename}' does not exist.")
except PermissionError:
print(f"Error: You don't have permission to read '{filename}'.")
except IsADirectoryError:
print(f"Error: '{filename}' is a directory, not a file.")
except UnicodeDecodeError:
print(f"Error: '{filename}' contains invalid text encoding.")这样能针对每种问题给出具体且有帮助的错误信息。用户能够清楚知道哪里出错,并采取相应行动。
27.7.6) 使用兜底的异常处理器
有时你希望捕获我们已覆盖的特定类型之外的任何意外文件相关错误。你可以在这些特定处理器之后使用通用的 Exception 处理器作为兜底:
filename = input("Enter filename: ")
try:
with open(filename, "r") as file:
content = file.read()
print(content)
except FileNotFoundError:
print(f"Error: '{filename}' not found.")
except PermissionError:
print(f"Error: No permission to read '{filename}'.")
except IsADirectoryError:
print(f"Error: '{filename}' is a directory.")
except UnicodeDecodeError:
print(f"Error: '{filename}' has invalid encoding.")
except Exception as e:
print(f"Unexpected error reading file: {e}")这能确保你的程序即便遇到你未预料到的错误也能进行处理。变量 e 包含异常对象,其中带有描述性的错误信息。把它打印出来会向用户提供关于问题的技术细节。
文件处理是编程中的一项基础技能。你已经学会了如何:
- 理解文件路径与当前工作目录
- 正确打开与关闭文件
- 使用不同文件模式进行读取、写入与追加
- 使用
with语句自动管理文件 - 使用多种方法读取文件(
read()、readline()、迭代、readlines()) - 写入与追加文件内容
- 优雅地处理常见文件 I/O 错误
这些技能让你能够创建在多次运行之间持久化(persist)数据的程序、处理文本文件、生成报告等。在下一章中,我们将深入探索上下文管理器(context managers),理解让 with 语句生效的机制,以及如何创建你自己的上下文管理器,用于管理文件之外的其他资源。