使用 Python 的 os.walk 函数自动化系统管理任务
使用 Python 的 os.walk 函数遍历文件和目录树。
我是一个网站开发者;我在 1993 年初搭建了我的第一个网站。因此,当我开始进行 Python 培训时,我假设我的大多数学生也将成为网站开发者或有抱负的网站开发者。事实远非如此。虽然我的一些学生确实对 Web 应用程序感兴趣,但他们中的大多数是软件工程师、测试人员、数据科学家和系统管理员。
最后一组,系统管理员,通常带着相同的故事来参加我的课程。他们工作的公司已经编写 Bash 脚本多年,但他们希望转向更高级别的语言,这种语言具有更强的表达能力和大量的第三方插件。(无意冒犯 Bash 用户;您可以使用 Bash 做令人惊奇的事情,但我希望您会同意脚本可能会变得笨拙且难以维护。)
事实证明,借助一些简单的工具和想法,这些系统管理员可以使用 Python 用更少的代码做更多的事情,以及创建报告和维护服务器。因此,在本文中,我描述了一个特别有用的工具,它经常被忽视:os.walk,一个允许您遍历文件和目录树的函数。
os.walk 基础知识Linux 用户习惯于使用 ls
命令来获取目录中的文件列表。Python 带有两个不同的函数,可以返回文件列表。一个是 os.listdir
,它表示 “os” 包中的 “listdir” 函数。如果需要,您可以将目录名称传递给 os.listdir
。如果您不这样做,您将获得当前目录中的文件名。所以,你可以说
In [10]: import os
当我在我的电脑上,在当前目录中执行此操作时,我得到以下内容
In [11]: os.listdir('.')
Out[11]:
['.git',
'.gitignore',
'.ipynb_checkpoints',
'.mypy_cache',
'Archive',
'Files']
正如您所看到的,os.listdir
返回一个字符串列表,每个字符串都是一个文件名。当然,在 UNIX 类型系统中,目录也是文件——因此,除了文件之外,您还会看到子目录,但没有任何明显的迹象表明哪个是哪个。
很久以前我就放弃了 os.listdir
,转而使用 glob.glob
,它表示 “glob” 模块中的 “glob” 函数。命令行用户习惯于使用 “globbing”,尽管他们通常不知道它的名称。Globbing 意味着使用 * 和 ? 字符以及其他字符,以便更灵活地匹配文件名。虽然 os.listdir
可以返回目录中的文件列表,但它无法过滤它们。但是您可以使用 glob.glob
In [13]: import glob
In [14]: glob.glob('Files/*.zip')
Out[14]:
['Files/advanced-exercise-files.zip',
'Files/exercise-files.zip',
'Files/names.zip',
'Files/words.zip']
在任何一种情况下,您都会获得文件名(和子目录)的字符串。然后,您可以使用 for
循环或列表推导来迭代它们并执行操作。另请注意,与 os.listdir
返回不带任何路径的文件名列表相比,glob.glob
返回每个文件的完整路径名,我经常发现这很有用。
但是,如果您想遍历每个文件,包括每个子目录中的每个文件怎么办?那么您会遇到更多问题。当然,您可以使用 for
循环来迭代每个文件名,然后使用 os.path.isdir
来判断它是否是子目录——如果是,那么您可以获取该子目录中的文件列表,并将它们添加到您正在迭代的列表中。
或者,您可以使用 os.walk
函数,它可以完成所有这些以及更多功能。虽然 os.walk
看起来和行为都像一个函数,但它实际上是一个 “生成器函数”——一个在执行时返回 “生成器” 对象的函数,该对象实现了迭代协议。如果您不习惯使用生成器,则运行该函数可能会有点令人惊讶
In [15]: os.walk('.')
Out[15]: <generator object walk at 0x1035be5e8>
想法是将 os.walk
的输出放在 for
循环中。让我们这样做
In [17]: for item in os.walk('.'):
...: print(item)
结果,至少在我的计算机上,是大量的输出,滚动速度太快,以至于我无法轻松阅读。这是否会发生在您身上取决于您在系统上的哪个位置运行此 for
循环以及存在多少文件(和子目录)。
在每次迭代中,os.walk
返回一个包含三个元素的元组
- 当前路径(即目录名)作为字符串。
- 子目录名称列表(作为字符串)。
- 非目录文件名列表(作为字符串)。
因此,通常会调用 os.walk
,以便在 for
循环中将这三个元素中的每一个分配给一个单独的变量
In [19]: for currentdir, dirnames, filenames in os.walk('.'):
...: print(currentdir)
迭代会继续,直到返回 os.walk
参数下的每个子目录。这允许您执行各种报告和有趣的任务。例如,上面的代码将打印当前目录 “.” 下的所有子目录。
假设您想计算当前目录下文件(非子目录)的数量。您可以说
In [19]: file_count = 0
In [20]: for currentdir, dirnames, filenames in os.walk('.'):
...: file_count += len(filenames)
...:
In [21]: file_count
Out[21]: 3657
您还可以做一些更复杂的事情,计算每种类型的文件有多少个,使用扩展名作为分类器。您可以使用 os.path.splitext
获取扩展名,它返回两项——不带扩展名的文件名和扩展名本身
In [23]: os.path.splitext('abc/def/ghi.jkl')
Out[23]: ('abc/def/ghi', '.jkl')
您可以使用我最喜欢的 Python 数据结构之一 Counter
来计数项目。例如
In [24]: from collections import Counter
In [25]: counts = Counter()
In [26]: for currentdir, dirnames, filenames in os.walk('.'):
...: for one_filename in filenames:
...: first_part, ext =
↪os.path.splitext(one_filename)
...: counts[ext] += 1
这会遍历 “.” 下的每个目录,获取文件名。然后,它迭代文件名列表,拆分名称以便您可以获取扩展名。然后,它将该扩展名的计数器加 1。
此代码运行后,您可以向 counts
请求报告。因为它是一个字典,所以您可以使用 items
方法并打印键和值(即,扩展名和计数)。您可以按如下方式打印它们
In [30]: for extension, count in counts.items():
...: print(f"{extension:8}{count}")
在上面的代码中,f strings
显示扩展名(在八个字符的字段中)和计数。
但是,如果只显示十个最常见的扩展名,那不是更好吗?是的,但是那样您必须对 counts
对象进行排序。更简单的方法是使用 Counter
对象提供的 most_common
方法,该方法不仅返回键和值,还按降序对它们进行排序
In [31]: for extension, count in counts.most_common(10):
...: print(f"{extension:8}{count}")
...:
.py 1149
867
.zip 466
.ipynb 410
.pyc 372
.txt 151
.json 76
.so 37
.conf 19
.py~ 12
换句话说——毫不奇怪——此示例表明,我在用于教授 Python 课程的目录中最常见的文件扩展名是 .py。没有任何扩展名的文件排在第二位,其次是 .zip、.ipynb(Jupyter 笔记本)和 .pyc(字节编译的 Python)。
文件大小您也可以提出更有趣的问题。例如,您可能想知道每种文件类型使用了多少磁盘空间。现在,您不再为每次遇到文件扩展名都加 1,而是添加文件的大小。幸运的是,由于 os.path.getsize
函数,这变得非常容易(这返回的值与您从 os.stat
获得的值相同)
for currentdir, dirnames, filenames in os.walk('.'):
for one_filename in filenames:
first_part, ext = os.path.splitext(one_filename)
try:
counts[ext] +=
↪os.path.getsize(os.path.join(currentdir,one_filename))
except FileNotFoundError:
pass
上面的代码包括与先前版本的三个更改
- 如所示,这不再为每个扩展名计数加 1,而是添加文件的大小,大小来自
os.path.getsize
。 -
os.path.join
将路径和文件名放在一起,并且(作为奖励)使用当前操作系统的路径分隔符。程序在 Windows 系统上使用的可能性有多大,因此需要反斜杠而不是斜杠?非常小,但使用这种内置操作并无害处。 -
os.walk
通常不查看符号链接,这意味着您可能会在尝试测量不存在的文件的大小时遇到一些麻烦。因此,这里的计数被包装在try/except
块中。
完成后,您可以确定目录中占用空间最大的文件类型
In [46]: for extension, count in counts.most_common(10):
...: print(f"{extension:8}{count}")
...:
.pack 669153001
.zip 486110102
.ipynb 223155683
.sql 125443333
46296632
.json 14224651
.txt 10921226
.pdf 7557943
.py 5253208
.pyc 4948851
现在情况似乎有所不同!在我的例子中,看起来我在 .pack 文件中有很多东西,表明我的 Git 存储库(我存储所有旧的培训示例、练习和 Jupyter 笔记本的地方)非常大。我有很多 zip 文件,我在其中存储我的每日更新。当然,还有很多 Jupyter 笔记本,它们以 JSON 格式编写,并且可能会变得很大。令我惊讶的是 .sql 扩展名,我真的忘记了我有这个。
每年文件数如果您想知道每年修改的每种类型的文件有多少个怎么办?这对于删除日志文件或(如果您像我一样)识别哪些大的、不必要的文件占用空间可能很有用。
为了做到这一点,您需要获取每个文件的修改时间(UNIX 术语中的 mtime
)。然后,您需要将 mtime
从 UNIX 时间(即自 1970 年 1 月 1 日以来的秒数)转换为您可以解析和使用的内容。
您可以只使用字典,而不是使用 Counter
对象来跟踪事物。但是,此字典的值将是 Counter
,年份作为键,计数作为值。由于您知道所有主字典都将是 Counter
对象,因此您可以只使用 defaultdict
,这将需要您编写更少的代码。
以下是如何完成所有这些操作
from collections import defaultdict, Counter
from datetime import datetime
counts = defaultdict(Counter)
for currentdir, dirnames, filenames in os.walk('.'):
for one_filename in filenames:
first_part, ext = os.path.splitext(one_filename)
try:
full_filename = os.path.join(currentdir,
↪one_filename)
mtime =
↪datetime.fromtimestamp(os.path.getmtime(full_filename))
counts[ext][mtime.year] += 1
except FileNotFoundError:
pass
首先,这会将 counts
创建为具有 Counter
的 defaultdict
实例。这意味着如果您请求尚不存在的键,则将创建该键,其值将是一个新的 Counter
,允许您说这样的话
counts['.zip'][2018] += 1
而无需初始化 zip
键(用于计数)或 2018
键(用于 Counter
对象)。您可以只将计数加一,并且知道它正在工作。
然后,当您迭代文件系统时,您从文件名中获取 mtime
(使用 os.path.getmtime
)。使用 datetime.fromtimestamp
将其转换为 datetime
对象,这是一个很棒的函数,可让您从 UNIX 时间戳移动到人类风格的日期和时间。最后,您将计数加 1。
再次,您可以显示结果
for extension, year_counts in counts.items():
print(extension)
for year, file_count in sorted(year_counts.items()):
print(f"\t{year}\t{file_count}")
counts
变量现在是一个 defaultdict
,但这表示它在大多数方面都像字典一样工作。因此,您可以使用 items
迭代其键和值,如此处所示,获取每个文件扩展名和每个文件的 Counter
对象。
接下来,打印扩展名,然后迭代年份及其计数,按年份对它们进行排序,并使用制表符(\t
)字符稍微缩进地打印它们。通过这种方式,您可以精确地看到每年修改了多少个每个扩展名的文件——并且可能了解哪些文件真正重要,哪些文件您可以轻松摆脱。
对于简单的脚本,Python 不能也不应该取代 Bash,但在许多情况下,如果您正在处理大量文件和/或创建报告,Python 的标准库可以轻松地使用最少的代码完成此类任务。