跳到主要内容

文件夹整理

收藏0

整理文件夹内散落的文件,按文件类型或修改时间自动归类到子文件夹,默认先预览、确认后执行,不覆盖同名文件。

让 AI 安装

Mercury最新版本 0.1.0更新于 2026-11-02下载 0 次

scripts/organize.py

6.7 KB
1#!/usr/bin/env python3
2"""
3organize.py — 整理文件夹内的文件
4
5默认行为(安全):
6 - 仅扫描目标文件夹顶层的“文件”,不移动子文件夹,不递归;
7 - 按扩展名归类到中文分类子文件夹(图片/文档/表格/...);
8 - 默认只预览(dry-run),必须加 --execute 才真正移动;
9 - 跳过隐藏文件(以 . 开头);目标已存在同名文件时自动加序号,不覆盖。
10
11常用用法:
12 python organize.py "D:\\下载" # 预览整理计划,不动任何文件
13 python organize.py "D:\\下载" --execute # 确认后真正执行(移动)
14 python organize.py "D:\\下载" --by-date # 改为按修改时间 年-月 分类
15 python organize.py "D:\\下载" --copy # 复制而非移动
16 python organize.py "D:\\下载" --recursive # 递归处理子文件夹中的文件
17"""
18
19import argparse
20import shutil
21import sys
22from datetime import datetime
23from pathlib import Path
24
25# 分类文件夹名 -> 扩展名集合(全部小写)
26CATEGORY_MAP = {
27 "图片": {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg",
28 ".heic", ".heif", ".tiff", ".tif", ".ico", ".raw"},
29 "文档": {".pdf", ".doc", ".docx", ".txt", ".md", ".rtf", ".odt",
30 ".wps", ".tex", ".epub"},
31 "表格": {".xls", ".xlsx", ".csv", ".ods", ".et"},
32 "演示文稿": {".ppt", ".pptx", ".key", ".odp"},
33 "音频": {".mp3", ".wav", ".flac", ".aac", ".ogg", ".wma", ".m4a", ".ape"},
34 "视频": {".mp4", ".mkv", ".avi", ".mov", ".wmv", ".flv", ".webm",
35 ".m4v", ".mpg", ".mpeg"},
36 "压缩包": {".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz"},
37 "程序": {".exe", ".msi", ".bat", ".cmd", ".ps1", ".sh", ".com", ".dll"},
38 "代码": {".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css", ".json",
39 ".xml", ".yaml", ".yml", ".java", ".c", ".cpp", ".h", ".go",
40 ".rs", ".php", ".sql", ".vue"},
41}
42DEFAULT_CATEGORY = "其他"
43CATEGORY_DIRS = set(CATEGORY_MAP.keys()) | {DEFAULT_CATEGORY}
44
45
46def classify(file_path: Path) -> str:
47 """按扩展名返回分类文件夹名。"""
48 ext = file_path.suffix.lower()
49 for category, exts in CATEGORY_MAP.items():
50 if ext in exts:
51 return category
52 return DEFAULT_CATEGORY
53
54
55def target_dir_for(file_path: Path, root: Path, by_date: bool) -> Path:
56 """决定单个文件应进入的子文件夹。"""
57 if by_date:
58 # 取文件修改时间的 年-月
59 ts = datetime.fromtimestamp(file_path.stat().st_mtime)
60 return root / ts.strftime("%Y-%m")
61 return root / classify(file_path)
62
63
64def unique_destination(dest: Path) -> Path:
65 """目标已存在时,在文件名后追加 _1、_2…,避免覆盖。"""
66 if not dest.exists():
67 return dest
68 stem, suffix, parent = dest.stem, dest.suffix, dest.parent
69 i = 1
70 while True:
71 candidate = parent / f"{stem}_{i}{suffix}"
72 if not candidate.exists():
73 return candidate
74 i += 1
75
76
77def is_protected(path: Path) -> bool:
78 """拦截对驱动器根、Windows 系统目录、用户主目录等危险位置的整理。"""
79 resolved = path.resolve()
80 if resolved.parent == resolved: # 驱动器根,如 C:\\
81 return True
82 protected_names = {"windows", "system32", "program files",
83 "program files (x86)"}
84 parts = {p.lower() for p in resolved.parts}
85 if parts & protected_names:
86 return True
87 if resolved == Path.home():
88 return True
89 return False
90
91
92def collect_files(root: Path, recursive: bool) -> list[Path]:
93 """收集待整理的文件,跳过隐藏文件与已生成的分类子目录。"""
94 files = []
95 if recursive:
96 for p in root.rglob("*"):
97 if not p.is_file():
98 continue
99 # 路径中任一级是分类文件夹则视为已整理,跳过
100 rel_parts = p.relative_to(root).parts
101 if any(part in CATEGORY_DIRS for part in rel_parts[:-1]):
102 continue
103 if p.name.startswith("."):
104 continue
105 files.append(p)
106 else:
107 for p in root.iterdir():
108 if p.is_file() and not p.name.startswith("."):
109 files.append(p)
110 return sorted(files, key=lambda x: x.name.lower())
111
112
113def main() -> int:
114 parser = argparse.ArgumentParser(description="整理文件夹内的文件")
115 parser.add_argument("folder", help="要整理的文件夹路径")
116 parser.add_argument("--execute", action="store_true",
117 help="真正执行;缺省只预览(dry-run)")
118 parser.add_argument("--by-date", action="store_true",
119 help="按修改时间 年-月 分类,而非按类型")
120 parser.add_argument("--copy", action="store_true",
121 help="复制文件而非移动")
122 parser.add_argument("--recursive", action="store_true",
123 help="递归处理子文件夹中的文件")
124 args = parser.parse_args()
125
126 root = Path(args.folder)
127 if not root.exists() or not root.is_dir():
128 print(f"错误:文件夹不存在或不是目录:{root}")
129 return 1
130 if is_protected(root):
131 print("错误:为避免误操作,不能整理驱动器根、系统目录或用户主目录。")
132 return 1
133
134 files = collect_files(root, args.recursive)
135 if not files:
136 print("没有需要整理的文件。")
137 return 0
138
139 action = "复制" if args.copy else "移动"
140 mode = "按修改时间(年-月)" if args.by_date else "按文件类型"
141 print(f"目标文件夹:{root.resolve()}")
142 print(f"整理方式:{mode};操作:{action};"
143 f"模式:{'执行' if args.execute else '预览(未改动任何文件)'}")
144 print("-" * 60)
145
146 counts: dict[str, int] = {}
147 for src in files:
148 dest_dir = target_dir_for(src, root, args.by_date)
149 dest = unique_destination(dest_dir / src.name)
150 label = dest_dir.name
151 counts[label] = counts.get(label, 0) + 1
152 rel_dest = dest.relative_to(root)
153 print(f"[{label}] {src.name} -> {rel_dest}")
154
155 if args.execute:
156 dest_dir.mkdir(parents=True, exist_ok=True)
157 final_dest = unique_destination(dest_dir / src.name)
158 if args.copy:
159 shutil.copy2(src, final_dest)
160 else:
161 shutil.move(str(src), str(final_dest))
162
163 print("-" * 60)
164 summary = ",".join(f"{k} {v} 个" for k, v in sorted(counts.items()))
165 print(f"共 {len(files)} 个文件:{summary}")
166 if not args.execute:
167 print("这是预览,未改动任何文件。确认无误后加 --execute 执行。")
168 else:
169 print("整理完成。")
170 return 0
171
172
173if __name__ == "__main__":
174 sys.exit(main())

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。