ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python自动整理下载目录:归档去重脚本实战

用Python自动整理下载目录:归档去重脚本实战 先交代一下背景这个“无标题”其实不是真的没有标题而是我本地一个文件整理脚本的项目代号。仓库里新建文件夹的时候随手打了个“无标题”后来一直没改这名字反而越用越顺眼——它要处理的本来就是一堆不知道该放哪、没有归属感的文件给它们分门别类找到一个“标题”正是这个工具干的事。这个脚本解决的是我电脑里最难堪的一个角落下载目录。相信不少人跟我一样Downloads文件夹半年不整理就能突破上千个文件从安装包到 PDF 到不知道什么时候存的压缩包堆成一团。真要手动整理吧每次盯着几百个文件就开始选择困难这个该算“工作”还是“杂项”那个算“图片”还是“素材”折腾十分钟就放弃。后来我写了一个按规则自动归档和去重的小脚本把“选择困难”变成了“规则看一眼脚本自动执行”彻底治好了整理拖延症。这个项目适合所有下载目录长期失控的人也适合刚学 Python 的朋友拿来当练手项目。整篇用的都是 Python 标准库不装任何第三方依赖就能跑。我会把设计思路、关键细节、完整代码和踩过的坑都拆开讲你可以直接抄作业也可以按自己的习惯改规则。1. 内容整体设计与思路拆解1.1 先想清楚你到底要整理什么东西动笔写代码之前我花了很长时间想一个问题整理文件夹这个需求本质上到底是在做什么后来想明白了它其实就三件事归档、去重、清理。归档把 PDF 放文档目录把图片放图片目录把安装包放安装包目录。去重找出同一个文件下载了多次的副本帮你识别出来。清理删掉不再需要的东西。这一步我一开始就没打算写进脚本里删除操作风险太高脚本角色应当是“搬运工”和“检查员”而不是“拆迁队”。发现明显不用的文件人工看一眼再删心里才踏实。所以这个小工具的定位非常清晰只做归档和重复检测删除动作留给用户自己决定。所有移动操作都能通过--dry-run参数先演练一遍只打印“准备把 A 移动到 B”不做任何实际改动这样即使规则写错了也不会造成破坏。确定了边界之后后面每一步开发都变得轻松很多因为“万一误删了怎么办”这个最大的心理负担直接没了。1.2 方案选型为什么是 Python 加纯标准库市场上已经有 Hazel、DropIt 这类文件整理工具为什么我还要自己写说实话这些工具做得都不错但都有各自的限制Hazel 基本是 macOS 专用DropIt 的规则界面老旧且长时间没大更新更关键的是我不想把“文件该怎么分类”这个判断完全交给一个黑盒工具。自己写脚本最大的好处是规则完全透明。分类依据是什么、匹配顺序怎么样、遇到重名文件怎么处理每一行代码都摆在明面上出了问题能立刻定位。另一个好处就是跨平台Python 脚本在 Windows、macOS、Linux 上都能跑。技术栈上我刻意保持克制只用了pathlib、shutil、hashlib、argparse、collections这几个标准库模块pathlib处理路径比老式的os.path可读性好太多。shutil.move负责移动文件。hashlib做 MD5 计算用来识别重复文件。argparse解析命令行参数方便传入目录和开关选项。collections的defaultdict在按哈希聚合重复文件时很好用。不引入第三方库的原因也很实在装依赖本身就是一个门槛别人拿到你的脚本还要先pip install一堆东西体验直接打折扣。纯标准库版本复制到哪台机器上都能跑这才是工具该有的样子。2. 核心细节解析与实操要点2.1 规则设计先关键词后扩展名整个工具的灵魂是分类规则。我第一版写的是纯扩展名映射跑完发现效果一般因为很多文件光看扩展名没法知道它的真实用途比如一份 PDF可能是简历可能是发票也可能是论文。后来我把规则分成了两层执行顺序很关键先跑关键词规则再跑扩展名规则最后扔进“其他”目录。关键词规则的用处是识别特殊身份文件。比如我的下载目录里经常有张三-简历.pdf、2024年度发票汇总.pdf这种文件单纯按扩展名会全归到“文档”里但实际情况是我希望简历和发票能被单独分出来方便找。于是就有了一个额外的关键词映射KEYWORD_RULES { 简历: [简历, resume, cv], 发票: [发票, invoice], 合同: [合同, contract], }费用逻辑是这样的拿到一个文件名先转成小写文件名大小写不统一是常态然后遍历关键词规则的每一项只要文件名里包含这个关键词就直接归类到对应目录。如果所有关键词都没命中再走扩展名映射。这样既不会让“简历”淹没在普通 PDF 里又不会因为过度规则化导致每个文件夹都被切得稀碎。扩展名映射我是按常见使用场景来的显示一个小参考版本目标目录扩展名规则图片.jpg .jpeg .png .gif .webp .bmp .svg视频.mp4 .mkv .avi .mov .wmv音频.mp3 .wav .flac .aac文档.pdf .docx .doc .txt .md .xlsx .xls .pptx .ppt压缩包.zip .rar .7z .tar .gz安装包.exe .msi .dmg .pkg .deb .rpm代码.py .js .ts .java .c .cpp .go .html .css .json其他上面都没匹配到的这里有个容易被忽略的细节扩展名判断时必须把所有字符转成小写否则一个IMG_123.PNG会被当成未知类型丢进“其他”。另外我故意把 .html 和 .css 归到“代码”而不是“文档”因为这类文件多数是下载的网页模板或者项目文件归到代码类更符合实际场景你可以按自己的习惯调整。2.2 冲突处理同名文件该怎么收场整理文件时最常遇到的意外就是目标目录里已经有同名文件。比如三周前下载过资料.zip今天又下载了一份新的资料.zip如果直接执行shutil.move脚本会抛异常甚至直接覆盖旧文件。我不能接受任何覆盖行为所以必须自己处理冲突。我的处理策略是如果目标目录中不存在同名文件直接移动如果存在则在文件名后面追加序号变成资料_1.zip、资料_2.zip。实现逻辑并不复杂def unique_path(target_dir: Path, filename: str) - Path: target target_dir / filename if not target.exists(): return target stem, suffix filename.stem, filename.suffix counter 1 while True: candidate target_dir / f{stem}_{counter}{suffix} if not candidate.exists(): return candidate counter 1这个方案也不是完美的比如重复下很多次同一个文件时会留下资料_1.zip、资料_2.zip等多个副本。所以后面我又加了一个 MD5 重复检测先把重复识别出来再由用户决定保留哪份。加序号只是保证了程序不死去重才是真正解决文件堆积的方案两者得配合使用。2.3 重复文件检测MD5 加文件大小双保险文件重名不一定代表内容相同文件名不同也可能内容完全相同。比如从不同网站下载的同一张图片名字可能叫photo.jpg和download.jpg实际内容一模一样这种必须靠内容去重。MD5 是一种哈希算法可以把任意内容映射成一段固定长度的字符串内容哪怕只改动一个字节哈希值也完全不一样。用它做重复检测的思路很简单计算每个文件的 MD5哈希相同的文件内容就是一样的。直接算 MD5 对大文件不太友好所以我在计算前先加了一道快速过滤文件大小。如果两个文件大小都不一样内容肯定不同没必要算哈希。只有文件大小相同的文件才会进入 MD5 比较环节这样能省掉大量不必要的磁盘读取。计算哈希时也要注意性能问题不能一次性把整个大文件读进内存正确做法是分块读取这里用了 8192 字节的分块大小def file_md5(path: Path, chunk_size: int 8192) - str: md5 hashlib.md5() with open(path, rb) as f: while chunk : f.read(chunk_size): md5.update(chunk) return md5.hexdigest()识别出重复文件后我默认不会自动删任何一方而是打印一份重复清单同时根据修改时间标出哪份更新。你确定之后可以手动删除或者以后再加一个--delete-duplicates参数来实现自动清理看个人需求。3. 实操过程与核心环节实现3.1 搭出分类脚本的基础版本整体思路清晰了代码写起来就快了。我建议从最小可用版本开始先实现目录扫描和自动归档跑通流程后再加去重。下面这个是基础版本的核心逻辑from pathlib import Path import shutil import argparse EXTENSION_RULES { 图片: [.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg], 视频: [.mp4, .mkv, .avi, .mov, .wmv], 音频: [.mp3, .wav, .flac, .aac], 文档: [.pdf, .docx, .doc, .txt, .md, .xlsx, .xls, .pptx, .ppt], 压缩包: [.zip, .rar, .7z, .tar, .gz], 安装包: [.exe, .msi, .dmg, .pkg, .deb, .rpm], 代码: [.py, .js, .ts, .java, .c, .cpp, .go, .html, .css, .json], } KEYWORD_RULES { 简历: [简历, resume, cv], 发票: [发票, invoice], 合同: [合同, contract], } def classify(file_path: Path) - str: name file_path.name.lower() for category, keywords in KEYWORD_RULES.items(): if any(k in name for k in keywords): return category ext file_path.suffix.lower() for category, extensions in EXTENSION_RULES.items(): if ext in extensions: return category return 其他 def organize_downloads(source_dir: Path, dry_run: bool False): if not source_dir.is_dir(): raise ValueError(f目录不存在: {source_dir}) for item in sorted(source_dir.iterdir()): if not item.is_file(): continue if item.name.startswith(.): continue category classify(item) target_dir source_dir / category target_dir.mkdir(parentsTrue, exist_okTrue) target_path target_dir / item.name if target_path.exists(): target_path unique_path(target_dir, item) if dry_run: print(f[演练] {item.name} - {target_path}) else: shutil.move(str(item), str(target_path)) print(f[移动完成] {item.name} - {target_path})有几个细节需要展开说一下。遍历使用sorted(source_dir.iterdir())是为了让处理顺序固定方便测试和复现。跳过隐藏文件的判断很关键否则.DS_Store、临时文件这种隐藏文件也会被搬走容易引起奇怪的问题。分类函数是先走关键词再走扩展名这个顺序在前面已经解释过是规则设计的重要一环。运行方式非常简单python organize.py --dir ~/Downloads --dry-run先通过--dry-run跑一遍确认规则符合预期后再去掉这个参数正式执行。这一步别偷懒我一个人第一次直接跑就把“简历”规则误伤了因为下载文件夹里有一个“简历模板大全.zip”被关键词规则分进了“简历”目录。演练模式能让你在几秒钟内发现这些规则问题。3.2 增加去重能力识别重复文件而不删除归档逻辑稳定后我开始加去重模块。这个模块我设计成可独立使用的既可以配合归档流程使用也可以单独扫一个目录检查重复文件。重复检测的核心是建立一个字典以文件大小为第一层索引再以 MD5 为第二层索引最后输出所有哈希相同的文件组from collections import defaultdict import hashlib import os scan_dir Path.home() / Downloads size_index defaultdict(list) for p in scan_dir.rglob(*): if p.is_file(): try: size_index[p.stat().st_size].append(p) except OSError: pass hash_index defaultdict(list) for size, paths in size_index.items(): if len(paths) 2: continue for p in paths: try: digest file_md5(p) hash_index[digest].append(p) except OSError: pass print(疑似重复文件分组) for digest, paths in hash_index.items(): if len(paths) 2: continue print(f哈希: {digest}) for p in paths: mtime p.stat().st_mtime print(f {p} (修改时间: {time.strftime(%Y-%m-%d %H:%M, time.localtime(mtime))}))实现时要注意rglob(*)会递归扫描所有子目录不仅能查下载目录也能查任何目录树。另外file_md5读取大文件是耗时的先按文件大小分组就是为了减少哈希计算次数。如果一组里只有一个是单个文件根本不用算哈希。这套逻辑跑出来的结果是一份重复文件分组清单每组的文件内容都相同但修改时间不同。你浏览清单自己决定保留哪份、删除哪份比让脚本替你决策要稳妥得多。我也理解有的朋友就喜欢全自动可以再写一个参数自动保留修改时间最新的文件删除其余文件但这个功能我始终没加上因为删除操作我还是要亲眼确认。3.3 完整脚本示例我日常使用的精简版下面是整合后的完整版本兼顾归档和去重我把平时个人用的精简版贴在这里你可以直接复制保存成organize.py#!/usr/bin/env python3 from pathlib import Path import shutil import hashlib import argparse import time from collections import defaultdict EXTENSION_RULES { 图片: [.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg], 视频: [.mp4, .mkv, .avi, .mov, .wmv], 音频: [.mp3, .wav, .flac, .aac], 文档: [.pdf, .docx, .doc, .txt, .md, .xlsx, .xls, .pptx, .ppt], 压缩包: [.zip, .rar, .7z, .tar, .gz], 安装包: [.exe, .msi, .dmg, .pkg, .deb, .rpm], 代码: [.py, .js, .ts, .java, .c, .cpp, .go, .html, .css, .json], } KEYWORD_RULES { 简历: [简历, resume, cv], 发票: [发票, invoice], 合同: [合同, contract], } def file_md5(path: Path, chunk_size: int 8192) - str: md5 hashlib.md5() with open(path, rb) as f: while chunk : f.read(chunk_size): md5.update(chunk) return md5.hexdigest() def unique_path(target_dir: Path, file_path: Path) - Path: target target_dir / file_path.name if not target.exists(): return target stem, suffix file_path.stem, file_path.suffix counter 1 while True: candidate target_dir / f{stem}_{counter}{suffix} if not candidate.exists(): return candidate counter 1 def classify(file_path: Path) - str: name file_path.name.lower() for category, keywords in KEYWORD_RULES.items(): if any(k in name for k in keywords): return category ext file_path.suffix.lower() for category, extensions in EXTENSION_RULES.items(): if ext in extensions: return category return 其他 def organize(source_dir: Path, dry_run: bool False): print(f开始整理目录: {source_dir}) for item in sorted(source_dir.iterdir()): if not item.is_file() or item.name.startswith(.): continue category classify(item) target_dir source_dir / category target_dir.mkdir(parentsTrue, exist_okTrue) target_path unique_path(target_dir, item) if dry_run: print(f[演练] {item.name} - {target_path}) else: shutil.move(str(item), str(target_path)) print(f[移动] {item.name} - {target_path}) def find_duplicates(source_dir: Path): print(f扫描重复文件: {source_dir}) size_index defaultdict(list) for p in source_dir.rglob(*): if p.is_file() and not p.name.startswith(.): try: size_index[p.stat().st_size].append(p) except OSError: pass hash_index defaultdict(list) for size, paths in size_index.items(): if len(paths) 2: continue for p in paths: try: digest file_md5(p) hash_index[digest].append(p) except OSError: pass found False for digest, paths in hash_index.items(): if len(paths) 2: continue found True print(f\n哈希: {digest}) for p in paths: mtime p.stat().st_mtime print(f {p} 修改时间: {time.strftime(%Y-%m-%d %H:%M, time.localtime(mtime))}) if not found: print(没有发现重复文件。) return found def main(): parser argparse.ArgumentParser(description无标题文件整理工具) parser.add_argument(--dir, typestr, defaultstr(Path.home() / Downloads), help要整理的目录) parser.add_argument(--dry-run, actionstore_true, help演练模式不做实际移动) parser.add_argument(--dedup, actionstore_true, help查找重复文件) args parser.parse_args() source_dir Path(args.dir).expanduser() if args.dedup: find_duplicates(source_dir) else: organize(source_dir, dry_runargs.dry_run) if __name__ __main__: main()用法# 演练一遍不做实际修改 python organize.py --dir ~/Downloads --dry-run # 正式整理下载目录 python organize.py --dir ~/Downloads # 查询下载目录里的重复文件 python organize.py --dir ~/Downloads --dedup完整脚本只有 100 行左右把关键词规则、扩展名规则、重名保护、重复检测都纳入了。这段代码在 Windows 下同样适用只要把~/Downloads换成实际的用户路径或直接让脚本默认读取Path.home() / Downloads。代码里有个细节值得注意unique_path函数接收的是file_path而不是文件名这是特意为了同时拿到stem和suffix避免自己手动拆分字符串。4. 常见问题与排查技巧实录4.1 实际使用中遇到的典型坑脚本用了快一年踩过的坑基本集中在四个地方。坑一快捷方式全部失效。下载目录里存的快捷方式文件比如.lnk或.desktop文件被移动到新目录后目标路径没变但快捷方式内容指向的还是旧位置于是鼠标点开就是一个空路径报错。这个不是脚本的问题是快捷方式本身的绝对路径特性决定的。我后来在处理时特意把快捷方式文件排除在外或者手动重新创建但更根本的办法是下载目录本身就不该放太多快捷方式。坑二文件被程序占用导致移动失败。有些文件比如在用的安装包、缓存的临时文件会被正在运行的程序锁定。Windows 下会直接抛PermissionErrorLinux 下则相对宽松但跨卷移动时也可能出问题。遇到这种错误我现在的处理方式是捕获异常、打印文件名、继续处理其他文件最后人工来关注失败项不要让整个脚本因为一个文件卡死。坑三跨盘符移动变成了“复制加删除”耗时翻倍。shutil.move在同一个文件系统内其实只是改一下目录项瞬间完成但如果源目录和目标目录不在同一个分区下这个函数会退回成“先复制整个文件到目标位置再删除源文件”的方式。我的下载目录和归档目录正好分在两个磁盘上移动几个 GB 的大文件时就慢得明显。解决思路很简单要么归档目录也放在同一磁盘要么接受这个代价提前把大文件挑出来特殊处理。坑四关键词规则误伤。关键词匹配是纯文本包含匹配所以会出现前面提到的“简历模板大全.zip”被扔进“简历”目录的情况。我的应对措施是给关键词规则加了一个精确匹配开关比如文件名里出现“简历”两个字时还要判断前面是否有人名字特征但这会把规则弄得很复杂。目前更实用的做法是像简历这种特殊分类把规则从“包含即触发”改成“文件后缀是 .pdf 且文件名包含”至少能排除掉压缩包和图片。4.2 问题速查表我把经常遇到的问题整理成一个表格方便你对照排查症状可能原因解决办法移动时报 PermissionError文件被程序占用或没有写权限关闭相关程序用管理员/超级用户权限跑捕获异常继续处理其他文件移动后源目录文件还在跨盘移动过程中被中断检查目标文件是否完整恢复后重新运行脚本有些文件没被移动文件名以点开头被隐藏文件逻辑过滤确认是否真的希望处理隐藏文件修改过滤条件重复检测结果不准只按大小分组大文件计算慢确认 MD5 计算是完整分块读取的先看文件大小是否一致中文文件名乱码终端编码问题Windows 先执行chcp 65001切换 UTF-8 编码目标目录出现大量_1、_2后缀文件同名文件太多之前没做去重先跑--dedup清一轮重复再整理归档快捷方式全失效快捷方式存储的是绝对路径移动后重新创建快捷方式或尽量不把快捷方式放下载目录4.3 最重要的调试技巧永远先来一遍--dry-run项目上线以来我最大的心得是无论你对规则多有信心正式执行之前一定先跑一遍--dry-run。这个习惯帮我避开了无数个潜在事故。有一次我调整了关键词规则结果下载文件夹里一个叫“最终版-合同-勿删.pdf”被正确分到“合同”但一个叫“劳动合同范本.zip”的压缩包也一起被分进了“合同”。演练模式一跑立刻看出问题这才来得及调整规则。演练模式打印的是“原路径”和“目标路径”的对应关系相当于给你一张完整的预执行清单。你花十秒钟扫一遍就能发现大部分规则问题比移动完之后再手动搬回来高效太多。我还建议在正式运行前看一眼日志输出把执行过程保留下来python organize.py --dir ~/Downloads organize.log 21日志文件里记录了什么文件去了哪里万一之后发现某个文件找不到了翻日志立刻能定位比凭记忆回想可靠得多。5. 后续扩展与一点个人体会5.1 可以快速上手的三个扩展方向这个基础脚本完全够日常用了但有三个方向我觉得非常值得扩展而且门槛都不高。第一个方向是配置文件外置。现在分类规则直接写在脚本里每次改规则都要改代码对不熟悉编程的人不友好。可以把规则抽到一个 JSON 文件里脚本启动时加载想改规则直接编辑 JSON 就行不用碰代码。我自己现在就已经把规则挪到rules.json里了方便在不同电脑上同步。第二个方向是定时任务自动化。配合系统自带的定时任务机制比如 Linux 的crontab或 Windows 的“任务计划程序”可以做到每周自动执行一次整理。不过自动运行有一个前提就是你已经用演练模式验证过规则足够稳定否则一个误伤规则在无人值守情况下执行后果只能靠日志来补救了。第三个方向是实时监控目录。使用watchdog库可以监听文件系统事件新文件一进入下载目录就立刻自动归档做到“边下载边整理”。这个扩展用起来很爽但有个需要注意的地方大文件还在下载中就被触发移动可能导致文件不完整。实现时一般要做一个“文件稳定等待”机制比如文件修改时间 N 秒内没变化才处理。这块门槛稍高适合基础版本已经跑顺后慢慢研究。5.2 复盘数据和个人体验这个“无标题”脚本在我电脑上用了快一年Downloads目录从最高峰的一千多个文件恢复到现在的两位数。每周五下班前跑一次顺手清掉--dedup扫出来的重复项整个过程不超过一分钟但换来的是一整周不会被乱糟糟的下载目录影响心情。整理文件这件事归根到底不是技术难题而是规则问题。你愿意花十分钟想清楚“我的文件到底应该怎么分类”后面的代码只是把这个规则忠实执行一遍。这也是我为什么坚持用脚本而不是图形化工具规矩自己定执行靠自动化出了问题看得见、改得动。如果你也一直被下载目录、桌面或者工作文件夹折磨不妨从今天这个最小版本开始先抄走用到顺手再按自己的使用习惯改关键词规则和扩展名规则。把规则写清楚剩下的交给脚本就好。
返回列表