Hello,大家好,我是 Ivan。
我们前面写的商品、成绩和用户信息,都是保存在变量里的。程序正常运行的时候没有什么问题,但是程序停止运行,这些数据也就跟着没了。在实际开发过程中,配置、日志、接口返回结果,还有程序生成的文本,不可能每次启动程序以后重新写一遍,所以就需要把它们保存到文件里。
思维导图




那今天我就以数据保存为切入点,带大家讲解 Python 的文件与目录操作,包括文件读取、内容写入、路径处理、目录遍历,以及文件的复制和移动。最后再写一个文本整理脚本,把这些操作放到一起使用。

一、先准备一个文本文件
新建一个项目目录,结构如下:
file_demo/
├─ main.py
└─ data/
└─ notes.txt
在 notes.txt 中写入几行内容:
学习 Python 文件操作
整理项目中的文本文件
把处理结果保存下来
文件已经准备好了,接下来在 main.py 中读取它:
python
file = open("data/notes.txt", "r", encoding="utf-8")
content = file.read()
print(content)
file.close()
open() 用来打开文件。第一个参数是文件路径,"r" 表示读取,encoding="utf-8" 指定文本编码。
文件打开以后,read() 会读取其中的内容,并返回一个字符串。操作完成以后还要调用 close() 关闭文件,把占用的系统资源释放掉。
二、使用 with 自动关闭文件
上面的写法可以正常运行,不过只要中间发生异常,最后的 close() 就可能执行不到。平时读写文件,更常见的是使用 with:
python
with open("data/notes.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)
进入 with 代码块时,Python 会打开文件。代码块执行结束后,文件会自动关闭,我们不用再单独写 file.close()。
这里的 file 只是一个普通变量名,也可以改成其他名字:
python
with open("data/notes.txt", "r", encoding="utf-8") as notes_file:
content = notes_file.read()
不过,文件对象离开 with 以后已经关闭,后面不能继续调用它的 read() 或 write()。需要用到的内容,应该在代码块中读取出来,再交给外面的变量。

三、文件内容有几种读取方式
read() 会一次读取整个文件:
python
with open("data/notes.txt", "r", encoding="utf-8") as file:
content = file.read()
print(type(content))
print(content)
如果只想读取一行,可以使用 readline():
python
with open("data/notes.txt", "r", encoding="utf-8") as file:
first_line = file.readline()
second_line = file.readline()
print(first_line)
print(second_line)
文件对象会记住当前读到了哪里。第一次调用拿到第一行,第二次调用会接着读取下一行。
readlines() 会读取所有行,并返回一个列表:
python
with open("data/notes.txt", "r", encoding="utf-8") as file:
lines = file.readlines()
print(lines)
你可能会看到这样的结果:
python
[
"学习 Python 文件操作\n",
"整理项目中的文本文件\n",
"把处理结果保存下来\n",
]
每一行末尾的 \n 是换行符。想去掉首尾空白和换行,可以使用 strip():
python
for line in lines:
print(line.strip())
处理日志或者体积较大的文本时,一般不需要一次把全部内容读进内存,可以直接遍历文件:
python
with open("data/notes.txt", "r", encoding="utf-8") as file:
for line in file:
print(line.strip())
这种写法每次处理一行,文件很大时也不会一下占用太多内存。

四、为什么要写 encoding
文本保存到磁盘时,实际存储的是字节。程序读取文件,需要按照对应的编码把字节还原成文字。如果保存和读取使用的编码不一致,就可能出现乱码,严重时会直接报错。
python
with open("data/notes.txt", "r", encoding="utf-8") as file:
content = file.read()
自己创建的文本文件,建议统一使用 UTF-8。Windows 上如果省略 encoding,Python 可能跟随系统默认编码,同一份代码放到其他电脑上运行,结果不一定相同。
如果拿到的是旧文件,也有可能使用 GBK:
python
with open("data/old_notes.txt", "r", encoding="gbk") as file:
content = file.read()
这里不能看到乱码就随便换编码。应该先确认文件原本使用的编码,再选择对应参数。

五、向文件中写入内容
把模式改成 "w",就可以写入文件:
python
with open("data/result.txt", "w", encoding="utf-8") as file:
file.write("文件处理完成\n")
file.write("共处理 3 条记录\n")
如果 result.txt 不存在,Python 会创建它。如果文件已经存在,"w" 会先清空原来的内容,再写入新内容。
write() 不会自动换行:
python
with open("data/result.txt", "w", encoding="utf-8") as file:
file.write("第一行")
file.write("第二行")
文件中得到的是:
第一行第二行
需要换行时,要自己加上 \n。另外,write() 只能写入字符串,数字需要先转换:
python
count = 3
with open("data/result.txt", "w", encoding="utf-8") as file:
file.write(f"共处理 {count} 条记录\n")
一次写入多行可以使用 writelines():
python
lines = [
"第一条记录\n",
"第二条记录\n",
"第三条记录\n",
]
with open("data/result.txt", "w", encoding="utf-8") as file:
file.writelines(lines)
writelines() 同样不会自动添加换行,所以列表中的每个字符串还要保留 \n。
六、r、w、a 和 x 有什么区别
打开文件时,第二个参数决定了接下来可以做什么。
| 模式 | 作用 | 文件不存在时 | 文件已存在时 |
|---|---|---|---|
r |
读取文件 | 报错 | 从头读取 |
w |
写入文件 | 创建文件 | 清空原内容 |
a |
追加内容 | 创建文件 | 从末尾继续写 |
x |
创建新文件 | 创建文件 | 报错 |
保存日志时,一般不能覆盖之前的记录,可以使用 "a":
python
with open("data/app.log", "a", encoding="utf-8") as file:
file.write("程序启动\n")
每运行一次,内容都会加到文件末尾。
如果想确保程序不会覆盖已有文件,可以使用 "x":
python
with open("data/report.txt", "x", encoding="utf-8") as file:
file.write("第一次生成报告\n")
当 report.txt 已经存在时,这段代码会抛出 FileExistsError。这种模式适合生成不能重复覆盖的文件。

七、使用 pathlib 处理路径
前面的代码直接使用了字符串路径:
python
"data/notes.txt"
路径简单时这样写没有问题。目录多起来以后,可以使用标准库里的 pathlib:
python
from pathlib import Path
file_path = Path("data") / "notes.txt"
with file_path.open("r", encoding="utf-8") as file:
print(file.read())
Path("data") / "notes.txt" 会拼接出完整路径。这里的 / 是 Path 对象提供的路径运算,不是数学除法。
如果代码可能从不同目录启动,最好根据当前脚本的位置定位文件:
python
from pathlib import Path
base_dir = Path(__file__).resolve().parent
file_path = base_dir / "data" / "notes.txt"
print(file_path)
__file__ 表示当前 Python 文件,resolve() 得到绝对路径,parent 取它所在的目录。这样在项目外面运行脚本时,也不会因为终端位置变化而找错文件。

八、直接读取和写入文本
Path 还提供了两个快捷方法:
python
from pathlib import Path
file_path = Path("data/notes.txt")
content = file_path.read_text(encoding="utf-8")
print(content)
写入文本可以使用 write_text():
python
result_path = Path("data/result.txt")
result_path.write_text(
"文件处理完成\n共处理 3 条记录\n",
encoding="utf-8",
)
这两个方法内部也会完成文件的打开和关闭,适合一次读完或一次写完的小文件。需要逐行处理、追加内容,或者对打开过程进行更多控制时,还是使用 open() 更方便。
九、创建目录并判断路径
创建目录可以使用 mkdir():
python
from pathlib import Path
output_dir = Path("output")
output_dir.mkdir()
目录已经存在时,再运行一次会报错。项目代码里通常会这样写:
python
output_dir.mkdir(exist_ok=True)
如果中间还有多层目录,可以加上 parents=True:
python
report_dir = Path("output") / "2026" / "reports"
report_dir.mkdir(parents=True, exist_ok=True)
判断路径状态时,可以使用下面几个方法:
python
path = Path("data/notes.txt")
print(path.exists())
print(path.is_file())
print(path.is_dir())
exists() 判断路径是否存在,is_file() 判断它是不是文件,is_dir() 判断它是不是目录。它们返回的都是布尔值。

十、查看路径中的文件信息
Path 对象可以直接取出文件名、扩展名和上级目录:
python
from pathlib import Path
file_path = Path("data/archive/notes.txt")
print(file_path.name)
print(file_path.stem)
print(file_path.suffix)
print(file_path.parent)
输出结果是:
notes.txt
notes
.txt
data/archive
修改扩展名可以使用 with_suffix():
python
markdown_path = file_path.with_suffix(".md")
print(markdown_path)
得到的路径是 data/archive/notes.md。这个操作只是生成一个新的路径对象,不会直接修改磁盘上的文件。
十一、遍历目录中的文件
假设 data 目录中有下面这些内容:
data/
├─ a.txt
├─ b.txt
├─ products.csv
└─ archive/
└─ old.txt
iterdir() 可以查看当前目录下的直接子项:
python
from pathlib import Path
data_dir = Path("data")
for path in data_dir.iterdir():
print(path)
这里只会看到 a.txt、b.txt、products.csv 和 archive,不会继续进入 archive。
只找当前目录里的 .txt 文件,可以使用 glob():
python
for path in data_dir.glob("*.txt"):
print(path)
连子目录一起查找,可以使用 rglob():
python
for path in data_dir.rglob("*.txt"):
print(path)
这次 archive/old.txt 也会被找到。遍历得到的顺序可能受到操作系统和文件系统影响,需要固定顺序时可以配合 sorted():
python
for path in sorted(data_dir.rglob("*.txt")):
print(path)

十二、重命名和删除文件
rename() 可以修改文件名,也可以把文件移动到同一磁盘中的其他位置:
python
from pathlib import Path
old_path = Path("data/notes.txt")
new_path = Path("data/python_notes.txt")
old_path.rename(new_path)
删除文件使用 unlink():
python
file_path = Path("data/temp.txt")
if file_path.exists() and file_path.is_file():
file_path.unlink()
删除空目录可以使用 rmdir():
python
empty_dir = Path("data/empty")
empty_dir.rmdir()
目录中还有文件时,rmdir() 会报错。删除操作通常无法像编辑器撤销那样恢复,所以路径来自用户输入或配置文件时,应该先打印并检查,不要直接执行。
十三、复制和移动文件
复制、移动文件时,可以使用 shutil:
python
from pathlib import Path
import shutil
source = Path("data/notes.txt")
backup_dir = Path("backup")
backup_dir.mkdir(exist_ok=True)
shutil.copy2(source, backup_dir / source.name)
copy2() 会复制文件内容,并尽量保留修改时间等元数据。
移动文件使用 move():
python
source = Path("data/result.txt")
target = Path("output/result.txt")
target.parent.mkdir(parents=True, exist_ok=True)
shutil.move(source, target)
复制整个目录可以使用 copytree():
python
shutil.copytree("data", "data_backup", dirs_exist_ok=True)
shutil.rmtree() 可以递归删除整个目录,包括里面的所有文件:
python
shutil.rmtree("data_backup")
这行代码影响的范围比 unlink() 大很多。实际项目里使用它时,必须确认目标路径,尤其不要把未经检查的字符串直接传进去。

十四、读取二进制文件
文本文件可以指定编码,图片、音频和压缩包保存的是二进制数据,需要使用 "rb" 或 "wb":
python
with open("data/photo.png", "rb") as source:
image_data = source.read()
with open("output/photo_copy.png", "wb") as target:
target.write(image_data)
这段代码会复制图片内容。二进制模式返回的是 bytes,不需要设置 encoding。
不过,复制文件时直接使用 shutil.copy2() 更方便。这里写 rb 和 wb,主要是为了看清文本模式与二进制模式的区别。以后处理上传文件、图片内容或网络响应时,还会遇到字节数据。
十五、常见的文件操作错误
1. 相对路径找不到文件
FileNotFoundError: [Errno 2] No such file or directory
遇到这个错误,可以先打印当前工作目录:
python
from pathlib import Path
print(Path.cwd())
相对路径是从当前工作目录开始计算的,不一定是 Python 文件所在的目录。项目里的固定资源,可以使用 Path(__file__).resolve().parent 定位。
2. 使用 w 覆盖了原文件
"w" 打开文件时会清空已有内容。只是想继续记录日志,应该使用 "a"。不确定文件能不能覆盖时,可以先用 exists() 检查,或者选择 "x"。
3. 忘记添加换行符
连续调用 write() 和 writelines() 都不会自动换行。写入多行内容时,要自己在字符串末尾添加 \n。
4. 把目录当成文件打开
python
path = Path("data")
with path.open("r", encoding="utf-8") as file:
print(file.read())
data 是目录,这样会出现 IsADirectoryError。路径来自外部时,可以先使用 is_file() 和 is_dir() 判断。
5. 文件正在被其他程序占用
Windows 上删除或移动文件时,如果它正在被编辑器、播放器或者其他程序占用,可能会出现 PermissionError。这时要先关闭占用文件的程序,再重新执行代码。
十六、综合示例:合并目录中的文本
现在做一个简单的文本整理脚本。它会找到 input 目录里的所有 .txt 文件,按照文件名排序,再把内容合并到 output/merged.txt。
项目结构如下:
file_demo/
├─ merge.py
├─ input/
│ ├─ a.txt
│ ├─ b.txt
│ └─ c.txt
└─ output/
在 merge.py 中写入:
python
from pathlib import Path
base_dir = Path(__file__).resolve().parent
input_dir = base_dir / "input"
output_file = base_dir / "output" / "merged.txt"
output_file.parent.mkdir(parents=True, exist_ok=True)
text_files = sorted(input_dir.glob("*.txt"))
with output_file.open("w", encoding="utf-8") as output:
for file_path in text_files:
content = file_path.read_text(encoding="utf-8")
output.write(f"===== {file_path.name} =====\n")
output.write(content.rstrip())
output.write("\n\n")
print(f"合并完成:{output_file}")
这里使用 __file__ 定位项目目录,运行脚本的位置发生变化也不会影响路径。glob("*.txt") 只查找 input 目录中的文本文件,sorted() 让每次合并的顺序保持一致。
写入时先加上原文件名,再写入正文。rstrip() 会去掉文本末尾多余的换行,随后统一补上两个 \n,不同文件之间就会留出一行。

十七、练习
练习一:统计文本行数
读取一个文本文件,统计其中共有多少行。空行也要计算在内,最后打印文件名和行数。
练习二:保存用户输入
让用户连续输入三条待办事项,把它们写入 todo.txt,每条内容单独占一行。再次运行程序时,原来的内容不能被覆盖。
练习三:整理指定类型的文件
遍历一个目录,把所有 .txt 文件移动到 texts 子目录中。移动前要创建目录,并检查当前路径确实是文件。
练习四:批量修改扩展名
找到目录中的所有 .log 文件,把扩展名改成 .txt。可以使用 glob()、with_suffix() 和 rename()。
练习五:备份目录
编写一个脚本,把 data 目录复制成 data_backup。目标目录已经存在时,仍然可以继续复制。
练习六:合并子目录中的文本
修改综合示例,让程序连 input 的子目录一起查找 .txt 文件。合并结果中除了文件名,还要写出文件相对于 input 的路径。
文件读写的代码本身并不复杂,容易出问题的地方通常是路径、打开模式和编码。项目里需要操作文件时,先把目标路径打印出来,再确认使用的是读取、覆盖还是追加模式,很多错误都能提前发现。