用 Python os.scandir 高效遍历大目录并统计文件大小
用 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 的缓存,能让每个普通文件只取一次大小,并用类型信息决定是否递归;同时显式跳过符号链接和权限错误,脚本更稳健。性能对比应在目标环境自行测量,关注系统调用次数和缓存影响,而不是依赖固定结论。
- 点赞
- 收藏
- 关注作者
评论(0)