登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

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 列表决定进入哪些子目录。因此必须修改列表本身,不能只把局部变量重新绑定到一个新列表。

Path根目录、dirnames原列表、忽略集合与剩余子目录的静态数据结构说明图
图1:top_down 模式下,过滤规则原地更新 dirnames;Path.walk 只继续访问列表中保留下来的子目录。
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() 的分类行为存在差异,迁移旧代码时不能默认两者完全一致。

Path.walk 遍历方向、符号链接、错误回调与路径规则的静态依赖说明图
图2:目录裁剪还受遍历方向、符号链接策略、错误回调和相对路径规则共同约束。

如果显式开启 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 更直接,也更容易验证访问边界。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>