Python pathlib.walk 如何在遍历时跳过目录树
来源:17golang原创
时间:2026-10-09 23:22:56 401浏览 收藏
用 pathlib.Path.walk() 遍历目录树时,跳过整个子树的关键不是在拿到文件后再做过滤,而是在 top_down=True 模式下原地修改当前轮返回的 dirnames 列表。最常用的写法是 dirnames[:] = [...]:不想进入的目录名从列表中消失后,遍历器就不会继续下探对应目录。
这个方法适合排除 .git、__pycache__、node_modules、构建产物和按相对路径指定的私有目录。Path.walk() 从 Python 3.12 加入标准库;如果项目还在 Python 3.11 或更低版本,应继续使用行为相近的 os.walk()。
官方文档:https://docs.python.org/3/library/pathlib.html#pathlib.Path.walk
一、触发信号:过滤了文件,遍历却仍然进入大目录
常见故障信号是程序最终没有处理被排除目录中的文件,但扫描时间、磁盘读取量和权限告警一点没少。原因通常是过滤发生得太晚:代码先进入所有子目录,生成文件路径以后才用 if 丢掉结果。此时只是“不要这些文件”,并没有“不要访问这棵子树”。
Path.walk() 每轮产出 (dirpath, dirnames, filenames) 三元组。其中 dirpath 是当前目录的 Path 对象,另外两个列表中的元素只是名称字符串。要组成完整路径,需要写成 dirpath / name。官方文档还说明,这两个列表的顺序取决于文件系统;如果业务依赖稳定顺序,应显式排序。
from pathlib import Path
root = Path("project")
for dirpath, dirnames, filenames in root.walk():
# dirnames 和 filenames 保存名称字符串,完整路径要与 dirpath 拼接。
for filename in sorted(filenames):
file_path = dirpath / filename
print(file_path)
先确认运行环境:如果 Path(".").walk 不存在,通常意味着 Python 版本低于 3.12,而不是导入方式错误。这个版本边界应在部署检查中明确记录。
二、核心处理:只改 dirnames,而且要原地改
top_down 默认就是 True。在这个模式下,父目录的三元组会先交给调用方,遍历器随后根据同一个 dirnames 列表决定进入哪些子目录。因此必须修改列表本身,不能只把局部变量重新绑定到一个新列表。

from pathlib import Path
SKIP_NAMES = {".git", "__pycache__", "node_modules", "dist", "build"}
def iter_source_files(root: Path):
for dirpath, dirnames, filenames in root.walk(top_down=True):
# 用切片赋值原地更新原列表,真正阻止遍历器进入这些子树。
dirnames[:] = [name for name in dirnames if name not in SKIP_NAMES]
for filename in filenames:
path = dirpath / filename
if path.suffix == ".py":
yield path
for source_file in iter_source_files(Path("project")):
print(source_file)
这里的 dirnames[:] = ... 与 dirnames = ... 看起来只差一个切片,效果却完全不同。前者保留原列表对象并替换内容,遍历器能看到变化;后者只是让当前函数里的名字指向新列表,遍历器仍持有旧列表,所以不会停止下探。
| 写法 | 是否裁剪子树 | 原因 |
|---|---|---|
dirnames[:] = kept | 是 | 原列表内容被更新 |
dirnames.remove("dist") | 是 | 直接修改原列表 |
dirnames = kept | 否 | 只重新绑定局部变量 |
只过滤 filenames | 否 | 不影响进入子目录的决定 |
三、按相对路径跳过,避免同名目录被全部排除
按名称过滤很直接,但它会排除任意层级中的同名目录。例如全局排除 cache,可能误伤源码树里一个合法的 src/cache 包。生产脚本通常需要两层规则:少量确定无用的目录按名称排除,位置敏感的目录按相对路径排除。
from pathlib import Path
SKIP_NAMES = {".git", "__pycache__"}
SKIP_RELATIVE = {
Path("frontend/node_modules"),
Path("services/report/private_exports"),
}
def should_descend(root: Path, parent: Path, name: str) -> bool:
if name in SKIP_NAMES:
return False
child = parent / name
# relative_to 让规则与根目录绑定,避免依赖机器上的绝对路径。
relative_child = child.relative_to(root)
return relative_child not in SKIP_RELATIVE
def collect_json_files(root: Path) -> list[Path]:
found: list[Path] = []
for dirpath, dirnames, filenames in root.walk(top_down=True):
# 仍然原地更新,让相对路径规则参与下一层目录选择。
dirnames[:] = [
name for name in dirnames if should_descend(root, dirpath, name)
]
found.extend(
dirpath / name for name in filenames if name.endswith(".json")
)
return found
如果规则希望排除某个路径下的全部后代,不必把每一级子目录都列出来;只要在它作为父目录的 dirnames 项出现时将其删除即可。判断中优先使用 Path 运算,不要手写斜杠拼接字符串,这样 Windows 与 POSIX 路径分隔符差异不会进入规则。
四、把符号链接和读取错误放进边界检查
目录裁剪不是唯一边界。Path.walk() 默认 follow_symlinks=False,指向目录的符号链接会被放进 filenames,而不是当成普通子目录继续进入。这一点与 os.walk() 的分类行为存在差异,迁移旧代码时不能默认两者完全一致。

如果显式开启 follow_symlinks=True,符号链接可能指回父目录,从而形成无限递归。官方实现不会自动记录已经访问过的目录,因此应保持默认值,或自行维护已访问目录集合。对扫描失败的目录,可通过 on_error 接收 OSError;回调返回后继续遍历,重新抛出则终止。
from pathlib import Path
def report_walk_error(error: OSError) -> None:
# filename 会指出哪一个目录在扫描时失败,便于记录权限问题。
failed_path = getattr(error, "filename", None)
print(f"无法读取目录: {failed_path}; 原因: {error}")
def walk_safely(root: Path):
for dirpath, dirnames, filenames in root.walk(
top_down=True,
on_error=report_walk_error,
follow_symlinks=False,
):
# 即使某个目录读取失败,其他可访问目录仍可继续处理。
dirnames[:] = [name for name in dirnames if name != ".git"]
yield dirpath, dirnames, filenames
还要注意遍历期间目录被替换的情况。官方文档说明,Path.walk() 假设目录树在遍历中不会被修改;如果 dirnames 里的目录后来被替换成符号链接,遍历行为可能不符合原先判断。面对持续变化的上传目录或发布目录,应使用快照目录、锁或版本化目录,而不是把一次遍历当成强一致视图。
五、快速验收:记录“访问过的目录”,不要只看最终文件
验收目录裁剪时,最有价值的信号是实际访问过哪些 dirpath。只检查最终文件列表,无法区分“没有进入被排除目录”和“进入后把文件结果丢掉”这两种实现。下面的最小测试构造一个临时目录树,并断言被排除子树没有出现在访问记录中。
from pathlib import Path
from tempfile import TemporaryDirectory
def visited_directories(root: Path) -> list[Path]:
visited: list[Path] = []
for dirpath, dirnames, _ in root.walk(top_down=True):
# 先记录当前目录,再原地删除不允许继续访问的缓存目录。
visited.append(dirpath.relative_to(root))
dirnames[:] = [name for name in dirnames if name != "cache"]
return visited
with TemporaryDirectory() as temporary:
root = Path(temporary)
(root / "src").mkdir()
(root / "cache" / "deep").mkdir(parents=True)
visited = visited_directories(root)
# 被裁剪的 cache 及其 deep 子目录都不应出现在访问记录中。
assert Path("cache") not in visited
assert Path("cache/deep") not in visited
assert Path("src") in visited
如果断言失败,按顺序检查三件事:是否使用 top_down=True;是否修改了 dirnames 而不是 filenames;是否使用切片赋值、remove() 或 del 修改原列表。top_down=False 时,即便修改 dirnames 也不会影响遍历,因为对应子目录在父目录三元组交给调用方之前就已经生成。
六、回滚与兼容:低版本改用 os.walk
如果发布后发现目标环境仍有 Python 3.11,最安全的回滚不是模拟一个不完整的 Path.walk(),而是切回 os.walk()。它同样支持在自顶向下模式中原地裁剪 dirs,只需把字符串根路径转换成 Path 后再进入后续业务逻辑。
import os
from pathlib import Path
def compatible_walk(root: Path):
for root_text, dirnames, filenames in os.walk(root, topdown=True):
# os.walk 也要求原地修改 dirnames 才能阻止进入子树。
dirnames[:] = [name for name in dirnames if name != "node_modules"]
yield Path(root_text), dirnames, filenames
回滚后要重新检查符号链接分类,因为 Path.walk() 在默认不跟随符号链接时,会把指向目录的链接列入 filenames。不要只替换函数名就认为语义完全一致。
发布前检查清单
- 运行环境是 Python 3.12 或更高版本;否则采用
os.walk()兼容实现。 - 需要裁剪子树时保持
top_down=True。 - 只通过切片赋值、
remove()或del原地修改dirnames。 - 名称规则与相对路径规则分开,避免误伤合法同名目录。
- 默认不跟随符号链接;如必须开启,增加已访问目录防环机制。
- 用
on_error决定读取失败时继续还是终止,并记录OSError.filename。 - 验收时检查访问过的目录,而不只检查最终收集到的文件。
相关问题
为什么 dirnames = filtered 没有跳过目录?
因为它只让局部变量指向新列表,没有修改遍历器持有的原列表。应使用 dirnames[:] = filtered。
top_down=False 时还能裁剪目录吗?
不能通过修改当前轮 dirnames 来阻止下探,因为子目录已经先被生成。目录裁剪应使用自顶向下模式。
Path.walk 默认会进入目录符号链接吗?
默认不会。follow_symlinks=False 时,指向目录的符号链接会进入 filenames。开启跟随时要自行防止链接环。
可以直接排序 dirnames 吗?
可以。文件系统返回顺序不保证稳定,需要可重复顺序时可在原列表上调用 dirnames.sort(),同时仍可删除不需要进入的名称。
glob 或 rglob 能替代 walk 的子树裁剪吗?
它们适合按模式收集路径,但当需求是根据运行时规则阻止进入某棵子树时,walk() 提供的可修改 dirnames 更直接,也更容易验证访问边界。
-
339 收藏
-
388 收藏
-
253 收藏
-
182 收藏
-
374 收藏
-
202 收藏
-
363 收藏
-
381 收藏
-
318 收藏
-
264 收藏
-
文章 · python教程 | 16小时前 | python · 内存优化 · Python教程 · 文件读取 大文件处理 Python mmap 分段映射 ALLOCATIONGRANULARITY146 收藏
-
225 收藏
-
文章 · python教程 | 20小时前 | 并发控制 · Python教程 · asyncio · 虚假唤醒 wait_for Python asyncio asyncio.Condition 异步同步478 收藏
-
417 收藏
-
文章 · python教程 | 1天前 | 异常处理 · 并发编程 · Python教程 · asyncio · asyncio 结构化并发 ExceptionGroup except* Python TaskGroup208 收藏
-
文章 · python教程 | 1天前 | 并发编程 · 工程实践 · Python教程 · 多进程日志 QueueListener multiprocessing.Queue RotatingFileHandler Python QueueHandler186 收藏
-
399 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习