一个脚本把乱成一锅粥的文件夹按扩展名分类

2026-10-06 · 本脚本已实跑验证(10 个脏文件全部正确归位,重名不覆盖,重跑幂等)

下载文件夹用半年是什么样:photo (1).jpg、合同 最终版.docx、工资表.xlsx、存档.zip 混在一起,翻找全靠搜索。Windows 11 有个"按类型分组"但只是视图不搬文件;这里用纯标准库 Python(shutil + os,零依赖)真正把文件移进 图片/ 文档/ 表格/ PDF/ 压缩包/ 子目录。

脚本(实跑通过)

# -*- coding: utf-8 -*-
"""文件夹按扩展名自动分类:图片/文档/表格/PDF/压缩包/其他。"""
import os, shutil, sys

RULES = {
    "图片": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"},
    "文档": {".doc", ".docx", ".txt", ".md", ".rtf"},
    "表格": {".xls", ".xlsx", ".csv"},
    "PDF": {".pdf"},
    "压缩包": {".zip", ".rar", ".7z", ".tar", ".gz"},
    "PPT": {".ppt", ".pptx"},
}
def classify(ext):
    for cat, exts in RULES.items():
        if ext in exts:
            return cat
    return "其他"

def main(folder):
    moved, report = 0, []
    for root, dirs, files in os.walk(folder):
        if root != folder:
            continue  # 只处理顶层,已分类的子目录不再翻
        for fn in sorted(files):
            ext = os.path.splitext(fn)[1].lower()
            cat = classify(ext)
            cat_dir = os.path.join(folder, cat)
            os.makedirs(cat_dir, exist_ok=True)
            dst = os.path.join(cat_dir, fn)
            if os.path.exists(dst):          # 重名防覆盖:加后缀
                base, e = os.path.splitext(fn)
                i = 1
                while os.path.exists(dst):
                    dst = os.path.join(cat_dir, f"{base}_{i}{e}")
                    i += 1
                report.append(f"[重名] {fn} -> {os.path.basename(dst)}")
            if os.path.abspath(dst) != os.path.abspath(os.path.join(folder, fn)):
                shutil.move(os.path.join(folder, fn), dst)
                moved += 1
                report.append(f"[OK] {fn} -> {cat}/")
    print(f"=== 分类完成,共移动 {moved} 个文件 ===")
    print("\n".join(report))

if __name__ == "__main__":
    main(sys.argv[1])

实跑结果

测试一:10 个脏文件全归位

测试数据故意选了最难伺候的一批:photo (1).jpg(空格+括号)、合同 最终版.docx(中文+空格)、截图 2026-10-06.png、无扩展名文件(没有扩展名)、data.csv 等 10 个。

=== 分类完成,共移动 10 个文件 ===
[OK] data.csv -> 表格/
[OK] notes.md -> 文档/
[OK] photo (1).jpg -> 图片/
[OK] readme.txt -> 文档/
[OK] 合同 最终版.docx -> 文档/
[OK] 存档.zip -> 压缩包/
[OK] 工资表.xlsx -> 表格/
[OK] 截图 2026-10-06.png -> 图片/
[OK] 报告.pdf -> PDF/
[OK] 无扩展名文件 -> 其他/

ls -R 复核:6 个子目录,10 个文件各归各位,一个不少。

测试二:重名冲突(防覆盖验证)

故意在顶层放 a.txt,同时 文档/ 里已有旧的 a.txt:

[重名] a.txt -> a_1.txt
[OK] a.txt -> 文档/
文档/: a.txt  a_1.txt    # 两个都在,旧文件未被覆盖

如果这里图省事直接 shutil.move 不检查,旧 a.txt 会被静默覆盖——新文件是下载的还行,万一旧文件是你改了三天的文档,就是数据事故。这是本脚本最重要的一行。

测试三:重跑幂等

对已整理好的目录再跑一遍:共移动 0 个文件,无报错无副作用。随手双击重跑也不怕。

实跑复现的坑

坑1:os.walk 不限顶层 = 整理完又翻出来

第一直觉是 os.walk(folder) 全遍历,但分类子目录建好后 walk 会走到 图片/、文档/ 里去,把刚归好位的文件再分类一遍(文档/ 里的 .txt 又会被"移到" 文档/,好在这步因路径相同被跳过;但嵌套目录场景就会出现二次搬家和 图片/图片/ 嵌套)。必须 if root != folder: continue 只处理顶层。

坑2:重名覆盖是静默数据事故

shutil.move 在目标已存在时的行为是覆盖(同文件系统)。下载的 a.txt 覆盖掉文档里的 a.txt,没有任何提示。解法:目标存在就循环找 a_1.txt、a_2.txt,并把改名决定写进报告让用户事后可查。

坑3:无扩展名文件的归属

os.path.splitext("无扩展名文件") 返回 ("无扩展名文件", ""),空扩展名匹配不到任何规则,归入"其他/"兜底——比报错中断整批、或丢在原地假装无事发生都好。想更智能可以再用 file 命令 / python-magic 嗅探内容,但对下载目录场景扩展名兜底已够用。

坑4:扩展名统一小写

.JPG、Pdf.PDF 这种大写扩展名很常见,比较前必须 .lower(),否则全进"其他/"。

需要现成的?

文件整理、批量改名、重复文件清理这些桌面杂活,喵窝都提供脚本定制服务(¥9.9 起):说出你的文件夹乱法和想要的结果,交付可复用脚本 + 使用说明,包免费修复(虚拟物品交付后不退款;未达承诺免费重做)。
→ 去喵窝资源铺看服务