page contents

利用Python整理文件夹目录

昨晚我在家收拾电脑,真不是夸张哈,我桌面那个“新建文件夹(23)”都快成传家宝了……然后微信群里还真有人问我,“东哥你不是写代码的吗,你咋也这么乱”,我说你们别笑啊,越忙越容易把文件当垃圾桶一股脑往里扔,等要找合同、截图、安装包的时候,哎呀就跟翻冰箱找酱油一样,翻半天还找不到。

attachments-2026-03-rr9B3VeD69b75d508ea15.png

昨晚我在家收拾电脑,真不是夸张哈,我桌面那个“新建文件夹(23)”都快成传家宝了……然后微信群里还真有人问我,“东哥你不是写代码的吗,你咋也这么乱”,我说你们别笑啊,越忙越容易把文件当垃圾桶一股脑往里扔,等要找合同、截图、安装包的时候,哎呀就跟翻冰箱找酱油一样,翻半天还找不到。

后来我就干了个很无聊但很爽的事:写个 Python 小脚本,把一个目录里的文件按规则“归类 + 归档 + 记录”,顺手还做了个“反悔/撤销”的清单。你们别觉得这小事不值得,线上事故很多时候就是“默认行为你没管”然后出事了,文件整理也一样,默认全堆一起,看着没事,等你真要用就爆炸。

我先说下我踩过的坑啊: 1)同名文件:比如 report.pdf 一堆,直接 move 会覆盖,直接炸。 2)跨盘移动:Windows 上从 D 盘挪到 C 盘,rename 不一定行,得走复制+删除,慢得要死。 3)误伤:把 .ini.cfg 这种配置也挪走了,结果某软件第二天打不开,你以为是它坏了,其实是你昨晚手欠。

所以我写的这个脚本,思路就很“怂”:先 dry-run 预演,再执行;执行时写 manifest;能回滚;同名就自动改名;另外加点“生活化规则”,比如:图片进 images/2026-01,视频进 videos/,压缩包进 archives/,代码文件进 code/,其余进 others/。差不多就够用了。

代码我放这儿,你们直接 copy 也行,改改规则就能用,反正是我自己敲的,不是网上搬的那种“看着很牛但跑不起来”的玩意儿。

from __future__ import annotations

import argparse

import json

import os

import shutil

from dataclasses import dataclass, asdict

from datetime import datetime

from pathlib import Path

from typing import Dict, List, Tuple

@dataclass

class MoveRecord:

    src: str

    dst: str

    size: int

    mtime: float


DEFAULT_RULES: Dict[str, List[str]] = {

    "images": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".heic"],

    "videos": [".mp4", ".mov", ".mkv", ".avi", ".flv", ".wmv"],

    "documents": [".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".txt", ".md"],

    "archives": [".zip", ".rar", ".7z", ".tar", ".gz"],

    "code": [".py", ".js", ".ts", ".java", ".go", ".rs", ".c", ".cpp", ".h", ".json", ".yml", ".yaml"],

}


def pick_bucket(file_path: Path, rules: Dict[str, List[str]]) -> str:

    ext = file_path.suffix.lower()

    for bucket, exts in rules.items():

        if ext in exts:

            return bucket

    return "others"


def month_folder(ts: float) -> str:

    dt = datetime.fromtimestamp(ts)

    return f"{dt.year:04d}-{dt.month:02d}"


def safe_target_path(dst_dir: Path, name: str) -> Path:

    """

    同名不覆盖:a.txt -> a (1).txt -> a (2).txt ...

    """

    candidate = dst_dir / name

    if not candidate.exists():

        return candidate


    stem = Path(name).stem

    suffix = Path(name).suffix

    i = 1

    while True:

        candidate = dst_dir / f"{stem} ({i}){suffix}"

        if not candidate.exists():

            return candidate

        i += 1


def move_file(src: Path, dst: Path) -> None:

    """

    用 shutil.move:跨盘也能处理(可能是 copy+delete)

    """

    dst.parent.mkdir(parents=True, exist_ok=True)

    shutil.move(str(src), str(dst))


def organize(root: Path, dry_run: bool, manifest_path: Path, rules: Dict[str, List[str]]) -> List[MoveRecord]:

    records: List[MoveRecord] = []


    for p in root.iterdir():

        # 只处理文件,文件夹先别动,怂一点

        if not p.is_file():

            continue


        stat = p.stat()

        bucket = pick_bucket(p, rules)

        # 图片/文档按月份再分一层,找起来舒服点

        sub = month_folder(stat.st_mtime) if bucket in ("images", "documents") else ""

        dst_dir = root / bucket / sub if sub else root / bucket


        dst = safe_target_path(dst_dir, p.name)


        records.append(MoveRecord(

            src=str(p),

            dst=str(dst),

            size=stat.st_size,

            mtime=stat.st_mtime

        ))


    # 先打印预演

    for r in records:

        print(("[DRY] " if dry_run else "") + f"{r.src}  ->  {r.dst}")


    if dry_run:

        return records


    # 真执行

    moved: List[MoveRecord] = []

    for r in records:

        src = Path(r.src)

        dst = Path(r.dst)

        # 避免“自己搬自己”那种低级错误

        if src.resolve() == dst.resolve():

            continue

        move_file(src, dst)

        moved.append(r)


    # 写 manifest,留着回滚

    manifest_path.parent.mkdir(parents=True, exist_ok=True)

    with manifest_path.open("w", encoding="utf-8") as f:

        json.dump([asdict(x) for x in moved], f, ensure_ascii=False, indent=2)


    print(f"已写入清单: {manifest_path}")

    return moved


def rollback(manifest_path: Path, dry_run: bool) -> None:

    if not manifest_path.exists():

        raise FileNotFoundError(f"找不到清单文件: {manifest_path}")


    data = json.loads(manifest_path.read_text(encoding="utf-8"))

    # 回滚最好反向来:先搬后面的,减少覆盖概率

    for item in reversed(data):

        src = Path(item["dst"])  # 注意:清单里 dst 是“搬过去的位置”

        dst = Path(item["src"])  # src 是“原位置”

        if not src.exists():

            print(f"[SKIP] 不存在: {src}")

            continue


        target = safe_target_path(dst.parent, dst.name)

        print(("[DRY] " if dry_run else "") + f"{src}  ->  {target}")


        if not dry_run:

            target.parent.mkdir(parents=True, exist_ok=True)

            shutil.move(str(src), str(target))


def main():

    parser = argparse.ArgumentParser(description="整理文件夹目录:按类型归档 + manifest 可回滚")

    parser.add_argument("root", type=str, help="要整理的目录")

    parser.add_argument("--dry-run", action="store_true", help="只预演不执行")

    parser.add_argument("--manifest", type=str, default=".organize_manifest.json", help="移动清单文件名")

    parser.add_argument("--rollback", action="store_true", help="根据 manifest 回滚")

    args = parser.parse_args()


    root = Path(args.root).expanduser().resolve()

    manifest_path = root / args.manifest


    if args.rollback:

        rollback(manifest_path, args.dry_run)

        return


    organize(root, args.dry_run, manifest_path, DEFAULT_RULES)


if __name__ == "__main__":

    main()

你们看哈,这里面我故意做了几个“人类会后悔”的保护:

--dry-run:先看它准备怎么搬,别一上来就动真格。这个就跟你删库前先 select 一下差不多,哎别笑,真有人不看就跑。

manifest:每次执行完写个清单,想撤就 --rollback,不至于你搬完第二天发现“靠我把发票搬到 videos 里了”。

safe_target_path:同名不覆盖,这个太关键了,不然你那堆“截图.png”分分钟只剩最后一张。

我一般怎么用呢,假设你要整理 D:\Downloads: 1)先预演:python organize.py D:\Downloads --dry-run2)没问题再执行:python organize.py D:\Downloads3)后悔了:python organize.py D:\Downloads --rollback(也可以加 --dry-run 先看看回滚会干啥)

然后你会发现一个很玄学的事:文件夹干净了,人也会变得不烦躁……真的,我不骗你。 当然也不是啥都能靠脚本解决哈,比如你要按“项目名/客户名”归类,那得加规则,甚至要读文件名、正则提取、再落到不同目录,这就开始像“消息队列解耦”那种味儿了:先把混乱输入变成结构化事件,再按规则消费处理,反正本质都是把复杂度从人的脑子里挪到程序里。

哦对了,还有个小细节我顺嘴提一下:不要一上来就递归整理整个盘,尤其是系统盘……你要真想递归,最好加白名单目录、跳过隐藏文件、跳过 .git、node_modules,不然你会整理到怀疑人生。这个我以前排查 TCP 那种“卡在某个边界值”的诡异 bug 时就学乖了,越是“看起来很简单”的东西,越容易在边界上翻车。

行了我先说到这儿,我锅里水开了我得去关一下,不然一会儿又糊……你们要是想要“按文件名里日期归档”“按大小把大文件单独拎出来”这种更变态的规则,我回头再补两段代码也行,反正我也挺爱折腾这些小破事的。

更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1875 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 2228 文章
  3. Pack 1875 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章