shutil.rmtree 删不掉只读文件:ignore_errors=True 最危险
删不掉的目录
有个清理程序报 PermissionError: [WinError 5] 拒绝访问。,路径指向一个目录。
我先怀疑是文件被某个进程占用。加了重试、加了 ignore_errors=True,问题还在,而且目录确实还在那儿。
后来发现根因很朴素:里面的文件是只读的。
复现
三个场景,我都在隔离子进程里跑(Windows 上目录权限的行为受进程环境影响,混着测容易得出错的结论)。
目录里有只读文件
os.chmod(ro_file, stat.S_IREAD)
shutil.rmtree(root)
结果:PermissionError: [WinError 5] 拒绝访问。
目录本身是只读的
os.chmod(subdir, stat.S_IREAD)
shutil.rmtree(root2)
结果:同样是 PermissionError: [WinError 5] 拒绝访问。
两种情况的报错完全一样,光看错误码分不出是哪种。这是个实际用起来别扭的地方。
ignore_errors=True 最坑
shutil.rmtree(root4, ignore_errors=True)
不抛异常了。 但检查一下目录:
- 根目录还在(
root_still_exists=True) - 只读文件还在(
file_still_exists=True)
ignore_errors=True 的语义是「遇到错误就跳过」,不是「想办法删掉」。它把错误咽下去了,代价是留下半清理的垃圾,而且调用方完全不知道。
这是最危险的一种失败模式:清理程序显示成功,磁盘空间没释放,下次还是不够。我见过这种问题被排查半天,最后发现是某处悄悄加了 ignore_errors=True。
两种能用的解法
带 onerror 回调,失败时改权限再删
import os, stat, shutil
def _force_remove(func, path, exc_info):
try:
os.chmod(path, stat.S_IWRITE)
func(path)
except Exception:
pass
shutil.rmtree(root, onerror=_force_remove)
实测结果:OK, exists=False,目录真的删掉了。
onerror 会在每个出错的地方被调用,参数是那个失败的函数(os.remove 或 os.rmdir)和路径。改完权限再重试一次就行。
Python 3.12 起这个回调改名成了 onexc,两者签名略有差别。跨版本兼容的话可以先判断:
kwargs = {'onexc': handler} if sys.version_info >= (3, 12) else {'onerror': handler}
shutil.rmtree(root, **kwargs)
干脆别用 shutil.rmtree
如果目录结构是你自己控制的,手写递归反而更可控:
import os, stat
def force_rmtree(path):
for root, dirs, files in os.walk(path, topdown=False):
for name in files:
p = os.path.join(root, name)
try:
os.chmod(p, stat.S_IWRITE)
except OSError:
pass
os.remove(p)
for name in dirs:
p = os.path.join(root, name)
try:
os.chmod(p, stat.S_IWRITE)
except OSError:
pass
os.rmdir(p)
os.rmdir(path)
先把所有文件和目录的权限改成可写,再从深往上删。这样不管原本是什么权限组合都能删掉。
代价是要自己处理符号链接、挂载点这些边界情况。收益是行为可预测,不会出现「一半删了」的状态。
怎么判断该用哪种
先确认是不是只读的问题,最快的办法是手动改权限试一次:
os.chmod(some_file, stat.S_IWRITE) # 改成可写
shutil.rmtree(root) # 再试
能删掉就是权限问题,加 onerror 回调解决。
如果文件正被别的进程占用(Windows 上删除被占用的文件也会报拒绝访问),那是另一类问题,得先找到占用进程。用 handle.exe 这类工具,或者重启相关进程。
一句话
shutil.rmtree 删不掉只读文件和只读目录,都报 PermissionError: [WinError 5]。ignore_errors=True 会把错误咽掉但留下垃圾,最危险。加 onerror 回调改权限重试,或者自己写递归删除。
- 点赞
- 收藏
- 关注作者
评论(0)