Administrator
Administrator
发布于 2026-10-31 / 0 阅读
0
0

Python 从入门到初级(17):文件整理实战:先预览,再按类型复制

本篇是《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;增加子目录,预期不会递归处理它。

如果已经执行过复制,先使用另一份全新的目标目录做下一次实验。能够预览、拒绝覆盖并解释部分失败的结果,比“所有文件看起来都移动了”更适合初次自动化。


上一篇:SQLite 数据库:保存订单与参数化查询 · 系列目录 · 下一篇:命令行与 logging:把脚本做成工具


评论