递归删除目录和所有符号链接

11

我尝试使用shutil删除一个目录及其内含的所有文件,如下所示:

import shutil
from os.path import exists
if exists(path_dir):
    shutil.rmtree(path_dir)

很遗憾,我的解决方案不起作用,会抛出以下错误:

FileNotFoundError: [Errno 2] No such file or directory: '._image1.jpg'

快速搜索显示我在遇到该问题时并不孤单

据我所知,rmtree函数相当于rm -Rf $DIR shell命令,但实际上并非如此。

p.s.为了重建目的,请使用ln -s /path/to/original /path/to/link来创建符号链接示例。


path_dir 是一个符号链接的路径吗? - gimix
不,path_dir是一个包含各种文件和文件夹的目录。 - Jürgen K.
3个回答

5

很奇怪,我在删除文件夹时使用shutil.rmtree() 无论是在包含软链接的还是不包含软链接的情况下都没有问题,在Windows 10和Ubuntu 20.04.2 LTS上都可以正常工作。

不过你可以尝试下面的代码,我已经在Windows 10和Ubuntu上测试过了。

from pathlib import Path
import shutil


def delete_dir_recursion(p):
    """
    Delete folder, sub-folders and files.
    """
    for f in p.glob('**/*'):
        if f.is_symlink():
            f.unlink(missing_ok=True)  # missing_ok is added in python 3.8
            print(f'symlink {f.name} from path {f} was deleted')
        elif f.is_file():
            f.unlink()
            print(f'file: {f.name} from path {f} was deleted')
        elif f.is_dir():
            try:
                f.rmdir()  # delete empty sub-folder
                print(f'folder: {f.name} from path {f} was deleted')
            except OSError:  # sub-folder is not empty
                delete_dir_recursion(f)  # recurse the current sub-folder
            except Exception as exception:  # capture other exception
                print(f'exception name: {exception.__class__.__name__}')
                print(f'exception msg: {exception}')

    try:
        p.rmdir()  # time to delete an empty folder
        print(f'folder: {p.name} from path {p} was deleted')
    except NotADirectoryError:
        p.unlink()  # delete folder even if it is a symlink, linux
        print(f'symlink folder: {p.name} from path {p} was deleted')
    except Exception as exception:
        print(f'exception name: {exception.__class__.__name__}')
        print(f'exception msg: {exception}')


def delete_dir(folder):
    p = Path(folder)

    if not p.exists():
        print(f'The path {p} does not exists!')
        return

    # Attempt to delete the whole folder at once.
    try:
        shutil.rmtree(p)
    except Exception as exception:
        print(f'exception name: {exception.__class__.__name__}')
        print(f'exception msg: {exception}')
        # continue parsing the folder
    else:  # else if no issues on rmtree()
        if not p.exists():  # verify
            print(f'folder {p} was successfully deleted by shutil.rmtree!')
            return

    print(f'Parse the folder {folder} ...')
    delete_dir_recursion(p)

    if not p.exists():  # verify
        print(f'folder {p} was successfully deleted!')

# start
folder_to_delete = '/home/zz/tmp/sample/b'  # delete folder b
delete_dir(folder_to_delete)

输出结果示例:

我们将要删除文件夹b

.
├── 1.txt
├── a
├── b
│   ├── 1
│   ├── 1.txt -> ../1.txt
│   ├── 2
│   │   └── 21
│   │       └── 21.txt
│   ├── 3
│   │   └── 31
│   ├── 4
│   │   └── c -> ../../c
│   ├── a -> ../a
│   └── b.txt
├── c


Parse the folder /home/zz/tmp/sample/b ...
symlink a from path /home/zz/tmp/sample/b/a was deleted
symlink c from path /home/zz/tmp/sample/b/4/c was deleted
folder: 4 from path /home/zz/tmp/sample/b/4 was deleted
symlink 1.txt from path /home/zz/tmp/sample/b/1.txt was deleted
file: b.txt from path /home/zz/tmp/sample/b/b.txt was deleted
file: 21.txt from path /home/zz/tmp/sample/b/2/21/21.txt was deleted
folder: 21 from path /home/zz/tmp/sample/b/2/21 was deleted
folder: 2 from path /home/zz/tmp/sample/b/2 was deleted
folder: 1 from path /home/zz/tmp/sample/b/1 was deleted
folder: 31 from path /home/zz/tmp/sample/b/3/31 was deleted
folder: 3 from path /home/zz/tmp/sample/b/3 was deleted
folder: b from path /home/zz/tmp/sample/b was deleted
folder /home/zz/tmp/sample/b was successfully deleted!

我认为你离正确很近了。该函数会删除所有文件夹内的所有文件,但不会在它们清空后删除这些文件夹。 - Jürgen K.
@JürgenK. 有什么问题吗?我期望空文件夹将在 p.rmdir() # time to delete an empty folder 中处理。是否有任何消息?您是在Linux还是Windows上进行测试? - ferdy
没问题,rmdir 可以正常工作,但不支持递归操作。因此,如果存在包含另一个空文件夹的空文件夹,rmdir 将无法删除。在 Mac 上进行测试。 - Jürgen K.
谢谢提供的信息,我会重新检查它。 - ferdy
@JürgenK已经修复了问题,现在它可以递归删除文件/子文件夹。 - ferdy

3
你可能正在使用 Mac OSX 操作系统,而且你的目录至少部分位于非 Mac 文件系统上(即非 HFS+)。在这些文件系统上,Mac 文件系统驱动程序会自动创建以 ._ 为前缀的二进制补充文件,记录所谓的扩展属性(在https://apple.stackexchange.com/questions/14980/why-are-dot-underscore-files-created-and-how-can-i-avoid-them中有解释,也在下面说明)。
对于不支持 os.scandir 中的文件描述符的系统(如 Mac OSX),rmtree 现在不安全地创建一个条目列表,然后逐个删除它们(创建已知的竞争条件:https://github.com/python/cpython/blob/908fd691f96403a3c30d85c17dd74ed1f26a60fd/Lib/shutil.py#L592-L621)。不幸的是,两种不同的行为使得这个条件每次都成立:
  1. 原始文件总是在扩展属性文件之前列出,并且
  2. 当删除原始文件(test.txt)时,元文件(._test.txt)同时被删除。
因此,当扩展属性文件轮到它的时候会丢失,并抛出你遇到的 FileNotFoundError
我认为最好通过cpython#14064来解决这个错误,该 PR 旨在一般忽略 rmtree 中的 FileNotFoundError

缓解方法

在此期间,你可以使用 onerror 忽略这些元文件上的删除错误:
def ignore_extended_attributes(func, filename, exc_info):
    is_meta_file = os.path.basename(filename).startswith("._")
    if not (func is os.unlink and is_meta_file):
        raise

shutil.rmtree(path_dir, onerror=ignore_extended_attributes)

Mac扩展属性的示例

为了说明,您可以创建一个小型的ExFAT磁盘映像,并使用以下命令将其挂载到/Volumes/Untitled

hdiutil create -size 5m -fs exfat test.dmg
hdiutil attach test.dmg            # mounts at /Volumes/Untitled
cd /Volumes/Untitled

mkdir test                         # create a directory to remove
cd test
touch test.txt
open test.txt                      # open the test.txt file in the standard editor 

在标准文本编辑器中打开文件会创建一个扩展属性文件._test.txt,并记录最后访问时间:

/Volumes/Untitled/test $ ls -a
.          ..         ._test.txt test.txt
/Volumes/Untitled/test $ xattr test.txt
com.apple.lastuseddate#PS

问题在于自动取消链接原始文件也会取消链接伴随的文件。
/Volumes/Untitled/test $ rm test.txt
/Volumes/Untitled/test $ ls -a
.          ..

很棒的答案。感谢您提供缓解代码。 - Hubert Schölnast

1

来源于如何在Python中删除包括其所有文件的目录?

# function that deletes all files and then folder

import glob, os

def del_folder(dir_name):
    
    dir_path = os.getcwd() +  "\{}".format(dir_name)
    try:
        os.rmdir(dir_path)  # remove the folder
    except:
        print("OSError")   # couldn't remove the folder because we have files inside it
    finally:
        # now iterate through files in that folder and delete them one by one and delete the folder at the end
        try:
            for filepath in os.listdir(dir_path):
                os.remove(dir_path +  "\{}".format(filepath))
            os.rmdir(dir_path)
            print("folder is deleted")
        except:
            print("folder is not there")

您也可以使用ignore_errors标志与shutil.rmtree()一起使用。

shutil.rmtree('/folder_name', ignore_errors=True) 这将删除一个包含文件内容的目录。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接