【Python自动化】写了个文件整理脚本,帮我从1000个杂文件中秒级归档(附源码)
从零写一个Python文件批量整理器:自动按类型归档桌面文件
专栏:《从零写一个小工具》 | 语言:Python | 依赖:纯标准库
一、场景介绍
公司发的报表、浏览器下的安装包、临时截图、各种格式的文档……几个月不收拾,桌面和下载文件夹就能堆成山。手动拖文件分类?太费时间,而且拖不了几次就放弃了。
这篇文章带你从零写一个小工具,把指定文件夹里的文件按类型自动归类,图片进图片夹,文档进文档夹,可执行文件单独放,整理完还给你输出一份统计报告。代码不到100行,拷走就能跑。
二、需求说明
这个小工具要解决这几个实际问题:
- 指定一个待整理目录(比如桌面或下载文件夹)
- 按文件扩展名自动分类:
.jpg/.png归图片,.docx/.pdf归文档,.exe/.msi归安装包,等等 - 如果目标分类文件夹不存在,自动创建
- 遇到同名文件不要覆盖,自动重命名保留
- 整理完打印一份统计报告:每类多少文件,总共多少
不需要装任何第三方库,只用 Python 自带的 os、shutil、pathlib。
三、环境准备
Python 3.7+ 就行,不用装任何东西。打开终端确认一下:
python --version
四、逐段代码编写
4.1 定义文件类型映射
先把扩展名和分类文件夹对应起来。这是整个工具的核心规则,你可以按自己习惯改:
CATEGORY_MAP = {
"图片": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"],
"文档": [".doc", ".docx", ".pdf", ".txt", ".md", ".xls", ".xlsx", ".ppt", ".pptx", ".csv"],
"视频": [".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv"],
"音频": [".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a"],
"压缩包": [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"],
"安装包": [".exe", ".msi", ".dmg", ".pkg", ".deb", ".rpm"],
"代码": [".py", ".js", ".html", ".css", ".java", ".c", ".cpp", ".go", ".rs", ".json", ".xml", ".yaml", ".yml"],
}
这里有个设计细节:用一个扩展名到分类的反向字典,后续查找更快,不用每次遍历:
EXT_TO_CATEGORY = {}
for category, exts in CATEGORY_MAP.items():
for ext in exts:
EXT_TO_CATEGORY[ext.lower()] = category
为什么要转小写? Windows 上扩展名可能是 .PDF 或 .pdf,统一转小写再匹配,避免漏网之鱼。
4.2 获取分类文件夹名称
传入一个文件路径,返回它该去哪个分类文件夹:
from pathlib import Path
def get_category(file_path: Path) -> str:
"""根据扩展名返回分类文件夹名称"""
ext = file_path.suffix.lower() # 获取扩展名并转小写
return EXT_TO_CATEGORY.get(ext, "其他") # 未知类型丢进"其他"
Path.suffix 是 pathlib 的方法,直接拿到带点的扩展名,比如 .jpg,比用字符串分割干净多了。
4.3 处理重名文件
同一个分类里可能已经有一个 报告.pdf,再移一个 报告.pdf 过来,默认会覆盖。我们要保留两份:
def safe_target_path(target_dir: Path, original_name: str) -> Path:
"""如果目标路径已存在,自动加序号重命名"""
target = target_dir / original_name
if not target.exists():
return target
# 有重名,开始加序号
stem = Path(original_name).stem # 文件名(不含扩展名)
suffix = Path(original_name).suffix # 扩展名
counter = 1
while True:
new_name = f"{stem}_{counter}{suffix}"
candidate = target_dir / new_name
if not candidate.exists():
return candidate
counter += 1
这里用 stem 和 suffix 拆分原文件名,序号从 1 开始递增,直到找到不冲突的名字。逻辑简单粗暴,但稳。
4.4 核心整理逻辑
遍历源目录,逐个文件判断类型、创建目标文件夹、移动文件:
def organize_folder(source_dir: str):
"""执行整理"""
source = Path(source_dir).expanduser().resolve() # 支持 ~/Desktop 这种写法
if not source.exists():
print(f"目录不存在: {source}")
return
stats = {} # 统计每类文件数量
# 只处理直接子文件,不递归进子文件夹
for item in source.iterdir():
if not item.is_file():
continue # 跳过子文件夹
category = get_category(item)
target_dir = source / category
target_dir.mkdir(exist_ok=True) # 分类文件夹不存在就创建
target_path = safe_target_path(target_dir, item.name)
try:
item.rename(target_path) # 移动文件
stats[category] = stats.get(category, 0) + 1
print(f"[移动] {item.name} -> {category}/")
except Exception as e:
print(f"[失败] {item.name}: {e}")
# 打印统计报告
print("\n" + "=" * 30)
print("整理完成,统计如下:")
total = 0
for category, count in sorted(stats.items()):
print(f" {category}: {count} 个文件")
total += count
print(f" 总计: {total} 个文件")
print("=" * 30)
几个关键选择:
- 用
Path.rename()做移动,同一盘符下是瞬时操作,没有拷贝开销 expanduser()让~/Desktop这种路径能正常解析resolve()把路径转成绝对路径,避免相对路径带来的意外- 只处理直接子文件(
iterdir()),不递归进子文件夹——这是故意的,防止把已经有层级的目录结构打烂。如果你想递归,把is_file()的判断逻辑改一下就行,后面拓展部分会说
4.5 入口与用法
if __name__ == "__main__":
import sys
# 支持命令行传路径,没传就默认整理当前目录
target = sys.argv[1] if len(sys.argv) > 1 else "."
print(f"开始整理目录: {Path(target).resolve()}\n")
organize_folder(target)
五、完整源码
把上面所有片段拼起来,就是一份可直接保存运行的完整脚本:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文件批量整理器
按扩展名自动分类归档指定目录下的文件
"""
import sys
from pathlib import Path
# ========== 配置区:按需修改 ==========
CATEGORY_MAP = {
"图片": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"],
"文档": [".doc", ".docx", ".pdf", ".txt", ".md", ".xls", ".xlsx", ".ppt", ".pptx", ".csv"],
"视频": [".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv"],
"音频": [".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a"],
"压缩包": [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"],
"安装包": [".exe", ".msi", ".dmg", ".pkg", ".deb", ".rpm"],
"代码": [".py", ".js", ".html", ".css", ".java", ".c", ".cpp", ".go", ".rs", ".json", ".xml", ".yaml", ".yml"],
}
# 构建扩展名 -> 分类的反向映射
EXT_TO_CATEGORY = {}
for category, exts in CATEGORY_MAP.items():
for ext in exts:
EXT_TO_CATEGORY[ext.lower()] = category
def get_category(file_path: Path) -> str:
"""根据扩展名返回分类文件夹名称,未知类型归到"其他""""
ext = file_path.suffix.lower()
return EXT_TO_CATEGORY.get(ext, "其他")
def safe_target_path(target_dir: Path, original_name: str) -> Path:
"""如果目标路径已存在,自动加序号重命名,避免覆盖"""
target = target_dir / original_name
if not target.exists():
return target
stem = Path(original_name).stem
suffix = Path(original_name).suffix
counter = 1
while True:
new_name = f"{stem}_{counter}{suffix}"
candidate = target_dir / new_name
if not candidate.exists():
return candidate
counter += 1
def organize_folder(source_dir: str):
"""整理指定目录下的文件"""
source = Path(source_dir).expanduser().resolve()
if not source.exists():
print(f"目录不存在: {source}")
return
stats = {}
for item in source.iterdir():
if not item.is_file():
continue
category = get_category(item)
target_dir = source / category
target_dir.mkdir(exist_ok=True)
target_path = safe_target_path(target_dir, item.name)
try:
item.rename(target_path)
stats[category] = stats.get(category, 0) + 1
print(f"[移动] {item.name} -> {category}/")
except Exception as e:
print(f"[失败] {item.name}: {e}")
# 统计报告
print("\n" + "=" * 30)
print("整理完成,统计如下:")
total = 0
for category, count in sorted(stats.items()):
print(f" {category}: {count} 个文件")
total += count
print(f" 总计: {total} 个文件")
print("=" * 30)
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else "."
print(f"开始整理目录: {Path(target).resolve()}\n")
organize_folder(target)
保存为 file_organizer.py。
六、运行测试
先准备几个测试文件:
mkdir test_folder
cd test_folder
touch report.pdf photo.jpg song.mp3 script.py archive.zip unknown.dat
运行:
python file_organizer.py ./test_folder
你会看到类似输出:
开始整理目录: D:\workspace\test_folder
[移动] archive.zip -> 压缩包/
[移动] photo.jpg -> 图片/
[移动] report.pdf -> 文档/
[移动] script.py -> 代码/
[移动] song.mp3 -> 音频/
[移动] unknown.dat -> 其他/
==============================
整理完成,统计如下:
代码: 1 个文件
压缩包: 1 个文件
图片: 1 个文件
文档: 1 个文件
音频: 1 个文件
其他: 1 个文件
总计: 6 个文件
==============================
目录结构变成:
test_folder/
├── 图片/
│ └── photo.jpg
├── 文档/
│ └── report.pdf
├── 音频/
│ └── song.mp3
├── 代码/
│ └── script.py
├── 压缩包/
│ └── archive.zip
└── 其他/
└── unknown.dat
七、功能拓展
到这儿工具已经能用了,但还可以加几个实用功能:
7.1 递归整理子文件夹
把 if not item.is_file(): continue 改成递归逻辑,可以把嵌套在子文件夹里的文件也捞出来统一分类:
def organize_folder(source_dir: str, recursive: bool = False):
# ...
items = source.rglob("*") if recursive else source.iterdir()
for item in items:
if not item.is_file():
continue
# ... 后续逻辑不变
注意:递归整理会把子文件夹里的文件全抽出来,原来的子文件夹会成空壳。可以顺手清理空文件夹:
# 整理完后清理空文件夹
for subdir in source.iterdir():
if subdir.is_dir() and subdir.name not in CATEGORY_MAP and subdir.name != "其他":
try:
subdir.rmdir() # 只删空文件夹
except OSError:
pass # 非空就跳过
7.2 按修改日期归档
如果你不是想按类型分,而是想按"几月份"分,把 get_category 改成读文件修改时间就行:
import time
def get_category_by_date(file_path: Path) -> str:
mtime = file_path.stat().st_mtime
return time.strftime("%Y-%m", time.localtime(mtime)) # 返回 "2025-05" 这种格式
7.3 加个 “只预览不执行” 的开关
整理前先看一眼它会动哪些文件,心里有个底:
def organize_folder(source_dir: str, dry_run: bool = False):
# ...
action = "[预览]" if dry_run else "[移动]"
# rename 之前判断 if not dry_run:
命令行传 --dry-run 就只看不动手。
八、关键要点总结
- 路径处理用
pathlib,比字符串拼接可靠得多,suffix、stem、rename()都是原生支持的 - 扩展名匹配转小写,Windows 上大小写不敏感,不转小写会漏掉
.PDF这种 - 重名处理必须做,不然批量整理时很容易误覆盖,加序号是最稳妥的方案
rename()在同盘符下是移动,跨盘符才会真正拷贝+删除,整理桌面/下载文件夹一般都在 C 盘,性能没问题- 默认不递归是保护机制,避免把已有目录结构打散,需要递归时自己打开开关
以上就是这个小工具的完整实现。代码保存下来,设个定时任务每月跑一遍,桌面再也不会乱了。
更多推荐

所有评论(0)