Python目录遍历:listdir、walk与scandir的性能对比与选型指南 1. 项目概述为什么我们需要三种遍历目录的方法在Python的日常开发中处理文件和目录几乎是绕不开的活儿。无论是写一个批量重命名图片的小脚本还是构建一个需要扫描整个项目结构来收集日志文件的分析工具你都得先知道怎么把目录里的东西“翻”出来。很多新手朋友一上来就搜“Python怎么遍历文件夹”然后大概率会找到os.listdir这个方法用着用着发现哎它只能看一层子文件夹里的文件怎么办于是又去搜发现了os.walk这个“大杀器”感觉一下子通透了。但用久了可能又会觉得os.walk在遍历超大型目录时好像有点慢这时候如果你再深究一下就会遇到Python 3.5引入的os.scandir它号称性能更好是os.listdir的增强版。所以今天我们就来把这三位“文件系统探险家”掰开揉碎了讲清楚。这不仅仅是三个函数怎么用的问题更是关于在不同场景下如何做出最合适选择的问题。os.listdir简单直接适合浅层遍历os.walk递归深入适合处理复杂的树状结构os.scandir则在性能和功能上做了平衡尤其适合对遍历速度有要求或者需要文件元信息的场景。理解它们各自的脾气秉性你就能在写代码时更加游刃有余避免因为选错工具而导致的性能瓶颈或者代码臃肿。2. 核心需求与场景解析你的代码到底需要什么在动手写遍历目录的代码之前先别急着复制粘贴。停下来花半分钟想想你的具体需求是什么这能帮你省下后面大量调试和重构的时间。场景一快速获取某个文件夹下的直接内容列表。比如你有一个“下载”文件夹里面混杂着一些图片、文档和压缩包你现在只想快速列出所有这些项目的名字然后 maybe 用程序自动把它们分分类。这种情况下你不需要关心子文件夹里有什么os.listdir()就是最轻量、最直观的选择。它返回一个简单的文件名列表代码写起来最快。场景二需要递归处理整个目录树的所有文件。这是更常见的需求。想象一下你要写一个程序统计你整个代码项目里所有.py文件的代码行数或者找出某个图片文件夹包含无数子文件夹里所有大于2MB的.jpg文件。这种需求要求你的代码能像“挖地三尺”一样钻进每一层子目录。os.walk()就是为这种场景而生的它通过生成器generator的方式一层一层地“走”遍目录树把每一层的路径、子目录列表和文件列表都喂给你。场景三对遍历性能有要求或需要丰富的文件属性。当你的脚本需要扫描一个包含数十万甚至上百万文件的存储系统比如NAS、大型备份目录时遍历速度就成了关键。同时你可能不仅需要文件名还需要立刻知道它是文件还是目录、大小多少、最后修改时间是什么。传统的os.listdir()只给名字要获取这些信息还得额外调用os.stat()每次调用都是一次磁盘I/O非常耗时。os.scandir()在这里就是救星它在一次系统调用中就能获取到这些元数据在支持的系统上比如Windows和Linux的新版本返回的DirEntry对象自带这些属性性能提升非常显著。搞清楚你的场景属于以上哪一种选择就变得简单了。下面我们就逐一拆解这三位选手。3. 方法一os.listdir() —— 简单直接的“快照”os.listdir(path)大概是所有人学会的第一个目录遍历函数。它的功能非常纯粹接收一个路径字符串返回一个列表里面包含了该路径下所有文件和目录的名称不包括.和..。3.1 基础用法与输出解析import os # 假设当前目录下有一个文件夹叫 test_dir contents os.listdir(test_dir) print(contents) # 可能的输出[file1.txt, image.jpg, subfolder, document.pdf]看输出就是一个普普通通的Python列表。这里有几个关键点需要注意只包含名称不包含完整路径。打印出来是file1.txt而不是/home/user/test_dir/file1.txt。如果你后续需要操作这个文件比如打开、移动通常需要将目录路径和文件名拼接起来。os.path.join()是你的好帮手。顺序是不确定的。这个列表的顺序取决于操作系统和文件系统的具体实现你不能指望它按字母顺序或者创建时间排序。如果需要特定顺序记得用sorted()函数对返回的列表进行排序。包含所有项目。它一视同仁文件和文件夹都会列出来且不会区分它们。3.2 典型应用场景与代码示例因为它简单所以常用于一些轻量级、一次性的任务。示例1统计某个文件夹下特定类型文件的个数。import os folder_path ./projects all_items os.listdir(folder_path) # 计算Python文件的数量 python_file_count sum(1 for item in all_items if item.endswith(.py)) print(fPython文件数量: {python_file_count}) # 找出所有的目录 subdirectories [item for item in all_items if os.path.isdir(os.path.join(folder_path, item))] print(f子目录列表: {subdirectories})注意这里用到了os.path.isdir()来判断是否为目录。注意我们必须使用os.path.join(folder_path, item)来构造完整路径再传给判断函数。直接os.path.isdir(item)是错误的因为item只是一个名字没有路径信息程序会在当前工作目录下寻找item很可能找不到。示例2快速批量重命名当前目录下的文件。import os prefix vacation_2024_ counter 1 for filename in os.listdir(.): # . 代表当前目录 if filename.lower().endswith((.jpg, .jpeg, .png)): # 构造新名字 new_name f{prefix}{counter:03d}{os.path.splitext(filename)[1]} # 保留原后缀 os.rename(filename, new_name) print(fRenamed {filename} to {new_name}) counter 13.3 局限性为什么它不够用os.listdir()的核心局限就在于它的“扁平化”。它只给你当前层级的“快照”对于子目录它仅仅将其作为一个名字返回。如果你想处理子目录里的内容就必须自己写递归逻辑import os def list_all_files_recursively(directory): all_files [] for item in os.listdir(directory): full_path os.path.join(directory, item) if os.path.isdir(full_path): # 如果是目录递归调用自己 all_files.extend(list_all_files_recursively(full_path)) else: # 如果是文件加入列表 all_files.append(full_path) return all_files # 使用 files list_all_files_recursively(/some/path)虽然这样能实现功能但代码需要自己维护递归栈并且对于每个项目都要调用os.path.isdir()进行判断在文件数量巨大时效率不如专门的递归遍历工具。因此当需求涉及深层遍历时我们就需要请出下一位选手。4. 方法二os.walk() —— 递归遍历的“标准答案”如果说os.listdir是手动挡汽车需要你自己换挡处理递归那么os.walk就是自动挡SUV带你轻松翻山越岭遍历目录树。它是Python中处理文件系统递归遍历最常用、最强大的工具。4.1 生成器机制与三元组解包os.walk(top, topdownTrue, onerrorNone, followlinksFalse)是一个生成器函数。这意味着它不会一次性返回所有结果而是在循环中每次“产出”yield一层目录的信息这对于遍历非常大的目录树时可以节省内存。它每次产出的是一个三元组(root, dirs, files)root: 当前正在遍历的目录的路径字符串。dirs: 当前目录下所有子目录名称的列表不包括.和..。files: 当前目录下所有非目录文件名称的列表。import os for root, dirs, files in os.walk(/path/to/start): print(f当前目录: {root}) print(f 子目录: {dirs}) print(f 文件: {files}) print(- * 40)运行这段代码你会看到它从起始路径开始先输出第一层的信息然后自动进入每一个子目录继续输出直到遍历完所有分支。4.2 深度优先 vs. 广度优先topdown参数的精髓os.walk默认是“自上而下”topdownTrue的遍历这通常也是我们需要的。但这里有一个非常强大且容易被忽略的特性你可以在遍历中实时修改dirs列表来控制walk的行为。场景你希望遍历目录但跳过所有名为__pycache__或.git的文件夹。import os for root, dirs, files in os.walk(/my_project, topdownTrue): # 在进入子目录前从dirs列表中移除你想跳过的目录名 dirs[:] [d for d in dirs if d not in (__pycache__, .git, node_modules)] for file in files: if file.endswith(.py): full_path os.path.join(root, file) print(f找到Python文件: {full_path})关键点dirs[:] ...这行代码是原地修改dirs列表。os.walk在下一步会根据修改后的dirs列表来决定进入哪些子目录。因此被排除的目录如.git及其所有内容都不会被遍历到。这是一种高效的内存就地过滤。如果将topdown设为False则遍历顺序是“自下而上”。它会先遍历最深处的子目录最后才是起始目录。这种模式适用于你需要先处理所有叶子节点文件再处理父目录的场景比如计算目录大小需要先知道所有子目录的大小。但此时因为已经遍历过子目录了你再修改dirs列表就毫无意义了。4.3 实战应用复杂文件系统操作os.walk的典型应用场景都非常“实在”。应用1查找并收集特定类型的文件。import os def find_files_by_extension(start_path, extension): found_files [] for root, dirs, files in os.walk(start_path): for file in files: if file.endswith(extension): found_files.append(os.path.join(root, file)) return found_files # 查找所有 .log 文件 log_files find_files_by_extension(/var/log, .log)应用2计算整个目录树的总大小。import os def get_dir_size(start_path): total_size 0 for root, dirs, files in os.walk(start_path): for file in files: file_path os.path.join(root, file) # 跳过可能存在的符号链接或无法访问的文件 if os.path.isfile(file_path): try: total_size os.path.getsize(file_path) except OSError: pass # 忽略无法访问的文件 return total_size size_in_bytes get_dir_size(/home/user/Documents) size_in_mb size_in_bytes / (1024 * 1024) print(f目录总大小: {size_in_mb:.2f} MB)应用3镜像一个目录结构只创建空文件夹。import os def mirror_directory_structure(source, destination): for root, dirs, files in os.walk(source): # 计算当前目录相对于源目录的路径 path_rel_to_source os.path.relpath(root, source) # 在目标位置构造对应的目录路径 target_dir os.path.join(destination, path_rel_to_source) # 如果目录不存在则创建 if not os.path.exists(target_dir): os.makedirs(target_dir) print(f创建目录: {target_dir}) # 使用 mirror_directory_structure(/source/folder, /backup/folder)实操心得在使用os.walk时尤其是在网络驱动器或外部存储上务必考虑异常处理try...except。你可能会遇到权限不足、路径过长、文件在遍历过程中被删除等问题。给os.walk加上onerror回调函数或者在内层循环中对文件操作进行try能让你的脚本更健壮。5. 方法三os.scandir() —— 高性能的“现代迭代器”Python 3.5 引入了os.scandir()并在 Python 3.6 中将其确定为稳定的内置函数。它的设计目标很明确在提供比os.listdir()更丰富信息的同时获得比os.walk()在底层使用listdir和stat更好的性能。5.1 DirEntry对象不仅仅是名字os.scandir(path)返回一个os.DirEntry对象的迭代器。每个DirEntry对象代表目录中的一个条目它最大的优点是在大多数操作系统上它在扫描时就已经获取了文件的基本信息如类型、属性而不需要额外的系统调用。import os with os.scandir(/tmp) as entries: for entry in entries: print(f名称: {entry.name}) print(f 路径: {entry.path}) print(f 是文件? {entry.is_file()}) print(f 是目录? {entry.is_dir()}) print(f 是符号链接? {entry.is_symlink()}) # 获取stat信息如果不需要全部信息用上面的方法判断更快 stat_info entry.stat() print(f 大小: {stat_info.st_size} bytes) print(f 修改时间: {stat_info.st_mtime}) print(- * 20)关键优势在于entry.is_file()和entry.is_dir()这些方法。在Windows和较新的Linux内核上这些判断直接使用扫描时已缓存的信息速度极快。而如果你用os.listdir()需要先os.path.join得到完整路径再调用os.path.isdir(full_path)后者内部会发起一次新的stat系统调用性能开销大。5.2 性能对比实测scandir快在哪里我们来做一个简单的性能对比实验import os import time def test_listdir(path): start time.time() for _ in range(100): # 循环多次放大差异 for name in os.listdir(path): full_path os.path.join(path, name) if os.path.isdir(full_path): # 额外的一次系统调用 pass return time.time() - start def test_scandir(path): start time.time() for _ in range(100): with os.scandir(path) as entries: for entry in entries: if entry.is_dir(): # 使用缓存信息通常无额外系统调用 pass return time.time() - start path_to_scan /usr/bin # 一个包含大量文件的目录 time_listdir test_listdir(path_to_scan) time_scandir test_scandir(path_to_scan) print(fos.listdir isdir 耗时: {time_listdir:.4f} 秒) print(fos.scandir is_dir 耗时: {time_scandir:.4f} 秒) print(fscandir 比 listdir 快: {time_listdir/time_scandir:.2f} 倍)在我的测试环境Linux一个包含3000多个条目的目录上scandir的方式通常能快上2到5倍。目录越大、条目越多性能优势越明显。这是因为listdir方式需要为每个条目发起至少两次系统调用一次getdents读目录一次stat判断类型而scandir在支持的系统上如Linux的getdents系统调用已返回文件类型一次调用就拿到了所有信息。5.3 用scandir实现walk功能既然scandir这么好我们能用它来实现类似os.walk的递归遍历吗当然可以而且通过手动实现你还能获得更精细的控制。import os from collections import deque def walk_using_scandir(top): 使用 os.scandir 和队列实现广度优先遍历 (类似 os.walk) dirs_to_explore deque([top]) while dirs_to_explore: current_dir dirs_to_explore.popleft() files [] subdirs [] try: with os.scandir(current_dir) as entries: for entry in entries: if entry.is_dir(follow_symlinksFalse): # 不跟随符号链接 subdirs.append(entry.name) # 将子目录的完整路径加入待探索队列 dirs_to_explore.append(entry.path) else: files.append(entry.name) except PermissionError: # 处理无权限访问的目录 print(f权限不足跳过目录: {current_dir}) continue yield current_dir, subdirs, files # 使用方式与 os.walk 完全相同 for root, dirs, files in walk_using_scandir(/start/path): for file in files: print(os.path.join(root, file))这个自定义的walk函数给了你更多的灵活性。例如你可以轻松地将队列deque换成栈list使用pop()来实现深度优先遍历或者在发现特定目录时选择不将其加入待探索列表实现过滤逻辑比修改os.walk的dirs列表更直观一些。注意事项os.scandir()返回的DirEntry对象只在迭代过程中和with语句块内是有效的、包含完整信息的。如果你需要将DirEntry对象存储起来后续使用比如放入一个列表那么必须调用entry.stat()将信息缓存下来或者存储entry.path和entry.name。否则在迭代器关闭后再调用entry.is_file()可能会失败或返回不准确的信息。6. 三种方法的核心差异与选型指南现在我们已经深入了解了三位选手是时候做一个全面的对比并给出清晰的选型建议了。特性维度os.listdir(path)os.walk(top)os.scandir(path)核心功能列出单层目录内容名称列表递归遍历整个目录树生成器列出单层目录内容带元数据的迭代器返回类型字符串列表List[str]生成器产出(root, dirs, files)三元组DirEntry迭代器信息丰富度仅名称名称分层级名称、路径并可快速获取类型、属性性能特点基础但获取类型需额外stat调用方便但底层基于listdir递归时可能较慢高性能在支持的系统上类型判断无额外开销内存使用一次性加载所有名称到列表惰性生成内存友好惰性迭代内存友好易用性极简适合快速脚本极高递归遍历的“一站式”解决方案中等需自行处理迭代和路径拼接适用场景1. 只需单层列表2. 简单、一次性任务3. Python 2/3 老代码兼容1. 需要递归遍历整个目录树2. 标准、通用的文件收集/处理任务3. 需要“自上而下”遍历并动态过滤子目录1.对遍历性能有要求2. 需要文件类型等元信息进行快速过滤3. 实现自定义的、复杂的遍历逻辑选型决策流问题你需要遍历子文件夹吗否- 进入步骤2。是- 直接选择os.walk()。除非你有极特殊的性能要求或遍历逻辑否则它是省心省力的最佳选择。问题你对遍历速度有苛刻要求或者需要立刻知道条目是文件还是目录吗否- 选择os.listdir()。它最简单代码最易读对于小型目录或简单任务完全够用。是- 选择os.scandir()。在需要快速过滤例如“只要文件不要目录”或扫描超大型目录时它能带来显著的性能提升。一个综合性的例子假设我们要高效地找到一个大型项目目录中所有最近7天内修改过的.py文件。import os import time def find_recent_py_files(start_path, days7): recent_files [] cutoff_time time.time() - (days * 24 * 60 * 60) for root, dirs, files in os.walk(start_path): # 使用 scandir 替代 listdir 来获取更好的性能Python 3.5 # 注意这里演示的是思路实际os.walk内部用的是listdir。 # 更彻底的性能优化需要自己用scandir实现walk如第5.3节所示。 for file in files: if file.endswith(.py): full_path os.path.join(root, file) try: mod_time os.path.getmtime(full_path) if mod_time cutoff_time: recent_files.append(full_path) except OSError: continue # 跳过无法访问的文件 return recent_files # 但更优的做法是如果完全追求性能可以基于scandir自实现walk def find_recent_py_files_fast(start_path, days7): recent_files [] cutoff_time time.time() - (days * 24 * 60 * 60) dirs_to_scan [start_path] while dirs_to_scan: current_dir dirs_to_scan.pop() try: with os.scandir(current_dir) as entries: for entry in entries: if entry.is_dir(follow_symlinksFalse): dirs_to_scan.append(entry.path) elif entry.is_file() and entry.name.endswith(.py): # 使用 entry.stat() 缓存的信息避免二次系统调用 if entry.stat().st_mtime cutoff_time: recent_files.append(entry.path) except PermissionError: pass return recent_files第二个函数find_recent_py_files_fast就是一个集成了scandir高性能优势的自定义递归遍历它在处理海量文件时会更有效率。7. 常见陷阱、疑难杂症与解决方案在实际使用中即使知道了方法也会踩到各种各样的坑。这里我总结了一些高频问题。7.1 路径拼接绝对不要用字符串加法这是一个新手甚至老手都容易犯的错误。# 错误示范 folder /home/user for name in os.listdir(folder): full_path folder / name # 在Windows上会出错 # 或者 full_path folder \\ name # 同样糟糕且不跨平台 # 正确做法永远使用 os.path.join for name in os.listdir(folder): full_path os.path.join(folder, name) # 自动处理不同操作系统的路径分隔符从Python 3.4开始更推荐使用面向对象的pathlib模块来操作路径它的可读性和安全性更高from pathlib import Path folder Path(/home/user) for item in folder.iterdir(): # iterdir() 类似于 os.scandir() full_path folder / item.name # 使用 / 运算符拼接路径 print(full_path)7.2 处理隐藏文件和特殊条目os.listdir()和os.scandir()默认会返回所有条目包括以点.开头的隐藏文件在Unix-like系统中以及.当前目录和..上级目录这两个特殊条目。os.walk()则比较智能自动过滤掉了.和..。如果你需要过滤掉隐藏文件可以这样做import os visible_items [item for item in os.listdir(.) if not item.startswith(.)]对于scandir你可以在迭代时判断with os.scandir(.) as entries: for entry in entries: if not entry.name.startswith(.): # 处理非隐藏项 pass7.3 权限错误与异常处理遍历文件系统时你肯定会遇到PermissionError权限不足和FileNotFoundError路径不存在。一个健壮的脚本必须处理它们。对于os.walk可以使用onerror回调import os import sys def on_walk_error(error): print(f遍历时发生错误: {error}, filesys.stderr) for root, dirs, files in os.walk(/some/path, onerroron_walk_error): # ... 你的处理逻辑对于os.listdir和os.scandir使用try...except包裹import os try: entries os.listdir(/root) # 普通用户通常无权限 except PermissionError as e: print(f无法访问目录: {e}) entries [] # 赋一个空列表避免后续代码崩溃 # 或者更精细地在scandir循环内处理 try: with os.scandir(/some/path) as it: for entry in it: try: if entry.is_file(): # 处理文件 pass except PermissionError: print(f无法访问条目: {entry.path}) continue except FileNotFoundError: print(指定的路径不存在)7.4 符号链接软链接的坑符号链接像一个快捷方式指向另一个文件或目录。遍历时如何处理它们需要小心。os.walk()默认不跟随符号链接followlinksFalse。如果遇到一个指向目录的符号链接它会将其视为一个普通的目录条目在files列表里不它会被放在dirs列表里吗。实际上os.walk会将其作为一个目录来处理但不会进入它进行遍历除非你设置followlinksTrue。设置True时要非常小心因为如果链接成环会导致无限递归。os.scandir()的entry.is_dir()和entry.is_file()方法默认也不跟随符号链接follow_symlinksFalse。它们判断的是链接本身的性质永远为file不链接本身是一种特殊文件但is_dir()会判断它指向的目标是否是目录。如果你想判断链接指向的目标需要传递follow_symlinksTrue参数。os.listdir()对此毫无感知只返回链接名本身。建议除非你明确知道自己在做什么并且能确保不会形成循环链接否则在遍历时保持followlinksFalse默认值是最安全的选择。7.5 编码问题那些令人头疼的“乱码”文件名在Windows上如果文件名包含非ASCII字符比如中文可能会遇到编码问题。os.listdir()返回的文件名是系统默认编码如Windows的gbk的字节串解码后的字符串但如果你的Python脚本使用utf-8编码或者文件名编码异常就可能出错。解决方案确保脚本文件编码为UTF-8在Python文件开头添加# -*- coding: utf-8 -*-。使用sys.getfilesystemencoding()这个函数返回系统用于文件名的编码。在遍历时如果遇到解码错误可以尝试用这个编码去处理。终极武器os.scandir()和pathlib对Unicode路径的支持更好。pathlib是处理路径的现代方式能最大程度避免编码问题。from pathlib import Path # pathlib 能很好地处理各种奇怪的文件名 path Path(./一个包含中文的目录) for item in path.iterdir(): print(item.name) # 通常能正确打印中文8. 进阶话题pathlib —— 面向对象的优雅之选虽然题目聚焦于os模块的三个函数但任何关于Python文件路径操作的讨论如果不提pathlib都是不完整的。它在Python 3.4中引入提供了一种面向对象的方式来处理文件系统路径极大地提升了代码的可读性和安全性。pathlib的Path对象有一个iterdir()方法其行为和os.scandir()非常相似返回一个生成器产出的是Path对象。from pathlib import Path base_path Path(/home/user) # 遍历单层目录 (类似 os.scandir) for item in base_path.iterdir(): if item.is_file(): print(f文件: {item.name}, 大小: {item.stat().st_size}) elif item.is_dir(): print(f目录: {item.name}) # 递归遍历所有文件 (类似 os.walk但更简洁) for py_file in base_path.rglob(*.py): # rglob 递归通配 print(py_file) # 递归遍历所有内容 (自己控制) for item in base_path.glob(**/*): # **/* 匹配所有子目录下的所有项目 print(item)pathlib的优势可读性path / subdir / file.txt比os.path.join(path, subdir, file.txt)更直观。方法链Path(file.txt).read_text().upper()一气呵成。安全更好地处理不同操作系统的路径差异和特殊字符。何时选择pathlib如果你的项目使用的是Python 3.4现在这几乎是标配强烈建议在新代码中使用pathlib。对于简单的路径拼接、文件检查、读写操作pathlib是首选。对于复杂的、需要精细控制遍历过程比如基于文件属性的复杂过滤的场景os.scandir()配合os模块的其他函数可能更底层、更直接。但pathlib的iterdir()在大多数情况下已经足够好并且代码更优雅。遍历目录是Python编程中的基础操作os.listdir、os.walk和os.scandir构成了从简单到高效的工具链。理解它们之间的细微差别能让你在面对不同任务时迅速找到最趁手的工具。记住核心原则单层快速查看用listdir递归遍历用walk追求性能或需元信息时用scandir。而在现代Python项目中将pathlib作为你的默认选择会让你的文件操作代码更加清晰和健壮。在实际编码中多考虑异常处理和边界情况你的脚本就能从容应对真实世界复杂多样的文件系统环境。