用 Python os.scandir 高效遍历大目录并统计文件大小

举报
yd_240403232 发表于 2026/11/06 14:45:07 2026/11/06
【摘要】 遍历大目录统计文件大小时,逐条 os.stat 会重复触发系统调用。本文用 os.scandir 的 DirEntry 缓存读取类型和大小,跳过符号链接与权限错误,并给出可运行脚本及与 os.listdir 思路的对比。

用 Python os.scandir 高效遍历大目录并统计文件大小

问题场景

日志归档、构建产物或数据目录往往包含大量文件与多层子目录。常见写法是先用 os.listdir 取名字,再用 os.path.join 拼路径,然后调用 os.path.isdir、os.path.isfile、os.path.getsize。这样每个条目可能触发多次系统调用,路径拼接也会增加开销。对于大目录,瓶颈通常不在 Python 循环本身,而在重复的 stat 与类型判断。

os.scandir 返回 DirEntry 对象,它在迭代目录时就能提供名称、路径以及文件类型信息;在支持的文件系统上,类型判断可以直接使用目录项信息。DirEntry.stat 会缓存同一 entry 的 stat 结果,避免对同一条目反复调用。统计文件大小仍需要一次 stat,但每个普通文件只取一次 st_size。

最小可运行脚本

以下脚本使用栈迭代,避免递归深度限制;跳过符号链接;捕获权限错误和常见 OSError;只统计普通文件。

#!/usr/bin/env python3
import os
import sys
from typing import Tuple

def dir_size(root: str) -> Tuple[int, int]:
    total_size = 0
    file_count = 0
    stack = [root]

    while stack:
        current = stack.pop()
        try:
            with os.scandir(current) as it:
                for entry in it:
                    try:
                        if entry.is_symlink():
                            continue
                        if entry.is_dir(follow_symlinks=False):
                            stack.append(entry.path)
                        elif entry.is_file(follow_symlinks=False):
                            st = entry.stat(follow_symlinks=False)
                            total_size += st.st_size
                            file_count += 1
                    except OSError:
                        continue
        except OSError:
            continue

    return total_size, file_count

if __name__ == '__main__':
    target = sys.argv[1] if len(sys.argv) > 1 else '.'
    size, count = dir_size(target)
    print(f'files={count} bytes={size}')

运行方法

保存为 scan_size.py,然后执行:

python scan_size.py /path/to/dir

输出示例只表示格式:

files=123 bytes=456789

可以构造临时目录验证跳过逻辑。在 Linux 下可用非 root 用户创建无权限目录;如果以 root 运行,权限错误可能不会出现。

mkdir -p demo/sub demo/locked
printf 'abc' > demo/a.txt
: > demo/sub/empty.bin
ln -s ../a.txt demo/sub/link.txt
chmod 000 demo/locked
python scan_size.py demo
chmod 700 demo/locked

预期只统计普通文件,符号链接与不可读目录不计入。具体文件数取决于你创建的目录内容。

与 os.listdir 思路的对比

等价的 listdir 版本通常会这样写:

def dir_size_listdir(root):
    total_size = 0
    file_count = 0
    stack = [root]
    while stack:
        current = stack.pop()
        try:
            names = os.listdir(current)
        except OSError:
            continue
        for name in names:
            path = os.path.join(current, name)
            try:
                if os.path.islink(path):
                    continue
                if os.path.isdir(path):
                    stack.append(path)
                elif os.path.isfile(path):
                    total_size += os.stat(path).st_size
                    file_count += 1
            except OSError:
                continue
    return total_size, file_count

listdir 只返回名称,后续 islink、isdir、isfile、stat 都需要基于拼接后的路径再次判断。对于普通文件,可能经历多次类型判断和至少一次 stat。scandir 版本直接使用 DirEntry,先跳过符号链接,再用 is_dir/is_file 判断类型,最后对普通文件调用一次 stat 取大小。实际差异取决于文件系统是否提供目录项类型、操作系统缓存以及目录规模;不要把它理解成固定倍数的加速。

如果要自行测量,用 time.perf_counter 包住两个函数,在同一目录重复运行并记录中位数。结果会受页缓存和文件系统影响,应同时记录运行环境。不要为了对比而修改系统缓存参数。

边界与限制

  • 只统计普通文件。目录自身的元数据大小不计入;符号链接不统计也不跟随,避免循环。
  • 硬链接会在多个路径下重复计数。若需要按 inode 去重,可记录 (st_dev, st_ino)。
  • st_size 是逻辑大小,不等于磁盘占用。Unix 下磁盘占用可参考 st_blocks;稀疏文件两者差异明显。
  • 权限不足的目录或文件会被跳过。root 用户可能绕过权限检查,测试时需注意。
  • 遍历期间文件可能被创建、删除或修改,结果不是一致性快照。
  • 某些网络文件系统、FUSE 或特殊文件系统可能不提供目录项类型,DirEntry 仍可能触发额外系统调用;缓存行为也依实现而定。
  • 脚本只保存目录路径,不长期保存 DirEntry。不要依赖已关闭迭代器中的 DirEntry 缓存做后续判断。
  • 大目录下 scandir 逐个产出条目,内存占用较低;但待处理目录栈仍可能保存大量路径。
  • 绑定挂载或循环挂载未被特殊处理,极端情况下可能重复遍历。生产脚本可增加已访问设备与 inode 集合。

小结

统计大目录时,核心是减少重复的路径判断与 stat。os.scandir 配合 DirEntry 的缓存,能让每个普通文件只取一次大小,并用类型信息决定是否递归;同时显式跳过符号链接和权限错误,脚本更稳健。性能对比应在目标环境自行测量,关注系统调用次数和缓存影响,而不是依赖固定结论。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。