本篇是《Python 从入门到初级》第 17 / 20 篇。示例使用 Python 3.12 兼容语法,完整目录结构与数据见配套源码包。
把下载目录中的文件分到图片、数据、文档目录,是很常见的自动化需求。第一次练习应该先看到将发生什么,再执行明确的复制,避免一运行就改动一整片真实目录。

1. 先把需求缩小到可检查的范围
本例只处理示例目录的 inbox 直接包含的普通文件,不递归子目录,不删除或移动原文件,不跟随源文件符号链接。按扩展名分类,不检测文件内部的真实格式。
教学 inbox 中有 readme.txt、sample.csv、settings.json 三个文件。预期一个进入 documents,两个进入 data。
把“选择哪些文件、按什么规则分类、同名怎么办、失败后保留什么”先写清楚,比先写 shutil 调用更重要。
2. 完整示例:默认只预览
运行 python lesson17.py:
import shutil
from pathlib import Path
def category_for(path):
suffix = Path(path).suffix.lower()
if suffix in {".png", ".jpg", ".jpeg"}:
return "images"
if suffix in {".csv", ".json"}:
return "data"
if suffix in {".txt", ".md"}:
return "documents"
return "others"
def build_plan(source, destination):
source = Path(source).resolve(strict=True)
destination = Path(destination).resolve()
if not source.is_dir():
raise ValueError("源路径必须是目录")
if destination == source or source in destination.parents:
raise ValueError("目标不能放在源目录内部")
plan = []
for path in sorted(source.iterdir()):
if path.is_symlink() or not path.is_file():
continue
target = destination / category_for(path) / path.name
if not target.parent.resolve().is_relative_to(destination):
raise ValueError("目标子目录指向了目标范围以外")
if target.exists() or target.is_symlink():
raise FileExistsError(target)
plan.append((path, target))
return plan
def apply_plan(plan):
for source, target in plan:
target.parent.mkdir(parents=True, exist_ok=True)
with source.open("rb") as reader, target.open("xb") as writer:
shutil.copyfileobj(reader, writer)
def main():
base = Path(__file__).resolve().parent
plan = build_plan(base / "inbox", base / "output" / "sorted")
for source, target in plan:
print(f"计划复制:{source.name} -> {target.parent.name}/{target.name}")
print(f"共 {len(plan)} 项;当前仅预览,没有复制。")
# 确认练习目录和目标后,才取消下一行的注释。
# apply_plan(plan)
if __name__ == "__main__":
main()
默认输出三条计划,并明确显示“当前仅预览,没有复制”。这时 output/sorted 不应因为预览而被创建。
输出目录在 inbox 之外。程序也会拒绝把目标放进源目录内部,避免后续扩展递归扫描时把自己产生的文件再次处理。
3. 为什么拆成分类、计划和执行
category_for() 只根据后缀判断分类,便于单独测试;build_plan() 查找文件并形成 source、target 对;apply_plan() 才真正写入。
这个结构可以把同一份计划用于终端预览、图形界面确认和实际执行,不必在扫描的同时就做不可见的写入。
pathlib 用于路径,shutil.copyfileobj 用于复制二进制内容,参考 shutil 文档。本例复制内容,不完整复制所有文件元数据和权限。
4. 确认后怎样执行
先检查打印出的源文件和目标目录确实属于练习,再把 main 里的 # apply_plan(plan) 取消注释,重新运行。
预期源目录仍有三个文件,目标目录出现三份副本,内容分别一致。重复执行时,因为同名目标已存在,程序拒绝覆盖,而不是静默替换。
目标以 xb 模式打开,意味着排他创建二进制文件。它能防止直接覆盖已存在文件,但本例不是批量事务:中途失败时,已经复制成功的项目仍然保留,失败文件也可能留下部分内容,需要根据错误检查。
5. 路径检查解决什么、没有解决什么
计划中会检查目标子目录解析后是否仍位于指定范围,避免已存在的子目录链接把结果指到其他位置。
不过这仍是自用练习工具,不是面向不可信用户、恶意并发改路径或复杂权限环境的文件服务。计划生成后文件也可能变化,真实自动化需要按场景补充一致性与失败恢复策略。
不要一开始把 source 改成整个家目录、系统目录或服务器数据目录。先在小型教学目录中验证分类和碰撞处理,再扩大受控范围。
6. 练习与参考结果
向 inbox 增加 PHOTO.JPG,预期按小写化后的扩展名归到 images。增加 archive.bin,预期归到 others;增加子目录,预期不会递归处理它。
如果已经执行过复制,先使用另一份全新的目标目录做下一次实验。能够预览、拒绝覆盖并解释部分失败的结果,比“所有文件看起来都移动了”更适合初次自动化。