2026-10-07 · 喵窝资源铺
下载目录里发票、合同、报表混成一锅粥,手动拖到手抽筋。上一篇《文件按修改时间归档》解决的是「按日期分」,更早的《按文件名关键字自动归档》是轻量版;这篇是安全加固版:套娃防御、同名绝不覆盖、跳过原因逐条可查——一份 关键词规则归类脚本:规则写在 JSON 里随改随用、扩展名白名单过滤、目标同名不覆盖、规则自相矛盾拒绝启动。代码完整实跑过,真实输出贴在下面。
{"发票": ["发票","invoice"]}),启动前先做交叉套娃检查(见坑 1);--copy 则复制)进对应子目录;目标已存在同名文件 → 跳过并报告,绝不覆盖(见坑 2);{
"发票": ["发票", "invoice"],
"合同": ["合同", "contract"]
}
键是目标子目录名,值是关键词列表。目录名随便起,关键词不限数量,中英文混着写都行。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""按关键词把文件夹里的文件自动归类到子目录。
用法: python3 sort_by_keyword.py <源目录> --rules 规则.json [--copy] [--ext .pdf,.docx]
规则.json: {"发票": ["发票","invoice"], "合同": ["合同","contract"]}
铁律: 同名冲突不覆盖 -> 跳过并报告; 目录名撞别的规则关键词 -> 拒绝启动。
"""
import argparse, json, os, shutil, sys
def load_rules(p):
with open(p, encoding="utf-8") as f:
r = json.load(f)
for k in r:
if os.sep in k or k in (".", ".."):
sys.exit(f"非法目录名: {k}")
return r
def match(name, kws):
low = name.lower()
return any(kw.lower() in low for kw in kws)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("src"); ap.add_argument("--rules", required=True)
ap.add_argument("--copy", action="store_true")
ap.add_argument("--ext", default="", help="逗号分隔白名单扩展名,如 .pdf,.jpg;空=全部")
a = ap.parse_args()
rules = load_rules(a.rules)
exts = {e.strip().lower() for e in a.ext.split(",") if e.strip()}
# 交叉套娃防御:目录A的名字不能命中目录B的关键词(A≠B),否则A里的
# 归类结果会成为B的扫描输入。同名自命中放行——目标目录名=关键词是常态。
names = list(rules)
for d in names:
for k in names:
if k != d and match(d, rules[k]):
sys.exit(f"规则目录名 {d} 会命中规则 {k} 的关键词,造成套娃,请改名")
moved, skipped = [], []
for name in sorted(os.listdir(a.src)):
p = os.path.join(a.src, name)
if not os.path.isfile(p):
continue
if exts and os.path.splitext(name)[1].lower() not in exts:
skipped.append((name, "扩展名不在白名单")); continue
hit = None
for d, kws in rules.items():
if match(name, kws):
hit = d; break
if not hit:
skipped.append((name, "未命中任何关键词")); continue
dest_dir = os.path.join(a.src, hit)
os.makedirs(dest_dir, exist_ok=True)
dest = os.path.join(dest_dir, name)
if os.path.exists(dest):
skipped.append((name, f"目标已存在同名 {hit}/,不覆盖")); continue
if a.copy:
shutil.copy2(p, dest)
else:
shutil.move(p, dest)
moved.append((name, hit))
for n, d in moved: print(f"[移动] {n} -> {d}/")
for n, r in skipped: print(f"[跳过] {n} ({r})")
print(f"共处理 {len(moved)} 移动 / {len(skipped)} 跳过")
if __name__ == "__main__":
main()
测试目录 10 个文件:发票类 4(含「电子发票1234」「invoice_march」中英混排)、合同类 3、干扰文件 3(jpg/txt),扩展名白名单 .pdf,.docx,.xlsx:
[移动] contract_v2.pdf -> 合同/ [移动] invoice_march.xlsx -> 发票/ [移动] 劳动合同-张三.docx -> 合同/ [移动] 发票-2026-03-酒店.pdf -> 发票/ [移动] 发票-2026-03-酒店副本.pdf -> 发票/ [移动] 电子发票1234.pdf -> 发票/ [移动] 采购合同最终版.docx -> 合同/ [跳过] photo.jpg (扩展名不在白名单) [跳过] 会议纪要0305.txt (扩展名不在白名单) [跳过] 说明.txt (扩展名不在白名单) 共处理 7 移动 / 3 跳过
| 检查项 | 结果 |
|---|---|
| 归类正确性 | 7 个文件逐个核对:发票 4 张全进 发票/,合同 3 份全进 合同/,中英文关键词都命中 |
| copy 模式 | --copy 下源文件原地保留 + 子目录出现副本,两边内容一致 |
| 多关键词先到先得 | 「合同发票混合件.pdf」在 {"合同":...,"发票":...} 规则下进 合同/——顺序语义明确且可预期 |
| 同名不覆盖 | 再造一个同名 invoice_march.xlsx 再跑:跳过并注明「目标已存在同名」,子目录里原文件内容原封不动 |
| 幂等 | 连跑 2 次,第二次 0 移动;子目录里再放新文件也不会被二次归类(只扫顶层) |
| 套娃防御 | 故意把目录名起成「发票复印件」(命中「发票」关键词)→ 脚本拒绝启动并指出哪条规则撞了 |
{"发票": ["发票", ...]},目标目录名就叫「发票」,必然命中自己的关键词,脚本拒绝启动,一次都跑不了。这是防御写反了:目标目录名 = 关键词是常态(不然文件归哪儿?),该拦的是交叉——目录 A 的名字命中了别的规则 B 的关键词(比如目录叫「发票复印件」又存在规则「发票」),那 A 里的文件下一轮会被 B 抢走。修法:if k != d and match(d, rules[k]),只拦交叉、放行自命中。shutil.move 遇到目标同名会静默覆盖,数据直接没了。这是全脚本最危险的一行:shutil.move(p, dest) 在 dest 已存在时不是报错、不是重命名,而是直接覆盖——同名旧文件内容永久丢失,不崩、不警告、退出码还是 0。桌面整理场景里「3 月发票.txt」和「3 月发票.txt」内容不同的概率极高,一覆盖就找不回来。修法:移动前 os.path.exists(dest) 检查,存在就跳过并打印原因。(别想着自动改成 -1 后缀:整理脚本的职责是「不弄丢任何东西」,自动改名会让文件数量悄悄变多,用户对不上账;跳过+报告让人自己决定才是对的。)--ext .pdf,.docx 只整理文档,图片视频不动;空 = 全部文件;--copy,确认无误后手动删源;"其他": [""] 空关键词匹配一切——但建议先跑一轮看跳过清单再决定,别把病毒样本也「归类」了。不想自己跑脚本?喵窝资源铺提供 AI 代办:¥9.9 起,把杂乱目录打包发给我们,按你的规则归类+去重+清单回报一次搞定,当天交付。脚本类服务同样 9.9 起,改到你满意为止。
👉 miaowoo.900890.xyz(虚拟物品,售出后不退款,下单前确认需求)
← 返回全部文章 · 下一篇预告:Word 简历批量转 PDF