27 KiB
如何用 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:
- 视窗 ** Linux + macOS*
***>>>
>>> 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 字符串中会自动将该对象转换成一个字符串,这就是为什么不再有WindowsPath或PosixPath注释的原因。
像这样用一个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()可能会更好,您将在接下来进行讨论。
用.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.txt或hello.txt |
?????? |
名称长度为六个字符的每一项,如01.txt、A-01.c或.zshrc |
A* |
以字符 A 开头的每一项,如Album、A.txt或AppData |
[abc][abc][abc] |
名称为三个字符但仅由字符 a 、 b 、 c 组成的项目,如abc、aaa或cba |
使用这些模式,您可以灵活地匹配许多不同类型的文件。查看关于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()的不同之处。
条件清单使用.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 模式选择文件。如果你想在物品的属性上做更高级的匹配或过滤,那么你需要额外的东西。
要运行更复杂的匹配和过滤,您至少可以遵循三种策略。您可以使用:
- 带有条件检查的
for循环 - 有条件表达的理解
- 内置的
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。更糟糕的是,它们无处不在,可以在任何层次筑巢。好消息是您不必列出它们,因为您将在接下来学习。
使用.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()函数,您将在接下来探索它。
创建递归.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()递归地列出可以产生巨大的性能优势——当您有垃圾文件夹而您想选择不迭代时。
在可下载的资料中,您会发现从pathlib和os模块中获取基本文件列表的方法的各种实现,以及对它们进行计时的几个脚本:
源代码: 点击这里下载免费的源代码、目录和额外材料,它们展示了用 Python 列出目录中的文件和文件夹的不同方式。
检查它们,修改它们,并在评论中分享你的发现!*******