shutil 文件与目录操作
当程序需要备份、整理或清理文件时,如何在改变文件系统的同时控制覆盖范围和删除边界?pathlib 负责表达和判断路径,shutil 负责复制、移动和递归删除文件系统内容。两者通常配合使用:
from pathlib import Path
import shutil
source_path = Path("data") / "student.txt"
target_path = Path("backup") / "student.txt"
shutil.copy(source_path, target_path)shutil 的操作会直接改变文件系统。运行代码前必须确认源路径、目标路径和覆盖范围。本章示例会把写入和删除限制在临时工作区中。
文件复制
单文件复制:copy()
shutil.copy(source, target) 复制一个文件:
from pathlib import Path
from tempfile import TemporaryDirectory
import shutil
source_path = Path(__file__).resolve().parent / "source_files" / "readme.txt"
with TemporaryDirectory() as temp_dir:
target_path = Path(temp_dir) / "readme-copy.txt"
copied_path = shutil.copy(source_path, target_path)
print(copied_path)
print(target_path.read_text(encoding="utf-8"))目标有两种常见写法:
- 目标是已经存在的目录:保留源文件名,复制到该目录中。
- 目标是完整文件路径:可以在复制时设置新文件名。
shutil.copy(source_path, backup_dir)
shutil.copy(source_path, backup_dir / "new_readme.txt")如果最终目标文件已经存在,copy() 会覆盖它。复制前应确认覆盖是否符合预期:
if target_path.exists():
print(f"目标已存在:{target_path}")
else:
shutil.copy(source_path, target_path)仅仅先判断并不能彻底避免文件系统在下一刻发生变化,真正复制时仍然要处理可能出现的异常。
元数据保留:copy() 与 copy2()
两个函数都会复制文件内容,区别在于保留的文件信息:
| 方法 | 主要行为 |
|---|---|
copy() | 复制文件内容和权限模式 |
copy2() | 在 copy() 基础上,尽可能保留修改时间等元数据 |
from pathlib import Path
import shutil
source_path = Path("source.txt")
target_path = Path("backup.txt")
shutil.copy2(source_path, target_path)“尽可能保留”不代表所有平台都能完整复制文件所有者、访问控制列表等信息。普通文件备份通常优先使用 copy2();只关心文件内容时,可以使用 copy()。
目录复制
复制目录树:copytree()
copytree() 递归复制整个目录:
from pathlib import Path
from tempfile import TemporaryDirectory
import shutil
source_dir = Path(__file__).resolve().parent / "source_files"
with TemporaryDirectory() as temp_dir:
backup_dir = Path(temp_dir) / "course_backup"
shutil.copytree(source_dir, backup_dir)
print(backup_dir.exists())
print((backup_dir / "readme.txt").exists())默认情况下,目标目录已经存在会抛出 FileExistsError。这是为了避免不明确地合并两棵目录树。
合并已有目录:
确实需要把内容合并到已有目录时,可以设置 dirs_exist_ok=True:
shutil.copytree(source_dir, backup_dir, dirs_exist_ok=True)合并时,同名文件可能被覆盖;目标目录中原本存在、但源目录没有的文件不会被删除。因此,dirs_exist_ok=True 不等于自动同步目录,使用前仍然需要确认目标内容。
忽略内容:
备份目录时,可以使用 ignore_patterns() 忽略日志和缓存:
shutil.copytree(
source_dir,
backup_dir,
ignore=shutil.ignore_patterns("*.log", "__pycache__"),
)忽略规则针对每一层目录中的名称进行匹配。它使用的是文件名通配符,不是正则表达式。
文件移动与改名
shutil.move(source, target) 可以移动文件或目录:
from pathlib import Path
import shutil
source_path = Path("reports") / "score.txt"
archive_dir = Path("archive")
shutil.move(source_path, archive_dir)如果目标是已经存在的目录,源文件会移动到该目录,并保留原文件名。
传入完整目标路径时,可以同时改名:
target_path = Path("archive") / "final_score.txt"
shutil.move(source_path, target_path)目标已经存在时的结果可能受到目标类型和操作系统影响。为了避免意外覆盖或形成额外嵌套目录,移动前应明确检查目标路径。
在同一文件系统中,移动通常可以直接重命名路径;跨文件系统移动时,shutil.move() 可能通过“复制后删除源文件”完成。因此,大文件跨磁盘移动不一定会立即完成,中途失败时也需要检查源和目标状态。
递归删除与安全边界
shutil.rmtree(path) 会删除目录以及其中的全部内容:
import shutil
shutil.rmtree(backup_dir)它与 Path 中两个删除方法的范围不同:
| 方法 | 删除对象 |
|---|---|
Path.unlink() | 文件或符号链接 |
Path.rmdir() | 空目录 |
shutil.rmtree() | 目录及其全部内容 |
rmtree() 不会进入回收站,通常难以恢复。不要把用户主目录、课程根目录、当前工作目录或来源不明的路径直接传给它。
删除前边界检查:
下面只允许删除临时工作区中的指定备份目录:
def remove_backup(backup_dir, workspace):
if backup_dir.parent != workspace:
raise ValueError("拒绝删除工作区之外的目录")
if backup_dir.name != "course_backup":
raise ValueError("拒绝删除未知目录")
shutil.rmtree(backup_dir)先验证父目录和目标名称,可以降低变量传错造成的误删风险。真实项目还应该结合业务规则、权限和备份策略做更严格的检查。
案例:安全备份与整理
需求: 在不修改只读源目录的前提下,创建备份、复制独立文件、整理目录内容,并在确认边界后清理临时备份。
功能分析: 使用 copytree() 过滤并复制目录,使用 copy2() 保留独立文件元数据,使用 move() 整理文件,使用边界检查和 rmtree() 安全删除备份。
案例提供一份只读源目录:
01-course-backup/
├── main.py
└── source_files/
├── debug.log
├── readme.txt
├── data/
│ └── scores.csv
└── notes/
└── lesson.md按目录树创建源文件。debug.log 用来验证复制时的忽略规则:
课程备份案例源文件。这条调试日志应在复制时被忽略。name,score
张三,90
李四,85# 文件备份练习
使用 shutil 复制和移动文件。程序在临时工作区中完成备份、复制、移动和删除,不会修改 source_files:
from pathlib import Path
from tempfile import TemporaryDirectory
import shutil
def show_files(directory):
for path in sorted(directory.rglob("*")):
if path.is_file():
print(path.relative_to(directory).as_posix())
def remove_backup(backup_dir, workspace):
if backup_dir.parent != workspace:
raise ValueError("拒绝删除工作区之外的目录")
if backup_dir.name != "course_backup":
raise ValueError("拒绝删除未知目录")
shutil.rmtree(backup_dir)
def main():
script_dir = Path(__file__).resolve().parent
source_dir = script_dir / "source_files"
with TemporaryDirectory() as temp_dir:
workspace = Path(temp_dir)
backup_dir = workspace / "course_backup"
shutil.copytree(
source_dir,
backup_dir,
ignore=shutil.ignore_patterns("*.log", "__pycache__"),
)
independent_copy = workspace / "readme-copy.txt"
shutil.copy2(source_dir / "readme.txt", independent_copy)
archive_dir = backup_dir / "archive"
archive_dir.mkdir()
shutil.move(backup_dir / "notes" / "lesson.md", archive_dir / "lesson.md")
(backup_dir / "notes").rmdir()
print("备份内容:")
show_files(backup_dir)
remove_backup(backup_dir, workspace)
print(f"备份目录存在:{backup_dir.exists()}")
print(f"独立副本存在:{independent_copy.exists()}")
if __name__ == "__main__":
main()运行结果:
备份内容:
archive/lesson.md
data/scores.csv
readme.txt
备份目录存在:False
独立副本存在:True案例中的 debug.log 被忽略,lesson.md 被移动到 archive,整个备份目录经过边界检查后被递归删除。临时工作区结束时,独立副本也会自动清理。
模块协作边界
| 模块 | 主要职责 |
|---|---|
pathlib | 表达、拼接和判断路径 |
shutil | 复制、移动和递归删除 |
zipfile | 创建、读取和解压 ZIP 文件 |
shutil 也提供归档相关接口,但本课程会在后续 zipfile 章节集中学习压缩文件,不在这里重复展开。
小结
copy()复制文件内容和权限模式,copy2()尽可能保留更多元数据。copytree()复制整个目录,默认要求目标目录不存在。dirs_exist_ok=True可以合并目录,但可能覆盖同名文件。ignore_patterns()可以在复制目录时忽略匹配的名称。move()可以移动或改名,跨文件系统时可能执行复制和删除。rmtree()会递归删除整个目录,执行前必须验证路径边界。Path可以直接传给shutil,不需要先转换成字符串。
练习
- 在
TemporaryDirectory()中使用copy()和copy2()复制同一个文件,比较结果。 - 使用
copytree()复制示例目录,并忽略.log文件。 - 在临时工作区中使用
move()移动文件并修改名称。 - 为
remove_backup()增加检查,拒绝删除不存在的目录。 - 修改综合案例,使用
dirs_exist_ok=True把新增文件合并到已有备份目录。