shutil.rmtree在Windows库中不起作用
我正在写一个简单的脚本,用来把一些文件备份到我的第二个硬盘上(谁知道会发生什么呢!)。所以,我用了一个叫做 shutil.copytree
的功能来把我的数据复制到第二个硬盘上。这个功能运行得很好,这不是问题所在。
如果目标位置已经存在,我会用 shutil.rmtree
这个功能来删除之前的文件夹。下面是我的代码:
import shutil
import os
def overwrite(src, dest):
if(not os.path.exists(src)):
print(src, "does not exist, so nothing may be copied.")
return
if(os.path.exists(dest)):
shutil.rmtree(dest)
shutil.copytree(src, dest)
print(dest, "overwritten with data from", src)
print("")
overwrite(r"C:\Users\Centurion\Dropbox\Documents", r"D:\Backup\Dropbox Documents")
overwrite(r"C:\Users\Centurion\Pictures", r"D:\Backup\All Pictures")
print("Press ENTER to continue...")
input()
如你所见,这只是一个简单的脚本。第一次运行脚本时,一切都很顺利。图片和文档都顺利地复制到了我的 D:
硬盘上。但是,当我第二次运行时,输出结果是这样的:
C:\Users\Centurion\Programming\Python>python cpdocsnpics.py
D:\Backup\Dropbox Documents overwritten with data from C:\Users\Centurion\Dropbox\Documents
Traceback (most recent call last):
File "cpdocsnpics.py", line 17, in <module>
overwrite(r"C:\Users\Centurion\Pictures", r"D:\Backup\All Pictures")
File "cpdocsnpics.py", line 10, in overwrite
shutil.rmtree(dest)
File "C:\Python34\lib\shutil.py", line 477, in rmtree
return _rmtree_unsafe(path, onerror)
File "C:\Python34\lib\shutil.py", line 376, in _rmtree_unsafe
onerror(os.rmdir, path, sys.exc_info())
File "C:\Python34\lib\shutil.py", line 374, in _rmtree_unsafe
os.rmdir(path)
PermissionError: [WinError 5] Access is denied: 'D:\\Backup\\All Pictures'
这个错误只在我第二次复制 Pictures
文件夹时出现;我猜这可能和它是一个库有关。
我该怎么办呢?
2 个回答
我在Windows上使用shutil.rmtree
时发现了一个问题,除了只读文件的问题(我是在Windows 7上测试的)。我在测试套件中使用shutil.rmtree
和shutil.copytree
的组合来创建测试环境,所以这个过程在短时间内(小于1秒)被多次调用。结果我在测试过程中遇到了不可预测的失败,出现了EACCES和ENOTEMPTY错误。这些症状让我觉得shutil.rmtree
函数在返回调用程序时并没有完成,只有过了一段时间被删除的文件名才可以重新使用。
简单来说,解决方案并不优雅——大体上是先重命名目录再删除它,但因为Windows文件系统似乎需要一些时间来跟上这些操作,所以有很多细节需要处理。实际的代码会捕捉各种失败情况,并在短暂延迟后重试失败的操作。
接下来会有更详细的讨论,最后会给出我的最终代码。
我最初的想法是尝试在删除目录树之前先重命名它,这样原来的目录名就可以立即重新使用。这似乎确实有帮助。为此,我创建了一个rmtree
的替代方案,其核心逻辑是这样的:
def removetree(tgt):
def error_handler(func, path, execinfo):
e = execinfo[1]
if e.errno == errno.ENOENT or not os.path.exists(path):
return # path does not exist - treat as success
if func in (os.rmdir, os.remove) and e.errno == errno.EACCES:
os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
func(path) # read-only file; make writable and retry
raise e
tmp = os.path.join(os.path.dirname(tgt),"_removetree_tmp")
os.rename(tgt, tmp)
shutil.rmtree(tmp, onerror=error_handler)
return
我发现这个逻辑有所改进,但os.rename
操作仍然可能出现不可预测的失败,可能会有几种错误。因此,我还在os.rename
周围添加了一些重试逻辑,如下所示:
def removetree(tgt):
def error_handler(func, path, execinfo):
# figure out recovery based on error...
e = execinfo[1]
if e.errno == errno.ENOENT or not os.path.exists(path):
return # path does not exist
if func in (os.rmdir, os.remove) and e.errno == errno.EACCES:
os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
func(path) # read-only file; make writable and retry
raise e
# Rename target directory to temporary value, then remove it
count = 0
while count < 10: # prevents indefinite loop
count += 1
tmp = os.path.join(os.path.dirname(tgt),"_removetree_tmp_%d"%(count))
try:
os.rename(tgt, tmp)
shutil.rmtree(tmp, onerror=error_handler)
break
except OSError as e:
time.sleep(1) # Give file system some time to catch up
if e.errno in [errno.EACCES, errno.ENOTEMPTY]:
continue # Try another temp name
if e.errno == errno.EEXIST:
shutil.rmtree(tmp, ignore_errors=True) # Try to clean up old files
continue # Try another temp name
if e.errno == errno.ENOENT:
break # 'src' does not exist(?)
raise # Other error - propagate
return
上面的代码没有经过测试,但这个大致思路似乎是有效的。我实际使用的完整代码在下面,使用了两个函数。它可能包含一些不必要的逻辑,但对我来说似乎更可靠(因为我的测试套件现在在Windows上可以重复通过,而之前大多数运行都是不可预测地失败):
def renametree_temp(src):
"""
Rename tree to temporary name, and return that name, or
None if the source directory does not exist.
"""
count = 0
while count < 10: # prevents indefinite loop
count += 1
tmp = os.path.join(os.path.dirname(src),"_removetree_tmp_%d"%(count))
try:
os.rename(src, tmp)
return tmp # Success!
except OSError as e:
time.sleep(1)
if e.errno == errno.EACCES:
log.warning("util.renametree_temp: %s EACCES, retrying"%tmp)
continue # Try another temp name
if e.errno == errno.ENOTEMPTY:
log.warning("util.renametree_temp: %s ENOTEMPTY, retrying"%tmp)
continue # Try another temp name
if e.errno == errno.EEXIST:
log.warning("util.renametree_temp: %s EEXIST, retrying"%tmp)
shutil.rmtree(tmp, ignore_errors=True) # Try to clean up old files
continue # Try another temp name
if e.errno == errno.ENOENT:
log.warning("util.renametree_temp: %s ENOENT, skipping"%tmp)
break # 'src' does not exist(?)
raise # Other error: propagaee
return None
def removetree(tgt):
"""
Work-around for python problem with shutils tree remove functions on Windows.
See:
https://stackoverflow.com/questions/23924223/
https://stackoverflow.com/questions/1213706/
https://stackoverflow.com/questions/1889597/
http://bugs.python.org/issue19643
"""
# shutil.rmtree error handler that attempts recovery from attempts
# on Windows to remove a read-only file or directory (see links above).
def error_handler(func, path, execinfo):
e = execinfo[1]
if e.errno == errno.ENOENT or not os.path.exists(path):
return # path does not exist: nothing to do
if func in (os.rmdir, os.remove) and e.errno == errno.EACCES:
try:
os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
except Exception as che:
log.warning("util.removetree: chmod failed: %s"%che)
try:
func(path)
except Exception as rfe:
log.warning("util.removetree: 'func' retry failed: %s"%rfe)
if not os.path.exists(path):
return # Gone, assume all is well
raise
if e.errno == errno.ENOTEMPTY:
log.warning("util.removetree: Not empty: %s, %s"%(path, tgt))
time.sleep(1)
removetree(path) # Retry complete removal
return
log.warning("util.removetree: rmtree path: %s, error: %s"%(path, repr(execinfo)))
raise e
# Try renaming to a new directory first, so that the tgt is immediately
# available for re-use.
tmp = renametree_temp(tgt)
if tmp:
shutil.rmtree(tmp, onerror=error_handler)
return
(上面的代码包含了一个解决只读文件问题的方案,来自Python脚本在Windows上以什么用户身份运行?,根据在Python中删除目录的说法,这是经过测试的。我认为我没有遇到只读文件的问题,所以假设在我的测试套件中没有测试这个。)
这是一个跨平台的一致性问题。你复制了带有 readonly
属性的文件或文件夹。第一次运行时,"dest
" 不存在,所以没有执行 rmtree 方法。但是,当你尝试运行 "overwrite" 函数时,我们会发现 "dest" 位置已经存在(以及它的子目录),但它是以只读方式复制的。所以这里就出现了问题。
为了“解决”这个问题,你必须为 onerror 参数提供一个处理函数,这个参数是 shutil.rmtree 方法的一部分。由于你的问题与只读有关,解决方法大致如下:
def readonly_handler(func, path, execinfo):
os.chmod(path, 128) #or os.chmod(path, stat.S_IWRITE) from "stat" module
func(path)
正如你在 Python 文档中看到的,onerror 必须是一个可调用的函数,它接受三个参数:函数、路径和异常信息。如果想了解更多信息,可以 查看文档。
def overwrite(src, dest):
if(not os.path.exists(src)):
print(src, "does not exist, so nothing may be copied.")
return
if(os.path.exists(dest)):
shutil.rmtree(dest, onerror=readonly_handler)
shutil.copytree(src, dest)
print(dest, "overwritten with data from", src)
print("")
当然,这个处理函数很简单且特定,但如果发生其他错误,新的异常会被抛出,而这个处理函数可能无法解决这些问题!
注意:Tim Golden(Python for Windows 的贡献者)一直在修复 shutil.rmtree 的问题,似乎在 Python 3.5 中会得到解决(见 问题 19643)。