Python递归复制文件夹:shutil.copytree用法详解
全面讲解Python标准库shutil.copytree函数的使用方法,涵盖基础递归复制、目标目录已存在合并处理、ignore排除规则、符号链接与元数据保留策略及常见错误排查。
在 Python 中批量复制文件夹时,普通的文件复制函数无法自动处理复杂的子目录嵌套结构。标准库内置的 shutil.copytree() 专用于解决这一问题,它能够以递归方式遍历源目录中的所有层级,同步创建对应的目录分支,并将内部的文件及元数据完整克隆到目标路径中。本文将深入拆解其核心参数、冲突覆盖规则、过滤匹配策略与典型避坑要点。
一、核心语法与基础用法(从一个最小示例开始)
shutil 是 Python 内置的高级文件操作模块,无需额外安装即可直接导入。其中,shutil.copytree() 的函数设计目标是“以源目录为模板,在目标位置完整重建该目录树”。
在深入各项进阶参数前,我们首先查看该函数在 Python 官方库中的标准签名:
shutil.copytree(
src,
dst,
symlinks=False,
ignore=None,
copy_function=copy2,
ignore_dangling_symlinks=False,
dirs_exist_ok=False
)
完成一次基础递归复制仅需提供两个核心位置参数:src(源目录路径)与 dst(目标目录路径)。
import shutil
# 定义源目录与目标备份路径
source_dir = "/path/to/source_project"
backup_dir = "/path/to/backup_project"
# 执行递归复制
shutil.copytree(source_dir, backup_dir)
print("目录树复制完成!")
在执行上述最基础的调用时,函数内部会按照深度优先遍历逻辑运作:先在目标位置创建与源目录根同名(或自定义指定)的新文件夹,然后逐层向下探索。若遇到子目录,则自动调用 os.makedirs 创建对齐的路径;若遇到具体文件,则默认调用 shutil.copy2 将文件内容及其元数据(包括最后修改时间、访问时间、文件标记等)写入目标节点。
二、目标目录已存在:dirs_exist_ok 参数的使用与行为差异
初学者使用 shutil.copytree() 最常遇到的报错是 FileExistsError: [Errno 17] File exists。这是因为在默认情况下,函数要求目标目录 dst 必须是一个尚未存在的路径。
1. Python 3.8 带来的重要更新
在 Python 3.8 之前,若目标目录已提前创建,copytree 会直接抛出异常中断执行。当时若要实现增量覆盖或合并,通常需要借助第三方库或手动递归编写 os.walk()。
自 Python 3.8 起,官方引入了 dirs_exist_ok 这一关键参数,默认值为 False。当显式将其指定为 True 时:
- 如果目标目录(或内部子目录)已经存在,函数不会报错,而是允许目录复用,继续向深层执行。
- 对于目标路径中已存在的同名文件,函数将直接采用新文件覆盖旧文件。
- 目标路径中已有但源路径中没有的文件,会被原封不动地保留(即实现“合并/Merge”而非“清空后覆盖”)。
import shutil
from pathlib import Path
source = Path("./release_v2")
destination = Path("/var/www/html")
# 启用 dirs_exist_ok=True 实现平滑合并复制
shutil.copytree(source, destination, dirs_exist_ok=True)
print("静态站点文件已更新合并至目标服务目录。")
如果代码需要兼容 Python 3.7 及更早版本,切勿传入 dirs_exist_ok 参数,否则会抛出 TypeError: copytree() got an unexpected keyword argument 'dirs_exist_ok'。
三、排除特定文件或目录:ignore 回调与 shutil.ignore_patterns
在实际项目备份或文件分发中,很多临时文件和版本控制元数据是不需要复制的,例如 .git 目录、Python 编译缓存 __pycache__、.DS_Store 或 *.log 文件。shutil.copytree() 提供了 ignore 参数来实现过滤。
1. 使用快捷匹配工具:shutil.ignore_patterns
最便捷的方式是使用 shutil.ignore_patterns(*patterns) 工厂函数。该函数支持类似 Unix shell 风格的通配符语法(例如 *、?、[seq])。
import shutil
# 复制整个项目目录,同时排除版本控制文件与字节码缓存
shutil.copytree(
"my_project",
"my_project_clean_backup",
ignore=shutil.ignore_patterns(
"*.pyc",
"*.pyo",
"__pycache__",
".git",
"*.tmp",
".DS_Store"
)
)
2. 自定义过滤函数的工作机制
为了理解 ignore_patterns 的工作原理,必须掌握 ignore 参数的签名要求。ignore 参数接收一个可调用对象(函数),其签名为:
def custom_ignore(dir_path, dir_contents):
# dir_path: 当前正被访问的目录路径(字符串)
# dir_contents: 该目录下所有直接子项的名称列表(即 os.listdir 返回的列表)
# 返回值: 一个包含需要被“忽略(不复制)”的名称集合或列表
return ignored_names
每当 copytree 进入一个新目录时,都会把当前目录的路径以及该目录下的所有子项名称传入此函数。函数计算出哪些名称属于被排除对象,copytree 便会在当前层直接跳过它们。如果被跳过的是一个文件夹,该文件夹及其内部的所有嵌套子项都将直接被剪枝,不再向下遍历。
四、符号链接、文件元数据与底层复制行为定制
除了目录与文件过滤外,文件系统层面的软链接(symlink)和属性状态(stat)在备份和迁移过程中同样至关重要。
1. 符号链接处理:symlinks 参数
symlinks 参数控制是否将符号链接本身作为软链接复制,默认值为 False:
symlinks=False(默认):遇到软链接时,会追踪其指向的真实原始文件,将真实内容复制到目标位置(解除引用)。若真实文件不存在,会引发异常。symlinks=True:在目标位置重新创建一个对等的软链接对象,而不解开链接。在支持软链接的类 Unix 系统中推荐在备份时开启,以避免重复复制大文件或破坏工程架构。
2. 悬空软链接:ignore_dangling_symlinks 参数
当 symlinks=False 时,如果源目录中包含一个指向已被删除文件的软链接(即悬空符号链接 / Dangling Symlink),copytree 会在尝试读取真实文件内容时抛出 FileNotFoundError。如果将 ignore_dangling_symlinks 设置为 True,函数将静默忽略该错误并跳过该项。
3. 元数据保留策略与 copy_function 参数
copytree 默认将 copy_function 设置为 shutil.copy2。与普通的 shutil.copy 相比,copy2 会在复制文件比特流后额外调用 shutil.copystat(),尽可能保留文件的:
- 最后访问时间(atime)与最后修改时间(mtime)。
- POSIX 权限模式(File Mode / Permissions)。
- 部分操作系统支持的拓展属性与安全标志。
若只需要复制内容而不需要保留修改时间(例如触发自动化构建重新编译),可以将 copy_function=shutil.copy 显式传入。
五、常见异常分析与高频避坑清单
在实际脚本化运行中,复制目录通常会跨越不同的磁盘卷或权限上下文,以下是高频发生的四类典型问题与排查指南:
| 异常名称 / 现象 | 常见诱发根因 | 标准排查与修复方案 |
|---|---|---|
FileExistsError |
目标目录 dst 已经存在,且未开启覆盖允许。 |
在 Python 3.8+ 中指定 dirs_exist_ok=True;或提前清理已存在的目标目录。 |
无限嵌套递归 |
将目标目录放置在源目录内部(如 copytree('./data', './data/backup'))。 |
目标目录必须位于源目录树之外,或在 ignore 模式中显式排除 backup 文件夹名。 |
PermissionError |
对源目录中某些文件缺少读权限,或对目标磁盘缺少写权限。 | 检查运行用户的读写权限;在 Windows 下注意文件是否被其他进程独占锁定。 |
shutil.Error |
遍历过程中多个子文件复制报错,copytree 将多项异常聚合抛出。 |
捕获 shutil.Error 并遍历其 args[0] 列表,每项为一个三元组 (src, dst, err_msg)。 |
如果在执行 shutil.copytree('/project', '/project/backup') 时没有在 ignore 中排除 backup,复制进程在扫描 /project 时会把刚创建的 backup 目录当作新发现的子目录继续向下钻取,导致递归深度激增或磁盘被写满。
六、工业级完整实践范例
综合前文的所有知识点,下面给出一个健壮的工业级备份函数封装示例。该示例完整考虑了目标目录已存在、常见临时文件过滤、异常信息捕获以及符号链接配置:
import os
import shutil
from pathlib import Path
def robust_backup(src_path: str, dst_path: str):
"""
健壮的目录树递归复制工具函数
"""
src = Path(src_path).resolve()
dst = Path(dst_path).resolve()
if not src.exists() or not src.is_dir():
raise ValueError(f"源路径不存在或并非目录: {src}")
# 防止无限嵌套复制陷阱
if dst == src or src in dst.parents:
raise ValueError("目标目录不能位于源目录内部,避免无限循环递归!")
print(f"开始备份: {src} -> {dst}")
try:
shutil.copytree(
src,
dst,
symlinks=True, # 类 Unix 环境下推荐保留软链接本身
ignore=shutil.ignore_patterns(
"*.pyc",
"__pycache__",
".git",
".DS_Store",
"*.tmp"
),
dirs_exist_ok=True # 允许目标存在并进行平滑合并覆盖
)
print("目录树复制成功完成!")
except shutil.Error as err:
print("复制过程中部分文件发生错误:")
for src_item, dst_item, msg in err.args[0]:
print(f" 文件: {src_item} -> {dst_item},原因: {msg}")
except PermissionError as err:
print(f"权限不足,无法完成文件读写: {err}")
except Exception as err:
print(f"复制失败,未知错误: {err}")
if __name__ == "__main__":
# 执行备份测试
robust_backup("./source_data", "./backup_data")
总结来说,shutil.copytree 是 Python 中处理复杂目录结构克隆与分发最直接、高效的原生工具。通过掌握 dirs_exist_ok=True 的合并特性,以及结合 shutil.ignore_patterns 的剪枝过滤能力,读者可以在自动化运维、版本打包、数据备份等各类日常开发场景中游刃有余地完成目录复制任务。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













