☰
Python实现文件自动分类的操作步骤
2026/10/16 15:15:58 网站建设 项目流程

前言


"文件自动分类"要解决的是一个很朴素的痛点:下载目录、桌面、微信文件夹里堆了几百个文件,想按类型(图片、文档、压缩包、音视频)自动归到不同子目录里。这件事用 Python 做只需要几十行,但能跑通和敢在真实目录上跑是两码事——前者是几行for循环,后者要考虑重名覆盖、移动失败、误伤隐藏文件这些问题。


本文先把分类逻辑拆成"扫描 → 判定 → 目标路径 → 移动"四步,再给出一份完整的、可以先空跑(dry-run)确认再真正执行的脚本。核心依赖是标准库里的pathlib和shutil,不装任何第三方包。


先明确一个边界:本文只讨论按扩展名分类。按文件内容(比如判断一个 PDF 是合同还是论文)需要读内容甚至上模型,那属于另一个话题。


一、四步拆解


第一步:扫描。用Path.iterdir()只列当前一层,不递归。注意它会连隐藏文件、目录一起列出来,需要自己筛。第二步:判定类别。取文件后缀.suffix(如.jpg),转小写后查一张映射表。第三步:算出目标路径。目标目录 = 源目录 / 类别名;若文件已在目标目录里,跳过,否则会死循环。第四步:移动。用shutil.move(src, dst),它跨磁盘分区时自动退化为"先复制再删除",比os.rename()更安全——os.rename()跨分区会直接抛OSError。


关于扩展名,有一个必须记住的细节:Path.suffix返回的是最后一个点之后的部分,archive.tar.gz的.suffix是".gz",要拿全部后缀得用.suffixes(返回列表)。分类时按最后一个后缀通常够用。


二、类别映射与判定


# 适用于 Python 3.8+
from pathlib import Path

CATEGORIES = {
"图片": {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"},
"文档": {".txt", ".md", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"},
"压缩包": {".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"},
"音视频": {".mp3", ".wav", ".flac", ".mp4", ".mkv", ".mov", ".avi"},
"代码": {".py", ".js", ".ts", ".html", ".css", ".json", ".yaml", ".yml"},
}


def classify(path):
"""根据后缀返回类别名,没匹配上就归到 '其他'。"""
ext = path.suffix.lower()
for name, exts in CATEGORIES.items():
if ext in exts:
return name
return "其他"

用set而不是list存后缀,判定是 O(1);其实对几百个文件差别可以忽略,但写成集合语义更贴切。path.suffix.lower()必须转小写,否则.JPG和.jpg会被分到两类。


三、处理重名:不要静默覆盖


如果目标目录里已经有同名文件,shutil.move的行为取决于os.rename()的语义——官方文档的原话是"若dst已存在但不是目录,它可能被覆盖,取决于os.rename()的语义"。这意味着在 POSIX 上它默认静默覆盖,在 Windows 上则倾向于抛FileExistsError。两种结果都不是"整理文件"想要的,而且行为随平台漂移,所以必须自己加一层防冲突逻辑:检测到重名就追加_1、_2。


# 适用于 Python 3.8+
def unique_path(dst):
"""dst 已存在时,返回带序号的新路径:a.txt -> a_1.txt -> a_2.txt"""
if not dst.exists():
return dst
stem, suffix, parent = dst.stem, dst.suffix, dst.parent
i = 1
while True:
candidate = parent / f"{stem}_{i}{suffix}"
if not candidate.exists():
return candidate
i += 1

四、完整脚本(含 dry-run)


把上面的碎片拼成一个可执行的脚本。它默认只打印将要做什么,加--apply才真正移动。


# 适用于 Python 3.8+
import argparse
import shutil
from pathlib import Path

CATEGORIES = {
"图片": {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"},
"文档": {".txt", ".md", ".pdf", ".docx", ".xlsx"},
"压缩包": {".zip", ".rar", ".7z", ".tar", ".gz"},
}


def classify(path):
ext = path.suffix.lower()
for name, exts in CATEGORIES.items():
if ext in exts:
return name
return "其他"


def unique_path(dst):
if not dst.exists():
return dst
stem, suffix, parent = dst.stem, dst.suffix, dst.parent
i = 1
while (parent / f"{stem}_{i}{suffix}").exists():
i += 1
return parent / f"{stem}_{i}{suffix}"


def organize(root, apply=False):
root = Path(root)
moved, skipped = 0, 0
for item in root.iterdir():
if not item.is_file() or item.name.startswith("."):
continue # 跳过目录、隐藏文件(含 .DS_Store 之类)
category = classify(item)
target_dir = root / category
if item.parent == target_dir:
continue # 已经在目标目录里,避免自我移动
dst = unique_path(target_dir / item.name)
if apply:
target_dir.mkdir(exist_ok=True)
shutil.move(str(item), str(dst))
moved += 1
else:
print(f"[dry-run] {item.name} -> {category}/{dst.name}")
print(f"共处理 {moved} 个文件,其余未匹配或已就位")


if __name__ == "__main__":
ap = argparse.ArgumentParser(description="按扩展名整理目录")
ap.add_argument("root", help="要整理的目录")
ap.add_argument("--apply", action="store_true", help="真正执行移动")
args = ap.parse_args()
organize(args.root, apply=args.apply)

几点值得说明。shutil.move(src, dst)从Python 3.9 起接受路径类对象(path-like object),所以在 3.9+ 上直接传Path也可以;为了兼容 3.8 及更早版本,写成str(dst)更保险。mkdir(exist_ok=True)只会创建一层,如果类别目录本身是嵌套的(比如图片/2024),要用parents=True。item.is_file()会跟随符号链接判断,如果不希望跟链接,用item.is_symlink()先排除。




候选函数行为适合度



os.rename()同分区改名,跨分区抛OSError只在确定同盘时用

shutil.move()跨分区自动退化为复制+删除推荐

shutil.copy2()复制并保留元数据,源文件还在想保留原文件时

Path.rename()底层就是os.rename(),同样有跨盘限制同分区改名



常见坑点



  1. ❌ 边遍历目录边往里移动文件,导致重复处理


✅ 先list(root.iterdir())固化文件列表再操作,并跳过已经处于目标目录内的项



  1. ❌ 直接shutil.move同名文件,以为行为一定一致


✅ 文档说"是否覆盖取决于os.rename()语义":POSIX 上可能静默覆盖,Windows 上可能抛FileExistsError;整理前统一做重名检测并生成_1、_2后缀,别依赖平台差异



  1. ❌ 用path.suffix去解析archive.tar.gz,以为拿到.tar.gz


✅.suffix只给最后一个".gz";要全部后缀用path.suffixes



  1. ❌ 忘了.lower(),.JPG和.jpg分进两类


✅ 一律先path.suffix.lower()再比对



  1. ❌ 把隐藏文件和系统文件(.DS_Store、Thumbs.db)也一起归类


✅ 用name.startswith(".")跳过,必要时再维护一张黑名单



  1. ❌ 用os.rename()把文件从 C 盘移到 D 盘


✅ 跨分区会抛OSError: [WinError 17],改用shutil.move()



  1. ❌ 脚本直接在真实目录上第一次就执行移动


✅ 先跑 dry-run 打印计划,确认无误再加--apply;重要目录先备份



  1. ❌ 手工拼路径字符串root + "/" + name


✅ 用Path的/运算符拼接,它会按平台选择分隔符


总结




步骤用什么关键点



扫描Path.iterdir()只列一层,筛掉目录与隐藏文件

判定path.suffix.lower()后缀映射用集合

定目标重名检测避免静默覆盖丢数据

移动shutil.move()跨分区安全



自动分类的难点从来不在分类算法,而在"移动"这个不可逆动作上的防护。把 dry-run、重名检测、跳过隐藏文件这三件事做进去,几十行的脚本就能从"玩具"变成"敢用"。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询