TheZTHEZ / UNDERSTANDING FIRST
Python 项目进阶30 分钟阅读

Python 文件整理工具:先预览,再移动

按扩展名分类本地文件,默认不修改,并处理同名冲突。

项目介绍

把图片、笔记和代码整理到 images、documents、code、other。文件操作有实际后果,所以默认仅预览;显式 —apply 才移动。请先用复制出来的测试目录,不要直接整理手机整个存储区。

学习目标

  • 使用 pathlib 读取明确指定的目录。
  • 按扩展名分类,跳过目录、隐藏文件与符号链接。
  • 先预览再修改,理解执行边界。
  • 对同名目标添加后缀,避免覆盖。

前置知识与运行环境

需要本地 Python 3 和源、目标目录读写权限。普通个人目录通常不需管理员权限,但安卓应用沙箱与授权可能限制范围。浏览器不能按电脑路径读取手机真实目录;网站运行器最多处理其虚拟文件。

先备份并阅读预览结果。示例假设目录由你控制、没有其他程序同时修改;它不是面向敌对文件系统的安全工具,也没有自动撤销或事务回滚。

前置:Python 函数、条件与循环、文件路径基础。项目涉及的标准库会在下方解释,不要求首次就记住全部接口。

实现步骤

  1. 创建 demo,放入复制来的 main.py、note.txt、photo.jpg。
  2. 建立扩展名到分类的字典,扩展名转小写。
  3. 只处理第一层,不递归进入其他文件夹。
  4. 检查目标不是符号链接或普通文件。
  5. 规划全部动作。同名时添加 _1、_2 等后缀。
  6. 无 —apply 时只打印;有参数时再创建目录与移动。

完整代码

源码 ZIP 的 examples/projects 目录含同一文件。保存完整代码,不要只复制 main。

"""Preview-first local file organizer; only --apply moves files."""
import argparse
import shutil
from pathlib import Path

GROUPS = {
    ".png": "images", ".jpg": "images", ".jpeg": "images",
    ".pdf": "documents", ".txt": "documents", ".md": "documents",
    ".py": "code", ".c": "code", ".js": "code",
}

def plan_moves(folder):
    reserved = set()
    moves = []
    for source in sorted(folder.iterdir()):
        if (not source.is_file() or source.is_symlink()
                or source.name.startswith(".")):
            continue
        group = GROUPS.get(source.suffix.lower(), "other")
        directory = folder / group
        if directory.is_symlink() or (directory.exists() and not directory.is_dir()):
            raise ValueError(f"目标目录不安全或不是文件夹:{directory.name}")
        target = directory / source.name
        index = 1
        while target.exists() or target in reserved:
            target = directory / f"{source.stem}_{index}{source.suffix}"
            index += 1
        reserved.add(target)
        moves.append((source, target))
    return moves

def main():
    parser = argparse.ArgumentParser(description="按扩展名整理文件;默认仅预览")
    parser.add_argument("folder", type=Path)
    parser.add_argument("--apply", action="store_true", help="实际移动文件")
    args = parser.parse_args()
    try:
        folder = args.folder.expanduser().resolve()
        if not folder.is_dir():
            raise ValueError("请选择已有文件夹")
        moves = plan_moves(folder)
        for source, target in moves:
            print(f"{source.name} -> {target.relative_to(folder)}")
            if args.apply:
                target.parent.mkdir(exist_ok=True)
                # Trusted personal folder; no concurrent writers assumed.
                if target.exists():
                    raise ValueError("目标文件已出现,请重新预览后再执行")
                shutil.move(str(source), str(target))
        print(f"{'已移动' if args.apply else '预览'} {len(moves)} 个文件")
    except (OSError, ValueError) as error:
        parser.exit(1, f"错误:{error}\n")

if __name__ == "__main__":
    main()

代码解释

  • plan_moves 返回 source、target 对,分开规划与执行,方便测试。
  • reserved 记录本轮已预订目标,exists 同时检查磁盘上已有文件。
  • stem 是主名,suffix 是扩展名,冲突时只改变主名。
  • —apply 默认 False,因此第一次不创建目录、不移动文件。
  • 移动前再次检查目标是否出现,遇到并发变化停止,避免覆盖。
  • 分类仅看扩展名,不分析文件内容。改名为 .txt 的图片仍被当作文档。

运行方法与验证

在独立练习目录运行:

python file-organizer.py ./demo
python file-organizer.py ./demo --apply

样例数据的查看或预览结果为:

main.py -> code/main.py
note.txt -> documents/note.txt
photo.jpg -> images/photo.jpg
预览 3 个文件

完整代码已在临时目录核对,文件整理的预览、真实移动与冲突分别检查。实际操作时仍需验证文件,不只看成功提示。

常见错误

  • 路径不存在:核对工作目录,含空格的路径加引号。
  • 目标是文件或符号链接:程序会拒绝,先在练习目录处理。
  • 途中失败:检查已完成的移动;示例无法自动恢复之前步骤。

拓展方向

增加日志后再考虑恢复脚本;允许配置扩展名;增加递归前先设计排除范围。始终保持默认预览,对冲突、空目录与权限失败分别测试。

这一节,你理解到哪里了?

这是你的自我反馈,与练习估计、课程进度分别记录。

本文为TheZ原创教学内容。正文采用 CC BY-NC-SA 4.0,代码采用 MIT 许可。请尊重署名与授权范围。