昨天晚上我真是…怎么说呢…本来想早点睡的,结果我电脑那个“下载”文件夹又炸了,里面啥都有,截图、PDF、zip、微信传过来的“最终版(3)(真的最终).docx”,你们懂吧,就那种一眼看过去人直接烦躁的那种。
我这人有个毛病,一烦就想写脚本。以前搞服务端的时候,默认配置不改就等着线上背锅那种事我真踩过 ,所以文件整理这事我也不太信“手动拖一拖就好”,拖着拖着就丢了,丢了你还找不到,跟丢消息一样难受。
场景给你们还原下哈: 我女儿(啊不对我这边就当“家里人”)突然要我把“去年旅行的照片”发出来,我说行啊,结果我发现照片在“相机导出/未分类/新建文件夹(12)/IMG_0001(1).jpg”…我当场血压就上来了。于是就干一件事:写个 Python,按“文件类型 + 日期”自动归档,还得支持 dry-run(就是先演练不真移动),不然你一跑错目录直接把自己电脑送走。
代码我写得有点啰嗦哈,口癖就是这样…你直接拿去改路径就能用。
from __future__ import annotations
import hashlib
import shutil
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
@dataclass
classRule:
# 扩展名 -> 目标文件夹名
ext_to_dir: dict[str, str]
# 兜底目录
fallback_dir: str = "_others"
# 重名策略:追加 (n)
rename_on_conflict: bool = True
defsha1_file(p: Path, chunk_size: int = 1024 * 1024) -> str:
h = hashlib.sha1()
with p.open("rb") as f:
whileTrue:
chunk = f.read(chunk_size)
ifnot chunk:
break
h.update(chunk)
return h.hexdigest()
defpick_date(p: Path) -> str:
# 用修改时间归档,简单粗暴但够用
ts = p.stat().st_mtime
return datetime.fromtimestamp(ts).strftime("%Y-%m")
defsafe_target_path(dst_dir: Path, name: str, rename_on_conflict: bool) -> Path:
target = dst_dir / name
ifnot target.exists() ornot rename_on_conflict:
return target
stem = target.stem
suffix = target.suffix
i = 1
whileTrue:
cand = dst_dir / f"{stem} ({i}){suffix}"
ifnot cand.exists():
return cand
i += 1
deforganize_folder(root: Path, rule: Rule, dry_run: bool = True, dedup: bool = True) -> None:
root = root.expanduser().resolve()
ifnot root.exists() ornot root.is_dir():
raise ValueError(f"目录不存在:{root}")
# 记录已见过的 hash,做简单去重:同内容只保留一份,其余丢到 _duplicates
seen_hash: dict[str, Path] = {}
dup_dir = root / "_duplicates"
ifnot dry_run:
dup_dir.mkdir(exist_ok=True)
for p in root.rglob("*"):
# 跳过目录、隐藏文件夹、以及我们自己创建的归档目录
if p.is_dir():
continue
if any(part.startswith(".") for part in p.parts):
continue
if p.parent.name in {"_duplicates"}:
continue
ext = p.suffix.lower().lstrip(".")
ym = pick_date(p) # 例如 2026-01
sub = rule.ext_to_dir.get(ext, rule.fallback_dir)
dst_dir = root / sub / ym
ifnot dry_run:
dst_dir.mkdir(parents=True, exist_ok=True)
# 去重(可选)
if dedup:
try:
h = sha1_file(p)
if h in seen_hash:
# 有同内容文件,扔 duplicates
target = (dup_dir / p.name) if dry_run else safe_target_path(dup_dir, p.name, True)
print(f"[DUP] {p} -> {target} (same as {seen_hash[h].name})")
ifnot dry_run:
shutil.move(str(p), str(target))
continue
else:
seen_hash[h] = p
except Exception as e:
# hash 算失败也别崩,先按普通文件走
print(f"[WARN] hash失败 {p}: {e}")
target = safe_target_path(dst_dir, p.name, rule.rename_on_conflict)
print(f"[MOVE] {p} -> {target}")
ifnot dry_run:
shutil.move(str(p), str(target))
if __name__ == "__main__":
# 你改这里:比如 ~/Downloads 或者某个摄影导出目录
ROOT = Path("~/Downloads")
rule = Rule(
ext_to_dir={
"jpg": "images",
"jpeg": "images",
"png": "images",
"gif": "images",
"mp4": "videos",
"mov": "videos",
"pdf": "docs",
"doc": "docs",
"docx": "docs",
"xls": "docs",
"xlsx": "docs",
"ppt": "docs",
"pptx": "docs",
"zip": "archives",
"rar": "archives",
"7z": "archives",
}
)
# 先 dry_run 看看输出对不对,确认没问题再 dry_run=False
organize_folder(ROOT, rule, dry_run=True, dedup=True)
你看我这脚本吧,最关键的其实不是“怎么移动”,而是两点: 一个是 dry-run,不然你第一次跑就真搬家,搬错了你得哭。 另一个是去重,我以前整理照片最烦的就是“IMG_0001(1)”这种,其实内容一样,留一份就行,其他的我统一扔到 _duplicates,至少不会混在主目录里恶心人。
对了我还加了个“按月份归档”,因为人找东西的时候脑子通常是“那是去年十月的东西”,不是“那是 jpg”,所以目录长这样会比较顺:images/2026-01/xxx.jpg、docs/2025-12/xxx.pdf,你翻起来就像翻相册一样。
中间有个小坑我得碎碎念一句: Windows 那边会有一些奇怪的权限文件,或者 OneDrive 同步的临时文件,hash 会读失败,我这边直接 [WARN] 打一下继续跑,别为了一个破文件把全局任务停了,真不值当。
还有哈,如果你不想按“修改时间”,想按“创建时间”,那就尴尬一点:不同系统对创建时间支持不一致,尤其 Linux 基本就…算了不展开了,你要真需要我再给你整一个“能跑就行”的版本。
最后一个小动作,很多人会问“我整理完咋确认目录结构对不对”,我一般会再生成个目录树文本,发给同事也好,自己留档也好:
from pathlib import Path
defdump_tree(root: Path, max_depth: int = 4) -> str:
root = root.expanduser().resolve()
lines = []
defwalk(p: Path, depth: int):
if depth > max_depth:
return
prefix = " " * depth
lines.append(f"{prefix}- {p.name}/"if p.is_dir() elsef"{prefix}- {p.name}")
if p.is_dir():
# 只展示一部分也行,你想全量就去掉切片
for child in sorted(p.iterdir(), key=lambda x: (x.is_file(), x.name.lower()))[:200]:
walk(child, depth + 1)
walk(root, 0)
return"\n".join(lines)
if __name__ == "__main__":
print(dump_tree(Path("~/Downloads")))
行了我差不多说完了…我现在困死了但又觉得目录干净了好爽,强迫症得到治疗那种。你要是想更狠一点,比如“按项目名识别、按关键字分类、或者把重复的直接删掉只留一份”,也能搞,就是要更谨慎点,删文件这种事一上头就容易出事故…我先不扯太远了,等你用起来卡哪了再说。