geekdoc-python-zh/docs/realpython/get-all-files-in-directory-...

27 KiB
Raw Permalink Blame History

如何用 Python 获得一个目录中所有文件的列表

原文:https://realpython.com/get-all-files-in-directory-python/

获得一个目录中所有文件和文件夹的列表是 Python 中许多文件相关操作的第一步。然而,当你深入研究它的时候,你可能会惊讶地发现有各种各样的方法去实现它。

当你面对做某事的许多方法时,这可能是一个很好的迹象,表明没有一个放之四海而皆准的解决方案。最有可能的是,每个解决方案都有自己的优势和权衡。在 Python 中获取一个目录的内容列表就是这种情况。

在本教程中,您将重点关注在 pathlib模块中列出目录中的项目的最通用的技术,但是您也将了解一些替代工具。

源代码: 点击这里下载免费的源代码、目录和额外材料,它们展示了用 Python 列出目录中的文件和文件夹的不同方式。

在 Python 3.4 的pathlib出现之前,如果你想处理文件路径,那么你可以使用 os 模块。虽然这在性能方面非常高效,但您必须将所有路径作为字符串来处理。

起初,将路径作为字符串处理似乎还可以,但是一旦您开始将多个操作系统混合在一起,事情就变得更加棘手了。您还会得到一堆与字符串操作相关的代码,这些代码可以从文件路径中抽象出来。事情很快就会变得神秘起来。

**注意:**查看可下载的材料,了解一些可以在您的机器上运行的测试。测试将比较使用来自pathlib模块、os模块、甚至未来 Python 3.12 版本的pathlib的方法返回一个目录中所有条目的列表所花费的时间。这个新版本包含了众所周知的walk()功能,这在本教程中不会涉及。

这并不是说将路径作为字符串工作是不可行的——毕竟,开发人员在没有pathlib的情况下也能很好地工作很多年!pathlib模块只是负责许多棘手的事情,让您专注于代码的主要逻辑。

这一切都是从创建一个Path对象开始的,这个对象会因操作系统(OS)的不同而不同。在 Windows 上,你会得到一个WindowsPath对象,而 Linux 和 macOS 会返回PosixPath:

***>>>

>>> import pathlib
>>> desktop = pathlib.Path("C:/Users/RealPython/Desktop")
>>> desktop
WindowsPath("C:/Users/RealPython/Desktop")
>>> import pathlib
>>> desktop = pathlib.Path("/home/RealPython/Desktop")
>>> desktop
PosixPath('/home/RealPython/Desktop')

有了这些支持操作系统的对象,您可以利用许多可用的方法和属性,比如获取文件和文件夹列表的方法和属性。

**注:**如果你有兴趣了解更多关于pathlib及其特性的信息,那么请查看 Python 3 的 pathlib 模块:驯服文件系统pathlib文档

现在,是时候开始列出文件夹内容了。请注意,有几种方法可以做到这一点,选择正确的方法将取决于您的特定用例。

用 Python 获取一个目录中所有文件和文件夹的列表

在开始列出清单之前,您需要一组与本教程中遇到的内容相匹配的文件。在补充资料中,你会找到一个名为 Desktop 的文件夹。如果你打算跟随,下载这个文件夹并导航到父文件夹,在那里启动你的 Python REPL :

源代码: 点击这里下载免费的源代码、目录和额外材料,它们展示了用 Python 列出目录中的文件和文件夹的不同方式。

你也可以使用自己的桌面。只需在桌面的父目录中启动 Python REPL示例应该可以工作但是输出中会有您自己的文件。

**注意:**在本教程中,你将主要看到作为输出的WindowsPath对象。如果你继续使用 Linux 或 macOS那么你会看到PosixPath。这是唯一的区别。你写的代码在所有平台上都是一样的。

如果你只需要列出一个给定目录的内容,而不需要得到每个子目录的内容,那么你可以使用Path对象的.iterdir()方法。如果你的目标是递归地浏览目录和子目录,那么你可以跳到递归列表的部分。

当在一个Path对象上调用.iterdir()方法时,该方法返回一个生成器,该生成器生成代表子项的Path对象。如果您将生成器包装在一个list()构造函数中,那么您可以看到您的文件和文件夹列表:

>>> import pathlib
>>> desktop = pathlib.Path("Desktop")

>>> # .iterdir() produces a generator
>>> desktop.iterdir()
<generator object Path.iterdir at 0x000001A8A5110740>

>>> # Which you can wrap in a list() constructor to materialize
>>> list(desktop.iterdir())
[WindowsPath('Desktop/Notes'),
 WindowsPath('Desktop/realpython'),
 WindowsPath('Desktop/scripts'),
 WindowsPath('Desktop/todo.txt')]

将由.iterdir()生成的生成器传递给list()构造函数会为您提供一个表示桌面目录中所有项目的Path对象列表。

与所有生成器一样,您也可以使用一个for循环来迭代生成器生成的每个项目。这使您有机会探索每个对象的一些属性:

>>> desktop = pathlib.Path("Desktop")
>>> for item in desktop.iterdir():
...     print(f"{item} - {'dir' if item.is_dir() else 'file'}")
...
Desktop\Notes - dir
Desktop\realpython - dir
Desktop\scripts - dir
Desktop\todo.txt - file

for循环主体中,您使用一个 f 字符串来显示每个项目的一些信息。

在 f 字符串的第二组花括号({})中,如果项目是一个目录,您使用一个条件表达式来打印目录,如果不是,则打印文件。要获得这些信息,您使用 .is_dir() 方法。

将一个Path对象放在 f 字符串中会自动将该对象转换成一个字符串,这就是为什么不再有WindowsPathPosixPath注释的原因。

像这样用一个for循环反复遍历对象,对于按文件或目录过滤来说非常方便,如下例所示:

>>> desktop = pathlib.Path("Desktop")
>>> for item in desktop.iterdir():
...     if item.is_file():
...         print(item)
...
Desktop\todo.txt

这里,您使用一个条件语句和 .is_file() 方法只打印文件项。

您还可以将生成器放入理解中,这可以产生非常简洁的代码:

>>> desktop = pathlib.Path("Desktop")
>>> [item for item in desktop.iterdir() if item.is_dir()]
[WindowsPath('Desktop/Notes'),
 WindowsPath('Desktop/realpython'),
 WindowsPath('Desktop/scripts')]

这里,您通过在理解中使用一个条件表达式来过滤结果列表,以检查项目是否是一个目录。

但是,如果您也需要文件夹子目录中的所有文件和目录,该怎么办呢?您可以将.iterdir()改编为递归函数,就像您将在教程的后面做的一样,但是使用.rglob()可能会更好,您将在接下来进行讨论。

Remove ads

.rglob()和递归列表

由于目录的递归性质,目录经常被比作树。在树木中,主干分裂成各种各样的主枝。每个主枝又分成更多的次枝。每个子分支也从自身分支,等等。同样,目录包含子目录,子目录包含子目录,子目录包含更多子目录,等等。

递归地列出目录中的项目意味着不仅要列出目录的内容,还要列出子目录及其子目录的内容,等等。

有了pathlib,遍历一个目录出奇的容易。您可以使用.rglob()返回所有内容:

>>> import pathlib
>>> desktop = pathlib.Path("Desktop")

>>> # .rglob() produces a generator too
>>> desktop.rglob("*")
<generator object Path.glob at 0x000001A8A50E2F00>

>>> # Which you can wrap in a list() constructor to materialize
>>> list(desktop.rglob("*"))
[WindowsPath('Desktop/Notes'),
 WindowsPath('Desktop/realpython'),
 WindowsPath('Desktop/scripts'),
 WindowsPath('Desktop/todo.txt'),
 WindowsPath('Desktop/Notes/hash-tables.md'),
 WindowsPath('Desktop/realpython/iterate-dict.md'),
 WindowsPath('Desktop/realpython/tictactoe.md'),
 WindowsPath('Desktop/scripts/rename_files.py'),
 WindowsPath('Desktop/scripts/request.py')]

"*"作为参数的.rglob()方法产生一个生成器,该生成器递归地从Path对象产生所有文件和文件夹。

但是.rglob()的星号参数是什么?在下一节中,您将研究 glob 模式,看看除了列出目录中的所有项目之外,您还能做些什么。

使用 Python Glob 模式进行条件列表

有时候你不想要所有的文件。有时候,您只需要一种类型的文件或目录,或者名称中包含某种字符模式的所有项目。

.rglob()相关的一种方法是.glob()方法。这两种方法都利用了 glob 模式。glob 模式表示路径的集合。Glob 模式利用通配符来匹配某些标准。例如,单个星号*匹配目录中的所有内容。

您可以利用许多不同的 glob 模式。查看以下 glob 模式选择,了解一些想法:

球状图案 比赛
* 每次
*.txt .txt结尾的每一项,如notes.txthello.txt
?????? 名称长度为六个字符的每一项,如01.txtA-01.c.zshrc
A* 以字符 A 开头的每一项,如AlbumA.txtAppData
[abc][abc][abc] 名称为三个字符但仅由字符 abc 组成的项目,如abcaaacba

使用这些模式,您可以灵活地匹配许多不同类型的文件。查看关于fnmatch文档,这是控制.glob()行为的底层模块,感受一下 Python 中可以使用的其他模式。

注意,在 Windows 上glob 模式是不区分大小写的,因为路径通常是不区分大小写的。在像 Linux 和 macOS 这样的类 Unix 系统上glob 模式是区分大小写的。

条件清单使用.glob()

一个Path对象的.glob()方法的行为与.rglob()非常相似。如果您传递了"*"参数,那么您将获得目录中的条目列表,但是没有递归:

>>> import pathlib
>>> desktop = pathlib.Path("Desktop")

>>> # .glob() produces a generator too
>>> desktop.glob("*")
<generator object Path.glob at 0x000001A8A50E2F00>

>>> # Which you can wrap in a list() constructor to materialize
>>> list(desktop.glob("*"))
[WindowsPath('Desktop/Notes'),
 WindowsPath('Desktop/realpython'),
 WindowsPath('Desktop/scripts'),
 WindowsPath('Desktop/todo.txt')]

在一个Path对象上使用带有"*" glob 模式的.glob()方法会产生一个生成器,该生成器生成由Path对象表示的目录中的所有项目,而不进入子目录。这样,它产生与.iterdir()相同的结果,你可以在for循环或理解中使用生成的生成器,就像你使用iterdir()一样。

但是正如您已经了解到的,真正使 glob 方法与众不同的是可以用来匹配特定路径的不同模式。例如,如果您只想要以.txt结尾的路径,那么您可以执行以下操作:

>>> desktop = pathlib.Path("Desktop")
>>> list(desktop.glob("*.txt"))
[WindowsPath('Desktop/todo.txt')]

因为这个目录只有一个文本文件,所以您得到的列表只有一项。例如,如果您只想获得以 real 开头的项目,那么您可以使用下面的 glob 模式:

>>> list(desktop.glob("real*"))
[WindowsPath('Desktop/realpython')]

这个示例也只生成一个项目,因为只有一个项目的名称以字符real开头。请记住,在类 Unix 系统上glob 模式是区分大小写的。

**注意:**名称在这里指的是路径的最后一部分,而不是路径的其他部分,在这种情况下,其他部分将从Desktop开始。

您还可以通过包含子目录的名称、正斜杠(/)和星号来获取子目录的内容。这种类型的模式将产生目标目录中的所有内容:

>>> list(desktop.glob("realpython/*"))
[WindowsPath('Desktop/realpython/iterate-dict.md'),
 WindowsPath('Desktop/realpython/tictactoe.md')]

在这个例子中,使用"realpython/*"模式产生了realpython目录中的所有文件。它会给你与创建一个代表Desktop/realpython路径的路径对象并在其上调用.glob("*")相同的结果。

接下来,您将进一步研究使用.rglob()进行过滤,并了解它与.glob()的不同之处。

Remove ads

条件清单使用.rglob()

就像使用.glob()方法一样,你可以调整.rglob()的 glob 模式,只给你一个特定的文件扩展名,除了.rglob()将总是递归搜索:

>>> list(desktop.rglob("*.md"))
[WindowsPath('Desktop/Notes/hash-tables.md'),
 WindowsPath('Desktop/realpython/iterate-dict.md'),
 WindowsPath('Desktop/realpython/tictactoe.md')]

通过将.md添加到 glob 模式中,现在.rglob()只在不同的目录和子目录中生成.md文件。

您实际上可以使用.glob(),通过调整作为参数传递的 glob 模式,让它以与.rglob()相同的方式运行:

>>> list(desktop.glob("**/*.md"))
[WindowsPath('Desktop/Notes/hash-tables.md'),
 WindowsPath('Desktop/realpython/iterate-dict.md'),
 WindowsPath('Desktop/realpython/tictactoe.md')]

在这个例子中,你可以看到对.glob("**/*.md")的调用等同于.rglob(*.md)。同样,对.glob("**/*")的调用相当于.rglob("*")

.rglob()方法是使用递归模式调用.glob()的一个稍微更显式的版本,所以使用更显式的版本可能比使用普通.glob()的递归模式更好。

使用 Glob 方法进行高级匹配

glob 方法的一个潜在缺点是,您只能根据 glob 模式选择文件。如果你想在物品的属性上做更高级的匹配或过滤,那么你需要额外的东西。

要运行更复杂的匹配和过滤,您至少可以遵循三种策略。您可以使用:

  1. 带有条件检查的for循环
  2. 有条件表达的理解
  3. 内置的filter()功能

方法如下:

>>> import pathlib
>>> desktop = pathlib.Path("Desktop")

>>> # Using a for loop
>>> for item in desktop.rglob("*"):
...     if item.is_file():
...         print(item)
...
Desktop\todo.txt
Desktop\Notes\hash-tables.md
Desktop\realpython\iterate-dict.md
Desktop\realpython\tictactoe.md
Desktop\scripts\rename_files.py
Desktop\scripts\request.py

>>> # Using a comprehension
>>> [item for item in desktop.rglob("*") if item.is_file()]
[WindowsPath('Desktop/todo.txt'),
 WindowsPath('Desktop/Notes/hash-tables.md'),
 WindowsPath('Desktop/realpython/iterate-dict.md'),
 WindowsPath('Desktop/realpython/tictactoe.md'),
 WindowsPath('Desktop/scripts/rename_files.py'),
 WindowsPath('Desktop/scripts/request.py')]

>>> # Using the filter() function
>>> list(filter(lambda item: item.is_file(), desktop.rglob("*")))
[WindowsPath('Desktop/todo.txt'),
 WindowsPath('Desktop/Notes/hash-tables.md'),
 WindowsPath('Desktop/realpython/iterate-dict.md'),
 WindowsPath('Desktop/realpython/tictactoe.md'),
 WindowsPath('Desktop/scripts/rename_files.py'),
 WindowsPath('Desktop/scripts/request.py')]

在这些示例中,您首先使用"*"模式调用了.rglob()方法,以递归方式获取所有项目。这将生成目录及其子目录中的所有项目。然后使用上面列出的三种不同的方法来过滤掉不是文件的项目。注意,在 filter() 的例子中,你使用了一个λ函数。

glob 方法非常通用,但是对于大型目录树,它们可能有点慢。在下一节中,您将研究一个例子,在这个例子中,使用.iterdir()来实现更可控的迭代可能是一个更好的选择。

选择不列出垃圾目录

比方说,你想找到你系统上的所有文件,但是你有各种各样的子目录,这些子目录有很多很多的子目录和文件。一些最大的子目录是你不感兴趣的临时文件。

例如,检查这个目录树,它有很多垃圾目录!实际上,这个完整的目录树有 1850 行长。无论你在哪里看到一个省略号(...),这意味着在那个位置有数百个垃圾文件:

large_dir/
├── documents/
   ├── notes/
      ├── temp/
         ├── 2/
            ├── 0.txt
            ...
         
         ├── 0.txt
         ...
      
      ├── 0.txt
      └── find_me.txt
   
   ├── tools/
      ├── temporary_files/
         ├── logs/
            ├──0.txt
            ...
         
         ├── temp/
            ├──0.txt
            ...
         
         ├── 0.txt
         ...
      
      ├── 33.txt
      ├── 34.txt
      ├── 36.txt
      ├── 37.txt
      └── real_python.txt
   
   ├── 0.txt
   ├── 1.txt
   ├── 2.txt
   ├── 3.txt
   └── 4.txt

├── temp/
   ├── 0.txt
   ...

└── temporary_files/
    ├── 0.txt
    ...

这里的问题是你有垃圾目录。垃圾目录有时叫做temp,有时叫做temporary files,有时叫做logs。更糟糕的是,它们无处不在,可以在任何层次筑巢。好消息是您不必列出它们,因为您将在接下来学习。

Remove ads

使用.rglob()过滤整个目录

如果使用.rglob(),只要在.rglob()生产出来之后就可以过滤掉了。要正确丢弃垃圾目录中的路径,您可以检查路径中的任何元素是否与目录列表中的任何元素匹配,以跳过:

>>> SKIP_DIRS = ["temp", "temporary_files", "logs"]

这里,您将SKIP_DIRS定义为一个列表,其中包含您想要排除的路径字符串。

用一个星号作为参数调用.rglob()将产生所有项目,甚至是那些您不感兴趣的目录中的项目。因为您必须遍历所有项目,所以如果您只查看路径的名称,可能会有一个问题:

large_dir/documents/notes/temp/2/0.txt

由于名称只是0.txt,它不会匹配SKIP_DIRS中的任何项目。您需要检查被阻止名称的整个路径。

您可以使用.parts属性获取路径中的所有元素,该属性包含路径中所有元素的元组:

>>> import pathlib
>>> temp_file = pathlib.Path("large_dir/documents/notes/temp/2/0.txt")
>>> temp_file.parts
('large_dir', 'documents', 'notes', 'temp', '2', '0.txt')

然后,您需要做的就是检查.parts元组中的任何元素是否在要跳过的目录列表中。

你可以通过利用集合来检查任意两个可重复项是否有一个公共项。如果您将其中一个 iterables 转换为一个集合,那么您可以使用.isdisjoint()方法来确定它们是否有任何共同的元素:

>>> {"documents", "notes", "find_me.txt"}.isdisjoint({"temp", "temporary"})
True

>>> {"documents", "temp", "find_me.txt"}.isdisjoint({"temp", "temporary"})
False

如果两个集合没有共同的元素,那么.isdisjoint()返回True。如果两个集合至少有一个元素相同,那么.isdisjoint()返回False。您可以将该检查合并到一个for循环中,该循环遍历由.rglob("*")返回的所有项目:

>>> SKIP_DIRS = ["temp", "temporary_files", "logs"]
>>> large_dir = pathlib.Path("large_dir")

>>> # With a for loop
>>> for item in large_dir.rglob("*"):
...     if set(item.parts).isdisjoint(SKIP_DIRS):
...         print(item)
...
large_dir\documents
large_dir\documents\0.txt
large_dir\documents\1.txt
large_dir\documents\2.txt
large_dir\documents\3.txt
large_dir\documents\4.txt
large_dir\documents\notes
large_dir\documents\tools
large_dir\documents\notes\0.txt
large_dir\documents\notes\find_me.txt
large_dir\documents\tools\33.txt
large_dir\documents\tools\34.txt
large_dir\documents\tools\36.txt
large_dir\documents\tools\37.txt
large_dir\documents\tools\real_python.txt

在这个例子中,您打印了large_dir中不在任何垃圾目录中的所有项目。

要检查路径是否在某个不想要的文件夹中,您将item.parts转换为一个集合,并使用.isdisjoint()来检查SKIP_DIRS.parts 是否没有任何共同的项目。如果是这种情况,则打印该项目。

您也可以使用filter()和理解来实现相同的效果,如下所示:

>>> # With a comprehension
>>> [
...     item
...     for item in large_dir.rglob("*")
...     if set(item.parts).isdisjoint(SKIP_DIRS)
... ]

>>> # With filter()
>>> list(
...     filter(
...         lambda item: set(item.parts).isdisjoint(SKIP_DIRS),
...         large_dir.rglob("*")
...     )
... )

不过,这些方法已经变得有点晦涩难懂了。不仅如此,它们的效率也不是很高,因为.rglob()生成器必须生成所有的项,这样匹配操作才能丢弃那个结果。

你肯定可以用.rglob()过滤掉整个文件夹,但是你不能逃避这样一个事实,即生成的生成器将产生所有的项目,然后一个接一个地过滤掉不需要的项目。这可能会使 glob 方法非常慢,这取决于您的用例。这就是为什么您可能选择递归.iterdir()函数,您将在接下来探索它。

Remove ads

创建递归.iterdir()函数

在垃圾目录的例子中,如果给定子目录中的所有文件与SKIP_DIRS中的某个名称匹配,那么理想情况下,您希望能够选择退出来迭代这些文件:

# skip_dirs.py

import pathlib

SKIP_DIRS = ["temp", "temporary_files", "logs"]

def get_all_items(root: pathlib.Path, exclude=SKIP_DIRS):
    for item in root.iterdir():
        if item.name in exclude:
            continue
        yield item
        if item.is_dir():
            yield from get_all_items(item)

在这个模块中,您定义了一个字符串列表SKIP_DIRS,它包含了您想要忽略的目录的名称。然后定义一个生成器函数,它使用.iterdir()遍历每一项。

生成器函数在第一个参数后使用了类型注释 : pathlib.Path来表示不能只传入代表路径的字符串。参数需要是一个Path对象。

如果项目名称在exclude列表中,那么您只需移动到下一个项目,一次性跳过整个子目录树。

如果这个项目不在列表中,那么您就放弃这个项目,如果它是一个目录,那么您就在这个目录上再次调用这个函数。也就是说,在函数体内,函数有条件地再次调用同一个函数。这是递归函数的标志。

这个递归函数可以有效地产生您想要的所有文件和目录,排除您不感兴趣的所有文件和目录:

>>> import pathlib
>>> import skip_dirs
>>> large_dir = pathlib.Path("large_dir")

>>> list(skip_dirs.get_all_items(large_dir))
[WindowsPath('large_dir/documents'),
 WindowsPath('large_dir/documents/0.txt'),
 WindowsPath('large_dir/documents/1.txt'),
 WindowsPath('large_dir/documents/2.txt'),
 WindowsPath('large_dir/documents/3.txt'),
 WindowsPath('large_dir/documents/4.txt'),
 WindowsPath('large_dir/documents/notes'),
 WindowsPath('large_dir/documents/notes/0.txt'),
 WindowsPath('large_dir/documents/notes/find_me.txt'),
 WindowsPath('large_dir/documents/tools'),
 WindowsPath('large_dir/documents/tools/33.txt'),
 WindowsPath('large_dir/documents/tools/34.txt'),
 WindowsPath('large_dir/documents/tools/36.txt'),
 WindowsPath('large_dir/documents/tools/37.txt'),
 WindowsPath('large_dir/documents/tools/real_python.txt')]

至关重要的是,您已经设法避免了检查不需要的目录中的所有文件。一旦您的生成器识别出该目录在SKIP_DIRS列表中,它就会跳过整个过程。

因此,在这种情况下,使用.iterdir()将比同等的 glob 方法更有效。

事实上,如果您需要过滤比 glob 模式更复杂的东西,您会发现.iterdir()通常比 glob 方法更有效。然而,如果您需要做的只是递归地获得所有.txt文件的列表,那么 glob 方法会更快。

查看一些测试的可下载资料,这些测试展示了用 Python 列出文件的不同方法的相对速度:

源代码: 点击这里下载免费的源代码、目录和额外材料,它们展示了用 Python 列出目录中的文件和文件夹的不同方式。

有了这些信息,您就可以选择列出所需文件和文件夹的最佳方式了!

结论

在本教程中,您已经研究了 Python pathlib模块中的.glob().rglob().iterdir()方法,以便将给定目录中的所有文件和文件夹放入一个列表中。您已经讨论了列出目录的直接后代的文件和文件夹,并且您还查看了递归列表

总的来说,您已经看到,如果您只需要目录中的基本条目列表,而不需要递归,那么.iterdir()是最干净的方法,这要归功于它的描述性名称。这项工作效率也更高。然而,如果你需要一个递归列表,那么你最好使用.rglob(),这将比一个等价的递归.iterdir()更快。

您还研究了一个例子,在这个例子中,使用.iterdir()递归地列出可以产生巨大的性能优势——当您有垃圾文件夹而您想选择不迭代时。

在可下载的资料中,您会发现从pathlibos模块中获取基本文件列表的方法的各种实现,以及对它们进行计时的几个脚本:

源代码: 点击这里下载免费的源代码、目录和额外材料,它们展示了用 Python 列出目录中的文件和文件夹的不同方式。

检查它们,修改它们,并在评论中分享你的发现!*******