Python批量获取目录下所有文件路径的方法
深入讲解 Python 批量获取指定目录下所有文件路径的四大主流方法,涵盖 pathlib.Path.rglob()、os.walk()、os.scandir() 和 glob.glob() 的语法示例、底层性能机制、目录剪枝与选型指南。
pathlib.Path.rglob(),兼顾可读性与跨平台规范;而在需要跳过指定子目录(剪枝)或处理数万乃至百万级超大目录的高吞吐场景下,则分别采用经典递归工具 os.walk() 与低层缓存加速的 os.scandir()。
一、核心方法对比与技术架构
在标准库中,Python 提供了多个用于文件和目录遍历的模块。很多开发者在初学阶段容易混淆 os.listdir()、glob.glob()、os.walk() 以及 pathlib 之间的边界。理解各模块的底层机制有助于在实际项目中做出精准的技术选型。
从架构上看,各方案的区别体现在三处关键设计上:
- 内存加载机制:
glob.glob()默认返回完整的列表,一次性将所有路径载入内存;而pathlib.Path.rglob()、os.walk()以及os.scandir()返回迭代器或生成器,具备惰性求值特性,内存开销恒定。 - 文件属性查询:传统的
os.listdir()结合os.path.isfile()会针对每一个文件名触发一次底层的stat()系统调用;而现代的os.scandir()和基于其封装的模块会在目录读取时预先缓存文件类型信息,避免重复上下文切换。 - 路径抽象类型:
os与glob返回纯字符串路径,需要手动处理不同操作系统的分隔符;pathlib则提供面向对象的Path实例,内置统一的路径拼接与解析接口。
二、方案一:现代化工程首选 pathlib.Path.rglob()
自 Python 3.4 引入并在后续版本不断优化的 pathlib 模块,是目前编写现代 Python 代码的首选。其中 rglob() 方法用于递归匹配目录树下的所有文件。
1. 完整代码示例
from pathlib import Path
from typing import List
def get_all_file_paths_pathlib(dir_path: str, ext: str = None) -> List[str]:
"""
使用 pathlib 递归获取指定目录下所有文件的绝对路径
:param dir_path: 目标目录路径
:param ext: 可选,按文件扩展名过滤(例如 '.py' 或 '.csv')
:return: 规范化的绝对路径字符串列表
"""
target_dir = Path(dir_path)
# 基础校验:确保目标路径存在且为目录
if not target_dir.exists() or not target_dir.is_dir():
raise ValueError(f"指定的路径不存在或不是有效目录: {dir_path}")
file_paths = []
# rglob('*') 匹配当前目录及其所有子目录中的所有节点
for item in target_dir.rglob('*'):
# 必须显式判断 item.is_file(),因为 rglob 同时会产出子目录本身的 Path 对象
if item.is_file():
if ext is None or item.suffix.lower() == ext.lower():
# resolve() 解析软链接与相对标记,返回标准化绝对路径
file_paths.append(str(item.resolve()))
return file_paths
if __name__ == '__main__':
# 示例调用
paths = get_all_file_paths_pathlib('./sample_project', ext='.py')
print(f"找到符合条件的文件总数: {len(paths)}")
for p in paths[:3]:
print(f" - {p}")
- /Users/developer/sample_project/main.py
- /Users/developer/sample_project/utils/helper.py
- /Users/developer/sample_project/modules/core.py
rglob('*') 产出的结果既包含文件也包含中间目录。如果不调用 item.is_file() 进行校验,获取到的列表中将混杂子目录自身的路径。此外,若只需遍历直接子级目录而不需要深层递归,将 rglob('*') 替换为 glob('*') 即可。
三、方案二:目录剪枝与深度遍历 os.walk()
当目标文件夹中包含版本控制目录(如 .git)、依赖缓存(如 node_modules、__pycache__)或无权限访问的特定敏感目录时,os.walk() 是实现高效排除(剪枝)的标准工具。
os.walk() 在每轮迭代中产生一个包含三个元素的三元组:(root, dirs, files)。关键机制在于:在循环体内直接就地修改 dirs 列表的内容,os.walk() 将严格放弃遍历被移除的目录子树。
1. 支持目录排除的剪枝代码
import os
from typing import List, Set
def get_files_with_pruning(base_dir: str, exclude_dirs: Set[str] = None) -> List[str]:
"""
使用 os.walk 遍历目录,支持就地剪枝排除指定子目录
:param base_dir: 根目录路径
:param exclude_dirs: 需要跳过的目录名集合(例如 {'.git', 'node_modules', '__pycache__'})
:return: 文件的绝对路径列表
"""
if exclude_dirs is None:
exclude_dirs = {'.git', '.idea', 'node_modules', '__pycache__', '.venv'}
result = []
# topdown=True 是能够进行目录剪枝的必要前提
for root, dirs, files in os.walk(base_dir, topdown=True):
# 核心剪枝操作:使用切片赋值就地修改 dirs 列表
# dirs[:] 改变的是原列表内存引用,不可使用 dirs = [...] 替换
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for file_name in files:
full_path = os.path.join(root, file_name)
result.append(os.path.abspath(full_path))
return result
if __name__ == '__main__':
# 示例调用
matched_files = get_files_with_pruning('/var/www/my_project')
print(f"剪枝过滤后收集到的文件数量: {len(matched_files)}")
dirs = [d for d in dirs if ...],只是让局部变量 dirs 指向了新列表对象,os.walk() 内部持有的原有列表并未改变,遍历器依旧会深入被排除的目录。必须使用 dirs[:] = [...] 进行就地切片替换。
四、方案三:极致吞吐与系统级调用优化 os.scandir()
在面对包含数十万乃至数百万个文件的大型存储、日志归档或文件服务器时,常规遍历往往消耗大量 CPU 时间在用户态与内核态的切换上。Python 3.5 引入的 os.scandir() 返回轻量级的 os.DirEntry 对象,直接复用底层操作系统 readdir 系统调用返回的元数据缓存。
1. 递归生成器实现
os.scandir() 本身是非递归的。结合生成器函数,可以构建一个占用内存极小且 I/O 效率极高的全目录扫描工具:
import os
from typing import Iterator
def scan_files_fast(directory: str) -> Iterator[str]:
"""
基于 os.scandir 实现的高性能流式文件扫描生成器
:param directory: 目标根目录
:return: 文件的绝对路径生成器
"""
try:
# scandir 作为上下文管理器可以确保及时释放底层系统文件描述符
with os.scandir(directory) as entries:
for entry in entries:
try:
# entry.is_file() 优先从 dirent 缓存中读取,不触发附加 stat 系统调用
if entry.is_file(follow_symlinks=False):
yield entry.path
elif entry.is_dir(follow_symlinks=False):
# 发现子目录时递归调用自身
yield from scan_files_fast(entry.path)
except PermissionError:
# 单个文件访问权限受限时优雅跳过,保证主流程不中断
continue
except PermissionError:
# 针对无权限访问的子目录进行静默跳过
pass
if __name__ == '__main__':
# 流式消费示例:无需一次性生成大列表,边扫描边处理
count = 0
for file_path in scan_files_fast('/mnt/storage/dataset'):
count += 1
if count <= 2:
print(f"流式提取: {file_path}")
print(f"扫描完毕,累计处理文件: {count}")
流式提取: /mnt/storage/dataset/batch_01/img_002.png
扫描完毕,累计处理文件: 350280
五、方案四:通配符模式匹配 glob.glob()
glob 模块支持类 Unix Shell 风格的文件名模式匹配(如 * 匹配任意字符,? 匹配单个字符,[0-9] 匹配数字区间)。如果只需要编写几行自动化脚本来提取特定格式的文件,glob 是最简短的方式。
1. 递归提取示例
import glob
import os
def get_files_by_glob(search_pattern: str) -> list:
"""
使用 glob 模块按规则提取文件路径
:param search_pattern: 匹配表达式,必须包含 ** 并启用 recursive=True 才能跨目录递归
"""
# recursive=True 允许 ** 匹配任意深度的层级目录
return glob.glob(search_pattern, recursive=True)
if __name__ == '__main__':
# 示例:递归查找当前目录下所有子目录中的所有 json 文件
pattern = "./data/**/*.json"
results = get_files_by_glob(pattern)
# 转换为绝对路径
abs_results = [os.path.abspath(p) for p in results if os.path.isfile(p)]
print(f"匹配到的 JSON 文件数目: {len(abs_results)}")
注意:当目标目录层级极深时,glob.glob() 会在内存中一次性分配全部匹配项的列表。若数据量庞大,建议改用 glob.iglob() 获得惰性生成器。
六、工程实践中的边界陷阱与防范
在实际生产部署中,批量获取文件路径常常面临跨平台差异、权限拦截和死循环等现实问题:
- 符号链接导致的死循环(Symlink Recursion):如果目录下存在指向父级目录或互相关联的软链接,递归遍历可能陷入无限死循环并触发栈溢出。在
pathlib与os.scandir中,默认行为均不会跟随危险的软链接(follow_symlinks=False)。在自定义递归中应保持此默认值,或通过检查os.path.realpath()记录已访问的 inode。 - Windows 与 POSIX 分隔符兼容性:在 Windows 系统中路径分隔符为反斜杠
\,而在 Linux/macOS 中为正斜杠/。直接使用字符串拼接(例如root + '/' + file)极易引发错误。应始终使用os.path.join()或pathlib.Path / "sub_path"语法。 - 无权限目录与 PermissionError:大型服务器或多用户环境下经常会遇到无读权限的文件夹。如果不加防护,单个无权子目录将导致整个扫描任务崩溃。编写递归逻辑时需在访问各级目录时加入
try...except PermissionError保护。
七、全方案对比与选型决策指南
针对不同的业务规模和功能需求,可参考以下特性对比矩阵与决策树进行选型:
| 方法名称 | 所属模块 | 返回形态 | 深层递归 | 就地剪枝 | I/O 吞吐级别 | 推荐使用场景 |
|---|---|---|---|---|---|---|
| Path.rglob() | pathlib |
生成器 (Path 对象) | 原生支持 | 需手动过滤 | 良好 (标准级) | 标准首选 日常业务逻辑与跨平台通用开发 |
| os.walk() | os |
迭代器 (三元组) | 原生支持 | 原生支持 (修改 dirs) | 良好 (标准级) | 需要灵活排除指定子目录、忽略隐藏版本库 |
| os.scandir() | os |
迭代器 (DirEntry) | 需编写递归 | 支持 | 极致极速 (缓存元数据) | 十万至百万级以上超大规模文件的批量批处理 |
| glob.glob() | glob |
列表 / 生成器 (iglob) | 需结合 ** 参数 | 不支持 | 中等 | 运维脚本、临时任务中基于后缀的一行代码快速抓取 |
总结:对于绝大多数常规开发任务,直接使用 pathlib.Path.rglob() 能够以最小的代码心智负担获得最佳的类型安全与代码可读性;如果遇到需要跳过 .git 等无用子树的场景,采用 os.walk() 配合 dirs[:] 剪枝是性价比最高的方式;而当性能瓶颈出现在极高密度的海量 I/O 操作时,采用基于 os.scandir() 构建的自定义递归生成器将释放最大吞吐潜力。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













