wx-file-clean-ty
电脑端微信接收文件的清理与归档工具链。通用版:脚本里不含任何具体目录、客户名或行业词,
全靠使用者填一份 rules.json 驱动。
两根支柱:
- 规则化分类——把几千个杂乱文件判到正确的归档目录(阶段 4,最值钱的部分)
- 决策看板——把判定结果交给用户批量拍板,一个一个问是问不完的(阶段 4 末)
去重是可选的减量步骤(阶段 3),属于支线。一次真实的清理里,主力链路是
「盘点 → 共议 → 分类 → 看板 → 执行」,去重只贡献了约 14% 的减量。
首次使用:四步初始化
换一台机器、换一个人,只做这四件事,代码一行不用改:
# 1. 定位微信文件目录(自动找,找不到会提示你手动指定)
python scripts/locate_wechat.py
# 2. 只读盘点,看看面对的是什么量级
python scripts/scan_inventory.py --root "<上一步得到的目录>" --out-dir ./work
# 3. 从你的真实文件归纳规则草案(不是让你照抄模板)
python scripts/suggest_rules.py --inventory ./work/inventory.csv \
--out ./work/rules.draft.json --min-files 3 --top 25
# 4. 把草案的 A-E 五段确认表交给使用者,逐条确认后再定稿成 rules.json
第 3、4 步是这套工具的立场:规则不该由工具强加,也不该让使用者凭空填模板。
先扫你的真实文件,从文件名里归纳候选词,把「命中多少文件 / 多大 / 举什么例」摆出来,
你增删合并之后才定稿。脚本内置的只有「通用功能词」和「安全保护底线」,
与行业、职业、公司无关——真正的分类词全部来自你自己的数据。
规则写得好不好,看 references/classification-guide.md。
When to use
- 磁盘空间告警,要清理微信/企微接收的文件
- 要把微信收到的文件归档进自己的目录体系
- 用户提到「微信文件 / 重复文件 / 去重 / 转移 / 空间紧张 / 清理收尾」
Hard rules(不可违反)
- 先确认目标目录是不是链接。用户常把微信文件目录用 junction / 符号链接挂到别处。
链接只是指针,它的体积不等于占用空间,绝不能当可释放空间上报,也绝不能删除或断开。
用locate_wechat.py会自动标注is_link与指向。 - "文件名 + 大小一致" ≠ 内容一致。任何基于此判据的删除,执行前必须双侧 MD5 交叉验证。
- 只报告、不删除,除非用户明确批准。删除一律走回收站,禁止
rm -rf/del /F。 - 清空回收站必须单独获批,不能与「删除」混为一谈。
- 同盘移动不释放空间。只有「删除 + 清空回收站」才释放——开工前必须先讲清(阶段 0)。
- 未授权不新建目录。用户在面板里填的路径 = 授权;否则目录不存在就先问。
- 用户勾「删除」但命中保护区 → 暂缓,单独列出来再问一次,绝不照单执行。
- 任何批量移动都必须可回滚:执行器每次都写
<tag>_移动清单_<时间戳>.csv
(含 源路径 / 目标路径 / MD5)。没有这张清单就不许执行移动——
分类一定会判错,判错之后能不能捞回来,是这套工具能不能用的分水岭。 - 主题词不能单独当类型判据。「洞察 / 分析 / 研究 / 趋势 / 观察」是主题词,
内部汇报、客户提案、竞品拆解都可以叫「XX洞察」。判定文档类型要靠
发布年份期号 / 机构署名 / 法律声明这三类特征,累加计分、凑够才归类,
凑不够降级为待确认。
⚠ 别用"三选二":机构署名覆盖率只有 25%,一旦当必要条件就会漏掉四分之三的真报告。
正确姿势是"命中即加分,没命中只是少一分",详见references/classification-guide.md第七节。
七阶段 SOP 总览
阶段 0 空间账 先讲清"移动不释放空间",避免沟通错位
阶段 1 定位与体检 locate_wechat.py:找目录 + 辨链接 + 量体量
阶段 2 盘点→共议 全程只读:scan_inventory → suggest_rules → 你确认 → rules_lint
阶段 3 去重(可选) dedup_scan → 去重面板拍板 → apply_batch 删除 → **重跑盘点**
阶段 4 分类→看板 classify_generic → gen_panel 出 HTML → 用户批量拍板
阶段 5 回传与对账 三段式回收 + 数量对账(不对账会漏文件)
阶段 6 安全执行 apply_batch.py:备份+MD5 → 进回收站 → 移动(写移动清单)
阶段 7 收尾 反向抽查(content_probe)→ 误判回滚(rollback)→ 清回收站 + 对账
附录 A 归档后二次去重(仅 A 类对照)
附录 B 脚本工具箱
附录 C 规模参考
阶段 0:空间账(开工前必须讲清)
最容易沟通错位的地方。用户以为「把 10 GB 文件移走 = 腾出 10 GB」,实际同盘移动只是指针搬家。
| 动作 | 是否释放空间 | 说明 |
|---|---|---|
| 移到同盘的归档目录 | ❌ 一个字节都不释放 | 价值在归档,在找得到,不在瘦身 |
| 删除到回收站 | ❌ 未真正释放 | 回收站也在同一个盘 |
| 清空回收站 | ✅ 真正释放 | 必须单独批准 |
开场白模板:「这次要做的两件事得分开看——归档(把文件归位,不腾空间)和瘦身(删除+清空回收站,才腾空间)。你想先要哪个?」
> 边界:本技能只管微信接收文件目录。全盘找空间大头(浏览器缓存、旧工作区、虚拟机镜像)属于另一件事,别在这里展开。
阶段 1:定位与体检
1.1 自动定位(不要硬编码路径)
python scripts/locate_wechat.py # 列出全部候选
python scripts/locate_wechat.py --only-file # 只打印接收文件目录,供管道消费
查找顺序:环境变量 WXFILES_ROOT → 命令行 --root → 系统里记录的微信文件管理路径
(Windows 注册表 HKCU\Software\Tencent\WeChat)→ 默认候选(Windows ~/Documents/WeChat Files、
~/Documents/xwechat_files、各盘根目录;macOS ~/Library/Containers/com.tencent.xinWeChat/...)
→ 在这些根下找特征子目录。
两类目录要分清:
| 特征路径 | 含义 | 处理 |
|---|---|---|
msg/file、FileStorage/File |
接收的文件 | 本技能的清理目标 |
msg/attach |
图片与缓存 | 体量常常比 file 大数倍,清理前必须单独确认,别按"文件"的方式归档 |
1.2 校验链接(硬规则 1)
locate_wechat.py 会输出 is_link 与 target。看到链接时:
- 它不是一份占空间的副本,只是指针
- 删除 / 断开它 = 直接破坏微信的文件目录
- 判断真实占用来看
target指向的那个盘
若脚本没认出来,Windows 上手工确认:
(Get-Item -LiteralPath '<路径>' -Force).LinkType
1.3 盘空间与体量
python scripts/scan_inventory.py --root "<file 目录>" --out-dir ./work
输出 inventory.csv(全量清单)+ 分布画像:扩展名 TOP15、体积分层、月份分布、最大 20 个文件、
同名同大小的可疑重复组数。
低于 5 GB 可用空间才展开全量清理;否则给一行状态即可。
阶段 2:盘点 → 共议(只读,最值钱的部分)
铁律:分类阶段只读,绝不改盘。 产出是一张映射 CSV,用户拍板后才动。
2.1 先盘点,别急着定规则
python scripts/scan_inventory.py --root "<file 目录>" --out-dir ./work
拿到 inventory.csv(全量清单)+ 画像:扩展名 TOP15、体积分层、月份分布、最大 20 个文件。
2.2 让它归纳草案,规则由你定(本阶段的核心)
不要照抄模板,也不要凭空想关键词。先让脚本从你的真实文件里归纳:
python scripts/suggest_rules.py --inventory ./work/inventory.csv \
--out ./work/rules.draft.json --min-files 3 --top 25
它会输出一张五段确认表,这五段要原样交给使用者逐条确认:
| 段 | 内容 | 使用者要做的决定 |
|---|---|---|
| A. 建议建组 | 候选词 + 命中文件数 + 体积 + 示例文件名 | 留哪些、删哪些、合并哪些、目录叫什么 |
| B. 短词待定 | 1-2 字的高频词 | 是品牌名还是通用词?后者挪进 need_context |
| C. 扩展名画像 | 各类扩展名数量与体积 | 要不要按扩展名建组(安装包 / 素材) |
| D. 保护底线 | 合同、录音、证件、简历 | 这是安全相关的默认值,可增删 |
| E. 体积阈值 | <1MB 碎片的占比 | skip.min_mb 定多少 |
脚本已经做了两件容易出错的事,但仍需你复核:
- priority 按「命中文件数越少 = 越具体」自动排序——实测不这么排的话,
宽泛的「客户」(6 个) 会把具体的「品牌方案」(4 个) 全部吞掉。子集关系会在 A 段标
「是「XX」的子集,已排在其前面」。 - 已被保护区覆盖的词不再建组(如「合同」),建了也不生效。
草案里每个组都带 _evidence(命中多少文件 / 多大 / 举什么例),
没有证据的词不要凭感觉加。default_action 一律为「看」——删不删是人的决定。
定稿三处必改:① archive_root ② 删掉不认可的组 ③ 调 priority。
改完把 _evidence 和 _comment 删掉即可(分类器会忽略它们)。
2.2b 分类之前,先给规则做个体检(必做一步)
规则定稿后、跑分类器之前,先过一遍静态体检:
python scripts/rules_lint.py ./work/rules.json # 有 E 级就退出码 1
python scripts/rules_lint.py ./work/rules.json --out lint.md --strict
它不检查"你的词表对不对"(那是 2.2 里人工确认的事),只检查结构性风险——
那些"看起来能跑、跑起来必出事"的模式。最该拦的是 E1:
> 一条规则只有 any 一个维度,词表里却是「洞察/分析/研究/趋势」这类主题词。
> 主题词只说明文件在讲什么,说明不了文件是什么,单独定罪必误判。
体检报告每条都写了为什么和怎么改,照着改即可。E 级清零再往下走;
W 级看情况;I 级是提示,多数可以忽略。
改完校验器本身也跑一次自检,确认没改坏:
python tests/selftest_rules_lint.py
2.3 规则文件字段说明
| 字段 | 作用 |
|---|---|
archive_root |
归档根,分组的 dir 里用 {{archive_root}}/xxx 引用 |
skip |
不纳入决策:小于 N MB 的碎片、明确噪音词、黑名单扩展名 |
protected |
保护区:命中即进「待人工」组,永不给删除建议 |
groups[].priority |
数字越小越先匹配——顺序错了会出事故 |
groups[].any / all / not |
命中任一 / 必须全部命中 / 命中即排除 |
groups[].ext |
扩展名白名单,空 = 不限 |
groups[].need_context |
短词与通用词专用:命中后不直接归类,转「待确认」。它本身就是判据,不必同时写进 any |
groups[].default_action |
移 / 删 / 留 / 看(别轻易写「删」) |
跑分类:
python scripts/classify_generic.py \
--inventory ./work/inventory.csv --rules ./rules.json \
--out ./work/batch1_ID映射.csv --prefix B
输出映射 CSV,列:ID, 文件名, 大小MB, 月份, 分组, 默认动作, 默认目录, 源路径, 判断依据。
同时打印分组分布与跳过原因 TOP5,以及一行自检提示(映射行数必须 == 面板 DATA 条数)。
2.4 分类方法论(五条,与行业无关)
完整版在 references/classification-guide.md,这里只列标题,写规则时必须过一遍:
- 漏斗原则:先具体后宽泛——三种典型顺序事故(合同被客户吞、分享稿被客户吞、服务商被当客户)
- 排除规则必须分三级力度(HARD / STRONG / SOFT)——「解决方案」这类词放错级别会误伤一批白皮书
- 误命中防呆:品牌名=通用词、项目名≠主体名、短词要有上下文、扩展名要隔离
- 阈值:小于 1 MB 的碎片数量占一半、体积只占百分之一二,不值得逐个决策
- 主题词 ≠ 类型词——见下面 2.6,这是「同事发来的工作文件被当成行业报告」的根因
2.5 保护区(命中即人工,绝不批量删)
| 类别 | 理由 |
|---|---|
| 录音、会议转录 | 不可再生 |
| 合同、报价、授权书、发票 | 版本差异是实质性的(主体、金额不同) |
带编号的图片/音视频(图(5)) |
编号是素材序号不是版本号 |
| 盖章/签署版 PDF | 盖章版常是 PDF,docx 原件反而是未盖章草稿 |
| 证件、体检、病历、家庭材料 | 个人资料 |
| 简历 | 涉及他人个人信息 |
| 用户勾「删除」但同批有同类文件被勾「保留」 | 高度疑似误勾,暂缓再问 |
2.6 文件名会骗人:主题词 ≠ 类型词
真实事故:两份内部数据分析 PPT,文件名是「XX分群复购产品消费者洞察.pptx」,
被规则里的「洞察」关键词判成行业报告,搬进了报告库。
打开第一页就能看到 XX指数=(特征在目标群体中的占比%/特征在总体中的占比%)*100
——这是同事做的数据分析,不是第三方出版物。
根因:洞察 / 分析 / 研究 / 趋势 / 观察 / 白皮书 / 报告 是主题词,不是文档类型词。
四类完全不同的文档都可以叫「XX洞察」:行业报告、内部汇报、客户提案、竞品拆解。
真正的类型特征是另外三样,累加计分、凑够才归类,凑不够降级为待确认:
| 特征 | 例子 | 实测覆盖率 |
|---|---|---|
| 发布年份 / 期号 | 2026、第57期、Vol.3 |
70% |
| 机构署名 | XX研究院、XX咨询、XX证券、公司全称 |
25%,命中即采信 |
| 法律声明 | 免责声明、版权归…所有、未经授权不得转载 |
命中即采信 |
⚠ 不要写成"三选二"。 在 1249 份真实报告上量过:机构署名只有 25% 能检出,
一旦把它当必要条件(要求"必须有机构"),会漏掉四分之三的真报告——
要求"机构 且 年份"同时成立时,只能认出 20.4%。
正确用法是命中即加分,没命中只是少一分,不是反证。
四道防线,按发生顺序排,越靠前越便宜。建议全部做:
| # | 时机 | 防线 | 做法 | 成本 |
|---|---|---|---|---|
| ① | 分类之前 | 规则体检 | rules_lint.py 静态扫描规则,把"主题词单独定罪""缺类型证据"等模式拦在执行之前 |
秒级 |
| ② | 写规则时 | 规则层 | 主题词写进 need_context 而不是 any,单独命中不归类 |
改一次 JSON |
| ③ | 归档之后 | 内容层 | content_probe.py 读首屏文字反向抽查 |
几分钟 |
| ④ | 出事之后 | 可回滚层 | 执行器必写移动清单,判错了 rollback.py 原路搬回 |
零 |
防线 ① 是后来补的。起因是复盘那次事故时发现:规则设计原本写的是
「报告特征词 + 年份」两个条件,但执行时用的是另一个脚本,那里只留了单条件
「关键词=洞察」——年份条件在从设计落到执行的过程中丢了。 而那份文件名里
恰好没有年份。规则本身没错,是没人检查"写下来的规则"和"跑起来的规则"
是不是同一条。rules_lint.py 就是用来堵这个口子的。
# ① 规则体检:分类之前必做一步,有 E 级就必须改完再跑
python scripts/rules_lint.py rules.json
python scripts/rules_lint.py rules.json --out lint.md --strict # 出报告 + W 级也卡
# ③ 内容探针:先看信号词有没有噪声,再正式扫,最后出复核清单
python scripts/content_probe.py --dir "<归档目录>" --signals signals.json --audit-words
python scripts/content_probe.py --dir "<归档目录>" --signals signals.json --out probe.csv --cache probe.cache.jsonl
python scripts/probe_report.py probe.csv --out 复核清单.html
# ④ 判错了就捞回来
python scripts/rollback.py ./work/W类_移动清单_0831.csv --filter 洞察 # dry-run
python scripts/rollback.py ./work/W类_移动清单_0831.csv --filter 洞察 --go
rules_lint.py 查什么(有 E 级就退出码 1,可以直接卡在流程里):
| 级别 | 检查项 |
|---|---|
| E1 | 主题词单独定罪:规则只有 any 一个维度,且词表里含洞察/分析/研究/趋势等主题词 |
| E2 | 出版物类分组缺类型证据:组名像"报告/白皮书",判据里却既无年份也无机构词 |
| E3 | 兜底层默认动作是"删":没被任何规则认领的文件会被直接建议删除 |
| W1 | 宽泛词排在具体词前面:词 A 是词 B 的子串且 A 先匹配,B 永远拿不到文件 |
| W2 | 排除词力度过重:解决方案 这类双面词放靠前的层排除,会误伤一批白皮书 |
| W3 | "删"类规则没有体积下限:碎片文件也被建议删除,收益极小、误删面扩大 |
| I | 单维判据无辅助 / 没有保护区 / 两字词直接当判据(可忽略) |
体检只覆盖结构性风险。词表本身贴不贴合你的文件,仍要靠 suggest_rules.py
归纳后人工确认——这两件事不能互相替代。
content_probe.py 的关键设计(都是实测踩出来的):
- 信号词分 strong / weak 两级权重。只按「命中任一词」判定,一份厂商公开方案
会因为正文里出现一次大厂名就被判成内部产出。强词(行业黑话、口径术语、版本标记)
命中 1 个即成立;弱词(大厂名、渠道名、通用商务词)要攒够 3 个不同词。
实测同一批文件:加权前 31 个疑似内部,加权后 8 个,误报降了 3/4。 - 必做一次
--audit-words,揪出噪声词。实测里「微信」在 1249 份真报告中命中 54.6%,
放进 internal 直接把探针搞失效。 --cache缓存正文。解 1000+ 个 PDF 要几分钟,调信号词要反复迭代,
有缓存后第二次 2 秒出结果。- 机构署名单列一组
orgs(可选,老词表没有这个键也能跑)。「谁发的」比「讲什么」
硬得多。之所以不直接并进published.strong:同一个机构会被切成好几个词
(XX大学国家发展研究院里同时含XX大学和研究院),混在一起就重复计分,
看着像命中一堆证据,其实只有一家机构。单列后判定只看「有没有机构」,不看「命中几个词」。
具名机构命中 1 个即采信;半具名(XX学院/XX研究中心)并进 published 弱词池一起攒。
实测:加入 40 个具名机构后,「无信号」341 → 333(8 份靠机构定案),
疑似内部一个没变 —— 纯增量、零误伤。 - 机构是加分项不是门槛:命中就采信,没命中不等于不是报告。实测 1247 份里机构只覆盖
10.7%(具名)/ 24.7%(含泛化后缀),大量报告首屏根本不写机构。 - 探针有盲区:图片型 / 扫描件 PDF 抽不出文字。实测 267 个「无信号」PDF 里
147 个属于这类,「无信号」不等于「没问题」。
阶段 2.7:混合态文件 —— 人工决策看板(按需,非必做,位于阶段 2 共议内)
探针会扫出一类「两边都像」的文件:同时命中 internal 和 published 信号
(实测 1247 份里有 52 份,约 4%)。
⚠ 先算账,再决定要不要开这个环节(实测教训)
2026-09-06 实测:1247 份 → 58 份进入人工决策 → 最终只挪动 2 份(0.16%)。
人工决策环节的假阳性率 97%。让人盯 58 份找出 2 份,ROI 极低。
所以开这个环节前先自问两件事:
- 默认保留的代价是多少? 如果错放率已经 <1%,
直接一律保留,把省下的时间用到「新文件入库拦截」上。 - 这些混合态文件是不是已经清过几轮了? 存量目录清过 2 轮以上,
剩下的基本都是灰色地带的泛资料,再抠也抠不出东西。
判断标准(满足任一就不开): 错放率 <1% · 已治理 2 轮以上 · 混合态 >50 份。
# ① 生成可勾选的 HTML(--targets 预置常用归档目录,逗号分隔;不填则只能手填)
python scripts/build_decision_board.py --csv probe.csv --out 决策看板.html \
--targets "D:/归档/客户资料,D:/归档/分享资料"
# ② 使用者在页面上勾选:保留 / 移出 / 删除,导出 CSV
# ③ 预演(默认 dry-run,不动任何文件)
python scripts/exec_decisions.py 决策清单.csv
# ④ 确认计划无误后真执行
python scripts/exec_decisions.py 决策清单.csv --apply
页面能力:三选一 + 目标目录下拉/手填 + 筛选(未决策/保留/移出/删除)+ 按可疑度排序
+ 自动存 localStorage(刷新不丢)+ 导出 CSV/JSON + 一键「未决策的都保留」。
exec_decisions.py 的安全设计(没有这些就别交付):
| 措施 | 说明 |
|---|---|
| 默认 dry-run | 不加 --apply 只打印计划 |
| 执行前预检 | 源文件不存在 / 移出未填目标 / 目标目录不存在,全部拦下并退出码 1 |
| 删除走回收站 | 调 recycle_delete.py(SHFileOperation + FOF_ALLOWUNDO),可还原 |
| 移动校验 MD5 | 搬完比对,不一致就记录「移动后MD5不一致」 |
| 写执行日志 | exec_log_<时间戳>.csv,含源路径、目标、结果、MD5,可回滚 |
预标注的三个坑(都踩过):
- 别把公司名/产品名当内部信号。使用者所在公司的官方白皮书是公开行业资料,
不是内部文件;讲协同办公的报告必然提到该司的会议产品名。这类词和「微信/小程序」
一样是噪声,放进强信号会误伤。分两级:强信号(内部资料/保密/立项/验收,+3 分)、
中等信号(公司名/产品名/作者,+1 分)。 - 噪声词要在页面上显式划掉。把
微信 / 小程序 / 版本号 / 口径 / 复盘 / 转化率
这类渲染成灰色删除线,使用者一眼能看出「这个证据不成立」,比藏起来更可信。 - 预标注只排序不拍板。给「保留 / 看一眼 / 移出」三档建议 + 一句判据理由,
最终决定权在人。 - 🔴 先验区分力,再调参数(本条最重要,写在这里是因为我栽过)。
2026-09-06 实测:预标注建议移出 5 个,使用者采纳 0 个,假阳性率 100%。
根因不是权重调得不好 —— 是「正文内部词」这个特征本身区分力为零
(58 份待决策文件 58 份全命中,包括保留的 56 份)。
零区分力的特征,怎么调权重都是噪声。
动手做预测前,先跑一张「特征 × 决策」对照表,例如:
| 特征 | keep | move | delete |
|---|---|---|---|
| 正文有内部词 | 56 | 1 | 1 |
| 文件名含客户名 | 0 | 1 | 0 |
一个把所有样本都判成正例的特征 → 信息量为零 → 直接弃用,别再调它。
样本极度不平衡时(正例只有个位数),任何统计规律都不可靠,直接承认机器判不了。
阶段 3:去重(可选,跑在阶段 2 盘点之后、阶段 4 分类之前)
> 先去重能让分类阶段少搬一堆垃圾;反过来先分类后去重,归档基线会一直在变,对不上账。
3.1 三步走
# 1) 扫描:只读,算出重复组并套四道保护
python scripts/dedup_scan.py --root "<微信 file 目录>" \
--inventory ./work/inventory.csv --out-dir ./work
# --inventory 可选:裁剪到盘点范围,并复用里面的 MD5 列省一大笔时间
# 产物:去重分组.csv(看板输入)/ 去重_ID映射.csv(执行输入)/ 去重统计.json
# 2) 出决策面板
python scripts/gen_dedup_panel.py --groups ./work/去重分组.csv \
--out ./work/去重面板.html --key dup1
node scripts/check_html_js.js ./work/去重面板.html # 四项全绿才交付
# 3) 面板拍板 -> 复制结果文本 -> 执行(复用阶段 6 的执行器,安全网零改动继承)
python scripts/apply_batch.py 决策结果.txt --map ./work/去重_ID映射.csv --tag dup1
python scripts/apply_batch.py 决策结果.txt --map ./work/去重_ID映射.csv --tag dup1 --go
# 4) 去重后必须重跑盘点,再往下做分类
python scripts/scan_inventory.py --root "<file 目录>" --out-dir ./work
3.2 三类组
| 类型 | 判据 | 默认建议 | 风险 |
|---|---|---|---|
| exact 完全重复 | MD5 字节级一致 | 保留最新,删其余 | 零风险,删副本没有任何信息损失 |
| version 版本重复 | 同一扩展名内、规范化名相同、内容不同 | 保留最新,删其余 | 中,必须过四道保护 |
| format 跨格式 | 可编辑原件 + pdf/图片导出件并存 | 全部保留,不预勾任何删除 | 高,只提示不自动判 |
一个文件只进一个组,优先级 exact > version > format,天然无 C/D 叠加冲突
(这是老版 dedup_analyze.py 的坑:C 判删、D 判留,同批撞车 10 个)。
norm() 规范化名:去扩展名 → 去 (1)(2) → 去尾部 v1/v1.2 → 去 8 位以上日期 → 去空白 → 转小写。
3.3 四道保护(version / format 生效,命中整组不预勾删除)
| # | 保护 | 为什么 |
|---|---|---|
| 1 | 图片/音视频不参与版本去重 | 插画 (5)~(13).png 是十个素材,编号是素材序号不是版本号 |
| 2 | 保留项体积 < 被删项 × 50%(pdf 用 70%) | 方案(1).docx 只有 0.03 MB 却要删 25 MB 带图版,那是精简版不是新版 |
| 3 | 合同/协议/报价/发票/确认函/证件/简历整组不删 | 省不了多少空间,丢了是事故 |
| 4 | format 组里 pdf 体积 > 原件 3 倍 | 导出件比原件大这么多,很可能含独立内容 |
exact 组不走保护:字节级完全一致,留一份删副本没有信息损失。
保护规则针对的是"版本差异可能有实质内容"(主体、金额、盖章不同),不适用于副本。
实测参考:C90 组 / D36 组原始可删 156 个 0.81 GB,四道保护拦下 71 个(约 45%),最终安全删 105 个。
3.4 决策面板能做什么
| 粒度 | 操作 |
|---|---|
| 整组 | 保留最新删其余 / 保留最大删其余 / 全部保留 / 恢复默认 |
| 单文件 | 每个文件单独点「保留」或「删除」 |
| 保险 | 每组至少保留一份——手动把整组全标删除时会被拦下并提示 |
| 其他 | 按类型筛选、只看被保护组、搜索文件名、展开折叠、预览、打开所在文件夹、localStorage 按 --key 存 |
被保护或被判 format 的组,卡头会写明原因(红底/黄底),默认一个都不删。
3.5 对账
- 映射 CSV 行数 == DATA 条数(
gen_dedup_panel.py会打印,必须一致) - dry-run 第一行「删除 N / 保留 M」必须满足
N + M == 面板总条数 - 执行后确认每一组都还剩至少一份(删完跑一次
dedup_scan.py,组数应显著下降)
阶段 4:分类 → 决策看板
4.1 生成
python scripts/gen_panel.py \
--map ./work/batch1_ID映射.csv --out ./work/决策面板.html \
--title "微信文件清理 · 决策面板" \
--subtitle "待决策文件清点" \
--key wxclean_batch1 \
--result-title "【微信文件清理 · 决策结果】" \
--dirs-root "<你的归档根>"
--dirs-root:目录自动补全的候选根(填你自己的归档根目录)--tips:可选 JSON{分组名: [默认动作, 说明, 色值]},覆盖自动推导的组说明与配色- 映射 CSV 必需列:
ID, 文件名, 大小MB, 月份, 分组, 默认动作, 默认目录, 源路径
> 生成器会打印「目录树 N 条」。N=0 说明你的归档根下还没建子目录,
> 面板里就没有补全候选,只能手敲全路径——先按 references/taxonomy.example.md 把目录树建好再来。
4.2 固化能力清单
| 能力 | 说明 |
|---|---|
| 懒渲染 | 组里 >60 条自动折叠,几千条也不会卡死 |
| 组级一键动作 | 全部移动/删除/保留 |
| 组级跳过已选行 | 已做过选择的文件保持不变,只改没选的,toast 报告保留数 |
| 填目录自动勾移动 | 行级/组级填了目录 → 自动勾「移动」(填了目录就是要移动) |
| 文件名就地编辑 | 可改名 + 批量清脏后缀((1) ~$ 等) |
| 目录自动补全 | datalist 内置归档根的两级目录树 |
| 跨组搜索 | 按文件名搜索 |
| 👁 预览 / 📂 打开文件夹 | 新标签页打开 |
| ID 区间压缩输出 | B0001-B0036 而不是 36 行,防结果文本被截断 |
| localStorage 批次隔离 | 按 --key 存,每批次必须不同,否则串批 |
目录选择只能手填 + 自动补全,别再试别的方案:原生「选择文件夹」按钮
(showDirectoryPicker 只给 handle 不给路径)、<input webkitdirectory>(只有相对路径)、
拖文件夹(交互不稳定)——三条路都验证过不可行或体验差。
4.3 交付前双绿校验(缺一不可)
node scripts/check_html_js.js ./work/决策面板.html
检查:JS 语法 / 事件里调用的函数是否都有定义 / DATA 条数 + ID 唯一 / 标签配对。
外加一条:映射 CSV 行数 == DATA 条数。曾经漏写 writerows 让映射表只剩表头,
是靠执行阶段报错才暴露的。
阶段 5:回传与双对账
5.1 回传三段式
- 结果文本(面板「生成结果文本」复制出来的 TSV):
ID区间 \t 动作 \t 目标目录 [\t 新文件名] B0001-B0036 move <归档根>/学习资料 B0041 move <归档根>/素材 新名.pdf
未选 = 不动,不输出。delete可以只有两列。 - 映射 CSV(阶段 4 产出,执行器靠它反查源路径)
- 执行 + 反查:
apply_batch.py用 ID 反查源路径,校验后执行
5.2 三数对账(硬要求,不做会漏文件)
解析出的决策数 + 未选数 == 面板总条数
实战教训:有一次回传 937 项、面板 1080 条、未选 143,937+143=1080 ✓,看起来对上了。
但对账之前,解析器有个 bug 把「delete 且无目标目录」的两列行静默跳过了——
838 个删除只解析出 119 个。是拿总数对账才发现的。
> 所以:dry-run 第一行输出「移动 N / 删除 N / 保留 N」,必须拿去和面板数字对账,
> 对不上先查解析器再执行。
阶段 6:安全执行
python scripts/apply_batch.py 决策结果.txt --map ./work/batch1_ID映射.csv --tag batch1 # dry-run
python scripts/apply_batch.py 决策结果.txt --map ./work/batch1_ID映射.csv --tag batch1 --go # 执行
执行顺序与安全规则:
- 删除:先完整备份 + MD5 到
_delete_backup_<tag>_<MMDD>,再走回收站(分批、失败即停) - 移动:
os.replace+ 移动前后 MD5 比对,不一致即报失败 - 目标同名 → 自动加
(副本),绝不覆盖 - 同批内同名(两个文件移到同一目录且同名)→ 视为不同版本,自动加
(来自<源子目录>)后缀共存 - 改扩展名一律拦截(防手滑把
.pdf写成.pdf.txt) - 区间两端 ID 前缀/位数不一致、ID 不存在、源路径消失 → 全部报错中止
常见残留:Office 文件被 Word/WPS 锁住会移动/删除失败。不要强杀进程——
那可能正是用户正开着的文件。报告路径,等用户关闭后补处理。
阶段 7:收尾
- 先复查,别急着收尾:对这次搬进归档目录的文件跑一次
content_probe.py
+probe_report.py。文件已经离位了,但捞回来只要一条命令(见 2.6 ③)。
这一步在真实清理里抓出过「内部数据分析被当成行业报告」这类误判。 - 清空回收站(单独获批):先跑
scripts/inspect_recycle_bin.py解析$I元数据,
还原每条已删文件的原路径/大小/时间,与删除清单比对,确认回收站里没有用户的其他残留,才清空。 - 对账:
映射源路径仍存在数vs盘上实际文件数,差异项列出来(通常是期间新收到的文件)。 - 规模记录:记下这一批的移动/删除量级、踩到的新坑、待办项(被锁文件、未闭环的决策)。
附录 A:归档后的二次去重(A 类对照)
> 主力的去重在阶段 3(dedup_scan.py,微信目录内部找重复,配决策面板)。
> 这一节只讲阶段 3 覆盖不到的那一层:微信目录与你的归档目录之间的对照。
> 时机是阶段 6/7 归档搬完之后——那时归档侧才有东西可对照。
A.1 对照层(微信侧 ↔ 归档侧)
| 层 | 判据 | 风险 | 处理 |
|---|---|---|---|
| A | 与归档目录内文件同名 + 同大小 | 零(归档侧已有) | MD5 交叉校验后可删微信侧副本,无需转移 |
exact / version / format 三类已并入阶段 3,见上文 3.2。四道保护同样在 3.3,这里不重复。
A.2 为什么 version / format 叠加在新版不会再撞车
老版 dedup_analyze.py 里 version 和 format 分别扫描同一批文件,
会出现「同一文件被 version 判删、被 format 判留」——首次运行抓出 10 个冲突
(三个版本的 pptx 同时被 version 判删、被 format 判留)。
新版 dedup_scan.py 用一个文件只进一个组(优先级 exact > version > format)从结构上消除了冲突,
不需要再做「删除集 ∩ 保留集 = ∅」的断言。老脚本保留在 scripts/ 里仅作历史参考。
A.3 删除前 MD5 交叉校验
A 类逐一对微信侧与归档侧算 MD5,只有一致才进最终清单;不一致的写进日志并排除。
同名 + 同大小 ≠ 内容一致,这条硬规则(见 Hard rules 2)在这里最容易翻车。
附录 B:脚本工具箱
skills/wx-file-clean-ty/
├── SKILL.md 本文件
├── references/
│ ├── rules.example.json ★ 规则模板(复制成 rules.json 改)
│ ├── classification-guide.md ★ 分类方法论四条
│ ├── signals.example.json ★ 探针信号词模板(strong/weak 两级 + orgs 机构组)
│ └── taxonomy.example.md ★ 归档树设计指南 + 三套骨架
├── scripts/
│ ├── locate_wechat.py ★ 自动定位微信文件目录(跨平台,辨链接)
│ ├── scan_inventory.py ★ 只读盘点 + 画像 + inventory.csv
│ ├── suggest_rules.py ★ 从真实文件归纳规则草案 + 五段确认表
│ ├── rules_lint.py ★ 规则体检:分类之前拦住结构性风险(E/W/I 三级)
│ ├── classify_generic.py ★ 规则驱动分类器(读 rules.json → 映射 CSV)
├── gen_panel.py ★ 决策面板生成器
├── panel_template.py ★ 面板固定主体(gen_panel 依赖,别手改)
├── apply_batch.py ★ 回传结果批量执行器(产出移动清单 = 回滚凭据)
├── check_html_js.js ★ 面板交付校验(JS语法/事件函数/DATA/标签)
├── content_probe.py ★ 内容探针:读首屏文字判「内部产出/正式出版物」
├── probe_report.py ★ 探针结果 → 可点击复核清单 HTML
├── rollback.py ★ 按移动清单原路回滚(误判归档的补救手段)
├── build_decision_board.py ★ 混合态文件 → 可勾选决策 HTML(含预标注)
├── exec_decisions.py ★ 执行决策 CSV(默认 dry-run,删除走回收站)
├── recycle_delete.py ★ 删到回收站(exec_decisions 依赖,可独立用)
├── merge_dirs.py ★ 重复目录合并(同名冲突检测 + MD5 + 可回滚)
└── analyze_decisions.py ★ 特征区分力检验(做预测前必跑,见阶段 2.7)
├── dedup_scan.py ★ 去重扫描:exact/version/format 三类 + 四道保护
├── gen_dedup_panel.py ★ 去重决策面板(整组一键 + 单文件拍板)
├── dedup_analyze.py 老版四层去重(仅历史参考,见附录 A.2)
├── exec_recycle_delete.ps1 回收站删除(纯 ASCII 无 BOM)
└── inspect_recycle_bin.py 回收站 $I 解析盘点
└── tests/
├── selftest_rules_lint.py ★ rules_lint 自检(改完校验器跑一遍)
├── selftest_content_probe_orgs.py ★ 探针机构组自检(含向后兼容反例)
├── selftest_dedup.py ★ 去重自检(norm + 四道保护反例 + 端到端冒烟)
└── selftest_panel_behavior.js ★ 面板交互行为自检(node,改完 panel_template.py 跑)
脚本通用化约定:任何绝对路径都不写死在代码里,一律走命令行参数或 rules.json。
改规则不改代码——这是这套工具能分享给别人的前提。
附录 C:规模参考
这一节的作用:让第一次做这件事的人有个量级感,知道大概要花多久、能期待什么结果。
数字来自一次真实清理,只保留规模信息。
一次典型清理:微信文件目录 3516 个文件 / 24.4 GB,分十个批次处理完,
累计移动约 1500 个 / 12.6 GB、删除约 1700 个 / 8.7 GB。
值得记住的三件事:
- 分类 + 决策面板贡献了主要减量(约 86%),去重只贡献约 14%。
别一上来就埋头找重复文件。 - 保护规则拦下的比例很高:去重阶段原始判定可删 156 个,四道保护拦下 71 个(约 45%)。
自动判定必须配保护规则,不然就是在赌。 - 对账救回过 719 个文件:解析器静默跳过了一批删除项,是靠「决策数 + 未选数 == 总条数」
这个等式发现的。这一步不能省。
Pitfalls(按阶段)
阶段 1 定位
- 链接目录不是副本,它的体积不等于占用空间,删除/断开会直接破坏微信文件目录。
msg/attach体量可能比msg/file大数倍,两者性质不同,别混为一谈。- 微信的文件管理路径可以在设置里改,注册表或默认路径都可能不准 —— 找不到就问用户。
阶段 4 分类
- priority 顺序错了会出事故:合同必须排在客户名之前,分享稿必须排在客户名之前,
服务商必须排在客户名之前。 - 泛词不能当独立判据。把
AI、Agent、解决方案这类词单独当判据,会把第三方的
报告全捞进来,数量虚高数倍。 - 主体名单里混进项目名/活动名 → 凭空生成假目录。只放品牌或公司名。
- 短词直接归类必错 → 走
need_context转人工确认。 - n-gram 会切出边界怪词(「周会」+「会议记录」→「会会议记录」)。
这类怪词比正确切法更长,会被「长词优先」的冗余过滤错误地保留。
suggest_rules.py用「相邻重复字」信号剔除它们,自己写脚本时要记得这层过滤。 - priority 别按体积排 → 按「命中文件数越少越具体」排,否则宽泛词吞掉具体词。
- 分类脚本被中断会写截断 → 恢复后先
wc -l+tail检查完整性再运行。 - 主题词当类型词用必出事故 → 「洞察」把同事发的数据分析搬进了行业报告库。
主题词只能进need_context,判定类型靠「年份期号 / 机构署名 / 法律声明」累加计分。 - 把低覆盖率特征当必要条件 → 机构署名在真报告里只有 25% 能检出,
拿它当门槛会漏掉四分之三。低召回特征的正确用法是"命中即加分"。 - 规则设计写了多条件,执行时退化成单条件 → 那次事故里 L3 层设计写的是
「报告特征词 + 年份」,执行脚本里却只剩「关键词=洞察」,而文件名恰好没有年份,
两个条件的保险丝被抽掉了一根。规则定稿后必跑rules_lint.py,
它专门查这类"设计有、执行没了"的口子。 - 校验器自己也会写错 → 判断"规则里有没有年份证据"时曾写成
(19|20)\d{2},
要求 20 后面跟两个字面数字,结果re:20\d{2}(\d是正则元字符)识别不出来,
E2 检查长期形同虚设而测试一直是绿的。
教训:判据识别逻辑要有含反例的对照测试(tests/selftest_rules_lint.py),
尤其要包括"不该被识别"的写法,比如版本号re:v\d+。 - 同一文件在多版清单里被判成两类时,必须做冲突断言,取后出的、证据更强的那版。
实测:同一份 PPT 在transfer_all.csv里是report/洞察,
在transfer_all2.csv里是work_maybe/biz(后者才对),
但最后执行用的是前者 —— 中间产物不收敛,正确判定会被先出的错误判定盖掉。 - 探针的「无信号」不是免检证 → 图片型 PDF 抽不出文字(实测 267 个无信号 PDF 里
147 个属于此类,共 1.5 GB),探针看不见,只能靠文件名或人工。
阶段 3 去重
- exact 组不要套合同/报价保护。字节级完全一致的文件,留一份删副本没有任何信息损失;
保护规则针对的是"版本差异可能有实质内容"(主体、金额、盖章不同)。
实测把「报价单模板」当保护词时,一组纯副本被白白拦下。 --min-mb会吃掉"精简版"。保护 2 的经典场景是方案(1).docx只有 0.03 MB 却要删 25 MB 带图版,
但 0.03 MB < 默认阈值 0.1 MB,这个文件根本进不了扫描,组都成不了,保护也就无从谈起。
想抓这类,把--min-mb调到 0.02 再跑一遍。- 去重后必须重跑盘点。删完文件数变了,
inventory.csv就是脏的,拿去分类会对不上账。 gen_dedup_panel.py的 onclick 里不要写内联表达式。check_html_js.js会把
onclick="openFile('...'.replace(/'/g,''))"里的replace当成未定义的函数调用报错。
正确做法是 onclick 只传 ID(openFile('D0001')),路径从GINDEX里取。- 老版
dedup_analyze.py的 C/D 会撞车(同一文件既判删又判留,首次跑抓出 10 个冲突)。
新版用一个文件只进一个组规避,老脚本别再用了。
阶段 4 看板
--key每批次必须不同,localStorage 按 key 存选择,重名会串批。- 生成器漏写
writerows(mp_rows)→ 映射 CSV 只有表头。交付前校验行数 == DATA 条数。 - 模板内部用三双引号分隔 CSS/JS,外层必须用三单引号包裹,别改。
- 改
panel_template.py的交互逻辑,语法校验不够,必须跑行为测试:
node tests/selftest_panel_behavior.js <生成的面板.html>。
实例:docstring 声称「填目录自动勾移动 / 组级动作跳过已选行」,实际两者都没实现——
setDir()只存目录不勾移动,setGroup()无条件覆盖全组。2026-09-06 修复后补的行为测试
含 8 项断言(含「组级动作不覆盖单独拍板行」这类回归点)。 - 组级动作只作用于未选行(已单独拍板的一律不动);行内填/改目录自动把该行勾成「移动」
(清空目录不动已有决策);组级填目录同样是「目录应用到全组 + 未选行自动勾移动」。
阶段 5 回传
- 「delete 无目标目录」是两列行,解析器若要求 ≥3 列会静默跳过(838 → 119 的血案)。
- dry-run 数量必须和面板对账:
决策数 + 未选数 == 总条数。
阶段 6 执行
- PowerShell 输出是 GBK,
subprocess用text=True会 UnicodeDecodeError →
不带text=True,手动decode('gbk')。 .ps1必须纯 ASCII 无 BOM,否则中文路径乱码。- Office 文件被 Word/WPS 锁住会失败,不要强杀进程,报告路径等用户关闭。
- 同盘移动不释放空间,别在报告里算成「已释放」。
- 改过的脚本必须端到端跑一遍,语法检查抓不到运行时错误。
实例:把a.go写成go,ast.parse完全通过、dry-run 前几行也正常输出,
直到--go那一刻才 NameError。只对解析器做单元测试是不够的,
必须造一批测试文件把「定位 → 盘点 → 分类 → 面板 → 执行」整条链路跑通。
阶段 7 收尾
- 对账时路径要先
os.path.normpath——walk 出的路径混着/和\,
集合比对会全部失配,假报「映射外新增」。 - 清空回收站前必须解析
$I元数据,确认里面没有用户此前删的其他东西。
Verification
- [ ] 已用
locate_wechat.py定位,并确认目标是file类而非attach类 - [ ] 链接(junction/symlink)已识别,未把它当可释放空间上报
- [ ] 规则来自使用者自己的文件(suggest_rules 归纳),不是照抄的模板
- [ ] 五段确认表(A-E)已交给使用者逐条确认并收到反馈
- [ ] 草案里每个组都带
_evidence证据,没有凭感觉加词 - [ ]
rules_lint.py体检已跑,E 级清零(E1 主题词定罪 / E2 缺类型证据 / E3 兜底层删) - [ ] 分类阶段只读,没有改过任何文件
- [ ] 规则里没有把主题词(洞察/分析/研究/趋势)单独当类型判据
- [ ] 多版分类清单已做冲突断言(同一文件被判两类时取哪一版有明确结论)
- [ ] 映射 CSV 行数 == 面板 DATA 条数
- [ ]
check_html_js.js四项全绿 - [ ] 去重(若做了)跑在分类之前,且执行后已重跑
scan_inventory.py - [ ] 去重执行后每一组都还剩至少一份(删完再跑一次
dedup_scan.py复查组数) - [ ]
--key与历史批次不重名 - [ ] dry-run 三数对账通过(决策数 + 未选数 == 总条数)
- [ ] 删除前备份 + MD5 清单已生成
- [ ] 移动清单已落盘(含源路径/目标路径),确认可用 rollback.py 回滚
- [ ] 执行后残留项已列出并说明原因(多为文件被占用)
- [ ] 归档目录已用 content_probe.py 反向抽查过,疑似内部产出的已复核
- [ ] 已提醒「清空回收站才真正释放空间」且未擅自清空
开源与分享
这套工具设计成可以直接分享给别人:脚本不含个人信息,使用者只需要填一份 rules.json。
分享前自查:
rules.json是使用者私有的,不要随技能一起发布(模板用rules.example.json即可)inventory.csv、映射 CSV、决策面板 HTML 都含真实文件路径,属于产物,不入库- 备份目录
_delete_backup_*是文件副本,更不入库
给别人用时的最短路径:
python scripts/locate_wechat.py
python scripts/scan_inventory.py --root "<目录>" --out-dir ./work
python scripts/suggest_rules.py --inventory ./work/inventory.csv --out ./work/rules.draft.json
# ↑ 把输出的 A-E 五段交给使用者确认,反馈后定稿成 rules.json
python scripts/classify_generic.py --inventory ./work/inventory.csv --rules ./rules.json --out ./work/batch1.csv
python scripts/gen_panel.py --map ./work/batch1.csv --out ./work/决策面板.html \
--title "微信文件清理" --subtitle "待决策文件" --key wxclean_batch1 \
--result-title "【微信文件清理 · 决策结果】" --dirs-root "<归档根>"
node scripts/check_html_js.js ./work/决策面板.html