项目垃圾清理(扫描 → 分档 → 断言删除 → 留清单)
用户口径铁律:清理范围由用户拍板,不许自己扩档。
本技能的价值不是"删得多",而是删得敢删、且事后能追溯。
0. 铁律:先全盘扫描,再看已知目录
★ 我在 2026-09-16 就栽在这里:交接单说"某文件夹里有 8 个试制品",
我就只 glob 了那个文件夹,清掉 145 个 .old 后一补扫 —— 全盘实为 1603 个。
已知目录只是线索,范围必须靠 os.walk 全盘数出来。
for r, d, fs in os.walk(BASE):
for f in fs: ...
统计口径给两项:(占用 MB, 文件数)。用 Python 算,别用 shell 的 stat
(Git Bash 下 find | while read | stat -c%s 遇到中文路径会返回 0 —— 本次实测被骗过一次)。
1. 分档报价(用户只挑一档,别自作主张合并)
按「删了会不会后悔」分两档,先把数报清:
| 档 | 判据 | 例子 |
|---|---|---|
| 零风险 | 删了没有任何信息损失 | *.old* 覆盖残留、_work_*.tmp 临时工程、排查中间 png、已逐字节验证过的重复 zip、纯缓存目录 |
| 需点头 | 能再生 / 是上一代,但要付出重跑成本 | 渲染掩膜缓存 _mask*、分类缓存、旧版 shp/tif、一次性脚本与笔记 |
报价时必须同时给「再生代价」(如"掩膜删了要重跑 prep_*_maps,慢"),
否则用户没法拍板。
2. 三条删除前断言(每条都是从事故边缘拉回来的)
2.1 .old* 墓碑:现役件必须存在 ★最重要
os.rename(p, p+'.old') 式覆盖会留下 X.old、X.old.old.old.old(每覆盖一次叠一层)。
删前断言去掉首个 .old 及其后所有后缀的"现役件" X 仍然在:
f = os.path.basename(p)
i = f.find('.old')
base = os.path.join(os.path.dirname(p), f[:i]) # town.cpg.old.old → town.cpg
if not os.path.isfile(base):
keep.append(p); continue # 现役件缺失 → 这是唯一副本,跳过并报出来
os.remove(p)
这条断言让"批量删墓碑"从危险操作变成安全操作,永远照做。
2.2 重复压缩包:逐文件 sha1 比对,别信文件名/大小
A.zip 与 A/ 目录同名不等于内容相同。本次删 139 MB zip 前跑了:
for n in zipfile.ZipFile(z).namelist():
if n.endswith('/'): continue
assert sha1(z.read(n)) == sha1(open(os.path.join(base, n), 'rb').read())
15/15 全等才删。有一个不等就整包留着。
2.3 先写清单再动手
清单内容 = 每个待删文件的 路径 + 字节数 + sha1前12,外加 freed_bytes / failures。
落盘到 03_归档与废弃\_清理记录_YYYYMMDD\清理清单*.json。
顺序必须是"写清单 → 删除",不能边删边记。
3. 只读文件:WinError 5
GDAL / WPS / ArcGIS 产出的文件常是只读(-r--r--r--)。直接 os.replace / shutil.rmtree
会 PermissionError: [WinError 5] 拒绝访问。标准写法:
# 单文件
st = os.stat(p); os.chmod(p, st.st_mode | 0o200)
try: ...改/删...
finally: os.chmod(p, st.st_mode & ~0o222) # 还原只读
# rmtree
def on_rm_error(func, path, exc):
os.chmod(path, os.stat(path).st_mode | 0o200); func(path)
shutil.rmtree(d, onerror=on_rm_error)
4. 动文件前先看进程(防文件锁)
MSYS_NO_PATHCONV=1 tasklist | grep -iE "arcmap|arcgis|wps|excel"
- ArcMap 在跑 → 别动
.mxd(会删失败或拖挂)。 - WPS 在跑(导 PDF 残留很常见)→ 只挪该目录的 png/中间图,别碰 docx/pdf。
- 只挪不删更稳:
os.replace(p, os.path.join(dir, '_debug', name))。
5. 收尾:复检 + 报告"实际释放"
删除后必须重扫一遍,给出三项实测,别只说"已完成":
残留 *.old* : 0 个
残留 _mask* 目录 : 0 个
目录现总体积 : 128.2 GB(清理前 132 GB)
(du 的块计账与按字节求和会有差;报告时用同一口径说清。)
6. 本类项目的高频垃圾清单(遥感/财政类项目实测)
| 模式 | 性质 | 档 |
|---|---|---|
*.old / *.old.old.old.old |
os.rename(p,p+'.old') 覆盖残留 |
零风险(带 2.1 断言) |
_mask\ / _mask_prev\ / _mask_0915b\ |
出图掩膜缓存,逐 AOI×期次×乡镇 | 需点头(可重跑再生) |
_work_*.mxd |
ArcMap 临时出图工程 | 零风险 |
_旧图名.png / _cover_200.png / 封皮_*对比.png |
排查中间图落在正式交付夹里 | 零风险(挪 _debug\,先别删) |
_cache\(vidx_*.tif) |
分类缓存 | 零风险 |
_*.py 散落在会话目录 |
一次性脚本 | 需点头 |
与目录同名的 .zip |
可能是重复包 | 零风险但必做 2.2 校验 |
旧版 *.shp(v1/v2/v4/v5_fix*) |
已被现行版取代 | 需点头,逐个列给用户标 |
*.tar.gz + 同名解包目录 |
压缩包与解压内容并存 | 零风险(先数解包件数,完整才删包) |
extracted\(错位的第二次解包) |
同一景解了三遍 | 零风险,逐字节比对主件后删 |
_cache_*.gpkg / _sas_cache.json |
处理缓存 | 零风险(可重生) |
*.rrd |
ERDAS 金字塔 | 零风险(同名 .img 在即可重建) |
同一天多版本 X.shp/X_v2.shp/X_v3.shp |
当日迭代链(尺寸常完全相同) | 需点头,留最新 + 名字最"正式"的那个 |
_chk_*.csv / _diag_*.csv |
质检/诊断导出 | 零风险(工具可重生成) |
_cover_cmp\ / _verify_tmp\ |
版式试制 / 校对临时 | 零风险 |
注意:同名的旧版图件可能是在产交付物(例:作物长势_逐图斑.shp 1.05 GB,
是甲方点名要的"长势按地块做")→ 大字标出来,明确说"未确认前不动"。
8. ★ 执行期两个坑(2026-09-16 实测,都吃过亏)
坑 A:脚本超时被自动重跑 → 输出说"跳过/不存在",实际首次已经删光
删 6 GB 级大文件时,前台脚本会被超时掐断、stdout 整段丢失;系统随后自动重跑一次,
于是第二次看到的全是"不存在 → 跳过"。只信这次输出会误判成"一项都没删"(当时 39 项已删,
输出却显示跳过 29 项)。
- 判据:报告"不存在"的项里,若包含你几分钟前刚亲眼扫到的大文件,就是重跑假象。
- 正确做法:删完后另起一个只读 probe 脚本逐路径 os.path.exists 复核 + 重算目录体积,
以 probe 为准,再把首次删除补记进审计清单(note 说明两次执行)。
- 预防:单批删除控制在 ≤5 GB,或改用后台运行。
坑 B:python -c "...\..." 里的反斜杠会被 shell 吞
r'...\' + x 经 bash 传递后变成 \,直接 SyntaxError: unterminated string literal(本次两次)。
- 规则:凡是跨多行、或含中文路径拼接的脚本,一律 Write 成 .py 再执行,不要用 -c。
坑 C:find / stat 在 Git Bash 下对中文路径会静默失败
stat -c%s 拿不到中文名文件的大小(返回空 → awk 求和为 0)。
- 规则:中文路径下统计一律走 Python os.walk + os.path.getsize,别用 shell 工具。
7. 汇报模板
- 分档表格(项 / 数量 / 占用 / 建议)
- 明确写「一个文件都没动」再问范围(扫描阶段)
- 执行后:逐档
动作 / 数量 / 结果表 + 复检三项 + 释放总量 - 把没做的也列出来(漏扫更正、需点头项、不敢动项)—— 用户最需要知道的是边界