Python os.walk 跳过目录:重新赋值为什么无效,切片修改却能阻止向下遍历

10-01 3阅读

不处理文件,不等于没进入目录

做项目扫描时,通常希望跳过依赖目录和缓存目录。有些脚本进入这些目录后才判断文件是否需要忽略,结果依旧花时间读取大量目录项;另一些脚本筛选了 dirs,却发现遍历路线完全没变。要真正阻止下降到某条分支,必须在遍历器继续向下之前修改它正在使用的目录列表。

os.walk 默认从父目录向子目录遍历,每次返回当前路径、子目录名称列表和文件名称列表。自顶向下模式允许调用者原地修改子目录列表。它并不会监视局部变量后来绑定到了哪里,所以“换一个列表”与“修改原列表内容”产生不同效果。

在临时目录里比较三种写法

下面代码保存为 Python 文件运行,本文在三点十二验证。它创建四个小文件,其中两个位于准备跳过的目录,结束时自动清理临时目录。结果统一转换成相对路径并排序,因此不会因为临时根目录名称或文件系统枚举顺序而改变输出。

第一种写法重新给变量赋值,第二种用切片替换原列表的内容,第三种仍用切片但改成自底向上遍历。三个结果一起看,才能证明生效条件既包括修改方式,也包括修改发生的时间,而不是某个过滤表达式写法更特别。

Python os.walk 跳过目录:重新赋值为什么无效,切片修改却能阻止向下遍历

AI概念配图,非真实界面

import os
from pathlib import Path
from tempfile import TemporaryDirectory

SKIP = {'vendor', '__pycache__'}

def scan(root, *, inplace, topdown=True):
    found = []
    for current, dirs, files in os.walk(root, topdown=topdown):
        keep = sorted(name for name in dirs if name not in SKIP)
        if inplace:
            dirs[:] = keep
        else:
            dirs = keep
        for name in files:
            found.append((Path(current) / name).relative_to(root).as_posix())
    return sorted(found)

def stop_on_error(error):
    raise error

with TemporaryDirectory() as folder:
    root = Path(folder)
    paths = ['README.txt', 'src/main.py',
             'src/__pycache__/main.pyc', 'vendor/pkg.py']
    for name in paths:
        path = root / name
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text('sample', encoding='utf-8')
    rebound = scan(root, inplace=False)
    pruned = scan(root, inplace=True)
    bottom_up = scan(root, inplace=True, topdown=False)
    assert rebound == bottom_up == sorted(paths)
    assert pruned == ['README.txt', 'src/main.py']
    print('rebound count:', len(rebound))
    print('pruned files:', pruned)
    print('bottom-up count:', len(bottom_up))
    try:
        list(os.walk(root / 'missing', onerror=stop_on_error))
    except FileNotFoundError:
        print('missing root: reported')
    else:
        raise AssertionError('scan failure hidden')
print('all walk checks passed')

原地修改的是遍历器还会使用的对象

第一种写法仍找到四个文件,说明局部变量指向的新列表没有改变原来的下降计划。第二种只保留说明文件和源码文件。切片赋值让原列表保持身份,内容却变成筛选后的目录名称,遍历器继续执行时就会按这个列表选择接下来的分支。

自底向上模式再次找到四个文件,因为处理到父目录时,它的子目录已经遍历过了。此时再删列表项也无法撤回之前的访问。需要按规则剪枝,就采用自顶向下;需要先处理子项再处理父项,则要接受剪枝策略必须另行设计这一边界。

列表中的项目只是当前目录下的名称,不是完整路径。示例按名称跳过 vendor,所以任何层级同名目录都会被跳过。如果只想忽略项目根目录的一项,应结合当前路径与根路径判断,不能把一个局部命名约定扩大成整棵目录树的规则。

排序、错误与链接需要各自决定

对保留下来的目录名排序可以稳定下降顺序,但并不会自动排序文件列表。示例最终排序所有相对路径,是为了输出可复核。生产扫描若要流式输出且保证顺序,就要同时考虑每层文件排序与目录遍历顺序,避免为了排序一次性积累过多结果。

默认情况下,底层扫描出错可能被忽略。缺失目录、权限不足或扫描期间的移动会让“没有结果”变得含糊。示例用 onerror 把异常重新抛出,区分空目录与扫描失败;批量工具也可以记录失败项继续运行,但最终报告应表明结果并不完整。

os.walk 默认不进入指向目录的符号链接。开启跟随以后可能遇到环路,它不会替你记录所有已访问目录;是否跟随、如何识别重复节点都应提前约定。过滤几个目录名也不构成访问控制,目录树在扫描过程中仍可能被别的进程修改。

把这段逻辑接到代码统计或资源清单工具时,先保留一个小目录树作为验收样本,检查应跳过和应保留的文件都符合预期。先证明遍历范围正确,再增加内容读取与统计,问题会更容易定位到具体阶段。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。