Skip to content

shutil 文件与目录操作 ​

当程序需要备份、整理或清理文件时,如何在改变文件系统的同时控制覆盖范围和删除边界?pathlib 负责表达和判断路径,shutil 负责复制、移动和递归删除文件系统内容。两者通常配合使用:

python
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 的操作会直接改变文件系统。运行代码前必须确认源路径、目标路径和覆盖范围。本章示例会把写入和删除限制在临时工作区中。

shutil 安全备份流程

文件复制 ​

单文件复制:copy()

shutil.copy(source, target) 复制一个文件:

python
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"))

目标有两种常见写法:

  • 目标是已经存在的目录:保留源文件名,复制到该目录中。
  • 目标是完整文件路径:可以在复制时设置新文件名。
python
shutil.copy(source_path, backup_dir)
shutil.copy(source_path, backup_dir / "new_readme.txt")

如果最终目标文件已经存在,copy() 会覆盖它。复制前应确认覆盖是否符合预期:

python
if target_path.exists():
    print(f"目标已存在:{target_path}")
else:
    shutil.copy(source_path, target_path)

仅仅先判断并不能彻底避免文件系统在下一刻发生变化,真正复制时仍然要处理可能出现的异常。

元数据保留:copy() 与 copy2()

两个函数都会复制文件内容,区别在于保留的文件信息:

方法主要行为
copy()复制文件内容和权限模式
copy2()在 copy() 基础上,尽可能保留修改时间等元数据
python
from pathlib import Path
import shutil

source_path = Path("source.txt")
target_path = Path("backup.txt")

shutil.copy2(source_path, target_path)

“尽可能保留”不代表所有平台都能完整复制文件所有者、访问控制列表等信息。普通文件备份通常优先使用 copy2();只关心文件内容时,可以使用 copy()。

shutil 文件复制分支与元数据保留对比

目录复制 ​

复制目录树:copytree()

copytree() 递归复制整个目录:

python
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:

python
shutil.copytree(source_dir, backup_dir, dirs_exist_ok=True)

合并时,同名文件可能被覆盖;目标目录中原本存在、但源目录没有的文件不会被删除。因此,dirs_exist_ok=True 不等于自动同步目录,使用前仍然需要确认目标内容。

忽略内容:

备份目录时,可以使用 ignore_patterns() 忽略日志和缓存:

python
shutil.copytree(
    source_dir,
    backup_dir,
    ignore=shutil.ignore_patterns("*.log", "__pycache__"),
)

忽略规则针对每一层目录中的名称进行匹配。它使用的是文件名通配符,不是正则表达式。

copytree 目录树通配过滤与合并机制

文件移动与改名 ​

shutil.move(source, target) 可以移动文件或目录:

python
from pathlib import Path
import shutil

source_path = Path("reports") / "score.txt"
archive_dir = Path("archive")

shutil.move(source_path, archive_dir)

如果目标是已经存在的目录,源文件会移动到该目录,并保留原文件名。

传入完整目标路径时,可以同时改名:

python
target_path = Path("archive") / "final_score.txt"
shutil.move(source_path, target_path)

目标已经存在时的结果可能受到目标类型和操作系统影响。为了避免意外覆盖或形成额外嵌套目录,移动前应明确检查目标路径。

在同一文件系统中,移动通常可以直接重命名路径;跨文件系统移动时,shutil.move() 可能通过“复制后删除源文件”完成。因此,大文件跨磁盘移动不一定会立即完成,中途失败时也需要检查源和目标状态。

shutil.move 移动机制:同卷指针重命名 vs 跨卷复制删除

递归删除与安全边界 ​

shutil.rmtree(path) 会删除目录以及其中的全部内容:

python
import shutil

shutil.rmtree(backup_dir)

它与 Path 中两个删除方法的范围不同:

方法删除对象
Path.unlink()文件或符号链接
Path.rmdir()空目录
shutil.rmtree()目录及其全部内容

rmtree() 不会进入回收站,通常难以恢复。不要把用户主目录、课程根目录、当前工作目录或来源不明的路径直接传给它。

删除前边界检查:

下面只允许删除临时工作区中的指定备份目录:

python
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)

先验证父目录和目标名称,可以降低变量传错造成的误删风险。真实项目还应该结合业务规则、权限和备份策略做更严格的检查。

删除操作危险等级梯队与 rmtree 三重防御门禁

案例:安全备份与整理 ​

需求: 在不修改只读源目录的前提下,创建备份、复制独立文件、整理目录内容,并在确认边界后清理临时备份。

功能分析: 使用 copytree() 过滤并复制目录,使用 copy2() 保留独立文件元数据,使用 move() 整理文件,使用边界检查和 rmtree() 安全删除备份。

案例提供一份只读源目录:

text
01-course-backup/
├── main.py
└── source_files/
    ├── debug.log
    ├── readme.txt
    ├── data/
    │   └── scores.csv
    └── notes/
        └── lesson.md

按目录树创建源文件。debug.log 用来验证复制时的忽略规则:

text
课程备份案例源文件。
text
这条调试日志应在复制时被忽略。
text
name,score
张三,90
李四,85
text
# 文件备份练习

使用 shutil 复制和移动文件。

程序在临时工作区中完成备份、复制、移动和删除,不会修改 source_files:

python
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()

运行结果:

text
备份内容:
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,不需要先转换成字符串。

练习 ​

  1. 在 TemporaryDirectory() 中使用 copy() 和 copy2() 复制同一个文件,比较结果。
  2. 使用 copytree() 复制示例目录,并忽略 .log 文件。
  3. 在临时工作区中使用 move() 移动文件并修改名称。
  4. 为 remove_backup() 增加检查,拒绝删除不存在的目录。
  5. 修改综合案例,使用 dirs_exist_ok=True 把新增文件合并到已有备份目录。