开头直接切入,用一个实际的开发痛点引出主题。我在处理一批项目文件时,经常需要把某个目录下所有层级的文件夹和文件都找出来,做批量改名、统计大小、或者整理归档。一开始我图省事,用现成的os.walk,但后来发现有些场景下我需要更精细的控制,比如跳过某些目录、自定义遍历顺序、或者按特定规则裁剪结果。这时候自己动手用Python递归写一个遍历函数,反而是最顺手的方式。这篇东西就把我实际用过的思路和踩过的坑整理出来,给准备自己实现指定文件夹遍历的朋友做个参考。
1. 递归遍历的核心思路与方案选择
1.1 递归的本质:把大问题拆成同类型的小问题
递归这个词听起来玄乎,其实概念很简单:一个函数在处理任务时,如果遇到子任务和自身任务是同一种类型,就调用自己来处理。放到文件夹遍历这个场景里就是:我要处理某个目录,但这个目录下面还有子目录,子目录里面又有子目录,每一层的处理逻辑完全一样,那就让函数处理完当前目录后,遇到子目录就再调用一次自己。
用现实生活类比,就像你整理一个套娃式的收纳箱:打开外面的箱子,把里面的东西归类;如果里面还有一个箱子,那就打开它再归类;一直到没有箱子可开为止。递归遍历文件夹就是这样一层层往下走,直到碰到不含任何子目录的“叶子目录”。
这种思路在代码上非常直观,比起循环嵌套,它对层级数量没有限制(理论上),不管目录嵌套了5层还是50层,写出来的代码都是同一套逻辑,不会因为层数增加而需要修改代码结构。
1.2 指定文件夹遍历的两种主流实现方式
Python标准库中实现文件夹遍历其实有现成方案,最常用的两个:
| 方式 | 核心函数 | 特点 |
|---|---|---|
| 现成遍历 | os.walk | 递归自动进行,直接按元组返回当前目录、子目录列表、文件列表 |
| 手工递归 | os.listdir+ 递归调用 | 完全自己控制遍历逻辑,灵活度高 |
os.walk本质上是Python官方帮你封装好的递归遍历,内部已经处理好了路径拼接、目录切换这些细节,拿过来就能用,非常适合大多数日常场景。但它的缺点是“打包”得比较死:你只能按它规定的顺序从上到下遍历,想在遍历过程中跳过某个目录、或者按文件类型动态决定是否进入某个子目录,就需要额外写过滤逻辑。
手工递归则是自己掌控全部细节。虽然代码量多一些,但你能在每一层做任何想做的事:记录路径深度、按条件剪枝、拼接自定义数据结构等等。我之所以写递归版本,主要是需要一个能统计每个文件相对于根目录的路径层级的功能,os.walk返回的数据结构还得再加工,不如直接在递归函数里面顺手算掉。
1.3 为什么推荐新手也动手写一遍递归
就算你平时都用os.walk,我也建议你找时间手写一遍递归遍历。原因很简单:文件系统本质上是树形结构,递归是处理树形结构最自然的思维方式。你以后写JSON解析、菜单权限树、评论楼层回复这些功能,全都离不开递归。拿文件夹遍历练手是成本最低的入门方式——目录结构你肉眼可见,递归到底走没走对,看一眼输出的路径列表就一目了然,调试起来非常直观。
如果不理解递归的底层过程,遇到问题的时候会非常痛苦。比如你写了递归函数但是忘记加结束条件,函数就会无限调用自己直到报错;或者你拼路径时少拼了一层,输出的文件路径怎么都不对。这些坑靠猜是猜不出来的,只有真正理解每一层调用在干什么,才能快速定位问题。
2. 动手实现:递归遍历的代码骨架
2.1 从最简单的版本开始
用Python写一个最基础的递归遍历函数,核心逻辑只有几行:
import os def scan_folder(path): items = os.listdir(path) for item in items: full_path = os.path.join(path, item) if os.path.isdir(full_path): print("目录:", full_path) scan_folder(full_path) # 遇到子目录,递归调用 else: print("文件:", full_path)这段代码里面有三个关键操作:
os.listdir(path):获取当前目录下的所有条目名称,注意它返回的只是名字,不包含完整路径。os.path.join(path, item):把目录路径和条目名字拼成完整路径。这一步必须用join而不是手动拼字符串,这样在Windows和Linux上都能得到正确的路径分隔符。os.path.isdir(full_path):判断这个条目是不是目录。是目录就递归往下走,是文件就做处理。
这个版本的逻辑没错,但它把所有结果直接打印出来,实际使用中我们肯定希望把结果收集起来返回,而不是打印完就没了。
2.2 收集结果的改进版
把打印改成收集到列表里,方便后续使用:
import os def scan_folder(path, dirs=None, files=None): if dirs is None: dirs = [] if files is None: files = [] items = os.listdir(path) for item in items: full_path = os.path.join(path, item) if os.path.isdir(full_path): dirs.append(full_path) scan_folder(full_path, dirs, files) # 同一个列表传下去 else: files.append(full_path) return dirs, files all_dirs, all_files = scan_folder("./my_project") print("文件夹数量:", len(all_dirs)) print("文件数量:", len(all_files))这里有个Python新手特别容易踩的坑:函数的默认参数不能用可变对象。如果你写了def scan_folder(path, dirs=[], files=[]),那么多次调用函数时,dirs和files会保留上一次调用的结果,导致数据越积越多。这是因为Python的默认参数在函数定义时只创建一次,之后每次调用如果没传这个参数,用的都是同一个列表对象。正确做法是默认参数设为None,函数内部再创建新列表。
2.3 增加层级深度统计
很多时候我们不光要知道有哪些文件,还想知道文件在第几层目录。这个信息在处理项目文件结构时特别好用,比如判断哪些文件放得太深,或者生成目录树。在递归函数里加一个depth参数就能实现:
import os def scan_folder(path, depth=0, result=None): if result is None: result = [] items = os.listdir(path) for item in items: full_path = os.path.join(path, item) info = { "path": full_path, "name": item, "depth": depth, "type": "dir" if os.path.isdir(full_path) else "file" } result.append(info) if os.path.isdir(full_path): # 进入子目录时,层级深度加1 scan_folder(full_path, depth + 1, result) return result tree = scan_folder("./my_project") for item in tree: prefix = " " * item["depth"] print(f"{prefix}{item['name']} ({item['type']}, 深度{item['depth']})")每次递归调用子目录时把depth加1,这样每一层目录里的文件自动就能知道自己处于第几层,逻辑清晰且不易出错。运行上面代码,输出看起来就像一棵目录树,缩进层级和实际目录层级一一对应,检查结果是否正确一目了然。
3. 进阶细节:让递归遍历更健壮
3.1 捕获权限异常
现实中的文件夹结构没那么理想。有些目录是系统保护的,当前用户没有读取权限;有些是System Volume Information这类特殊目录,普通程序压根进不去。如果直接用os.listdir读这些目录,程序会抛出PermissionError直接崩溃。
处理办法是在递归调用处加异常捕获:
import os def scan_folder(path, dirs=None, files=None): if dirs is None: dirs = [] if files is None: files = [] try: items = os.listdir(path) except PermissionError as e: print(f"没有权限访问: {path}, 错误: {e}") return dirs, files except OSError as e: print(f"访问出错: {path}, 错误: {e}") return dirs, files for item in items: full_path = os.path.join(path, item) try: is_dir = os.path.isdir(full_path) except OSError: continue if is_dir: dirs.append(full_path) scan_folder(full_path, dirs, files) else: files.append(full_path) return dirs, files注意异常捕获要放在listdir外层,而不是放在整个递归函数外面统一处理。如果只在整个函数外面包一层try-except,那只要有一个子目录没权限,整个遍历都会中断;在递归调用处捕获才能做到“跳过这个目录,继续遍历其他分支”。
权限之外,os.path.isdir在某些特殊情况下也可能抛异常,比如路径太长、或者中间某层是坏的符号链接。稳妥起见,对这两个操作都做异常保护。
3.2 符号链接与死循环的防御
这是递归遍历文件夹时最隐蔽的坑:符号链接(Windows上的快捷方式,Linux上的软链接)可以指向任意位置,包括它的某个祖先目录。如果遍历时遇到一个符号链接指向当前目录的上级,那么递归就会陷入死循环,文件夹数量无限增长,直到内存耗尽或者递归深度超限报错。
处理策略很简单:遇到目录时,判断它是不是符号链接,如果是就直接跳过,不进入递归:
import os def scan_folder(path, dirs=None, files=None): if dirs is None: dirs = [] if files is None: files = [] try: items = os.listdir(path) except (PermissionError, OSError): return dirs, files for item in items: full_path = os.path.join(path, item) # 跳过符号链接,防止循环引用导致死循环 if os.path.islink(full_path): continue if os.path.isdir(full_path): dirs.append(full_path) scan_folder(full_path, dirs, files) else: files.append(full_path) return dirs, filesos.path.islink在Windows和Linux上都能正常判断符号链接。如果你确实想把符号链接指向的真实目录也遍历进去,那就需要记录已经访问过的真实路径集合(比如用os.path.realpath拿到真实路径后存入集合),每次进入目录前检查是否访问过。但说实话,绝大多数场景下直接跳过符号链接是最省心的方案,因为符号链接在项目整理时往往会造成重复文件,影响统计结果。
3.3 路径排序与稳定性
不知道你注意到没有,os.listdir返回的条目顺序在文件系统层面是随机的,特别是NTFS和ext4上,返回顺序和创建时间、磁盘碎片情况都有关系。这就导致同一个递归遍历函数每次运行,输出顺序都可能不一样。如果你要把遍历结果做差异对比,这就是一个隐藏的不稳定因素。
解决办法是在调用listdir之后加一个排序:
items = sorted(os.listdir(path))sorted默认按字符串排序,中文环境下按Unicode码点排序。这样所有层级的目录和文件都会按名称有序排列,遍历结果具有确定性,后续做对比、生成清单都很方便。排序在条目数量多的时候会有些微性能开销,但绝大多数文件夹规模下可以忽略不计,换来的是稳定的输出,值得。
4. 完整示例:带过滤条件的遍历器
4.1 按文件后缀、排除目录过滤
实际使用中我们通常不需要全部文件,比如只统计图片文件,或者只要Python源码文件。给递归函数加两个可选参数:一个用于过滤文件名,一个用于排除指定目录。
import os def scan_filtered(path, dirs=None, files=None, suffix_filter=None, exclude_dirs=None, depth=0): """ 带过滤功能的递归遍历 参数: path: 起始目录路径 suffix_filter: 文件后缀列表,如 ['.py', '.md'],None表示全部文件 exclude_dirs: 要排除的目录名集合,如 {'node_modules', '.git'} depth: 当前层级深度(内部递归用) """ if dirs is None: dirs = [] if files is None: files = [] if suffix_filter is None: suffix_filter = [] if exclude_dirs is None: exclude_dirs = set() try: items = sorted(os.listdir(path)) except (PermissionError, OSError): return dirs, files for item in items: full_path = os.path.join(path, item) if os.path.islink(full_path): continue if os.path.isdir(full_path): # 排除指定目录,不进入递归 if item in exclude_dirs: print(f"跳过目录: {full_path}") continue dirs.append(full_path) scan_filtered(full_path, dirs, files, suffix_filter, exclude_dirs, depth + 1) else: # 按后缀过滤文件 if suffix_filter: ext = os.path.splitext(item)[1].lower() if ext not in suffix_filter: continue files.append(full_path) return dirs, files # 使用示例:只找项目里的Python文件,跳过虚拟环境和缓存目录 code_dirs, py_files = scan_filtered( "./my_project", suffix_filter=['.py'], exclude_dirs={'venv', '.git', '__pycache__', 'node_modules'} ) print(f"找到Python文件 {len(py_files)} 个") for f in py_files: print(f)这段代码我在整理一个老项目时用过。项目里有虚拟环境(venv)、Git仓库(.git)和一堆__pycache__缓存目录,直接全量遍历会多出几千个无关文件,加上过滤后立刻就只剩下几百个真正的源代码文件。exclude_dirs用集合而不是列表,判断元素是否存在时时间复杂度是O(1),比列表的O(n)快得多,虽然目录数量不多时差异不明显,但养成用集合的习惯总是好的。
4.2 统计不同扩展名的文件数量
过滤器的另一个常见用法是按扩展名做统计。我拿到一个陌生项目的压缩包时,习惯先扫一眼里面有哪些类型的文件,判断项目的技术栈组成:
import os from collections import Counter def count_extensions(path): """递归统计所有文件的扩展名分布""" _, all_files = scan_filtered(path) ext_counter = Counter() for f in all_files: ext = os.path.splitext(f)[1].lower() or "(无扩展名)" ext_counter[ext] += 1 return ext_counter exts = count_extensions("./some_project") for ext, count in exts.most_common(15): print(f"{ext or '(无扩展名)':12s} {count:6d} 个")运行结果类似:
.py 128 个 .md 42 个 .json 18 个 .txt 12 个 .png 9 个Counter是collections模块里一个很实用的计数器,统计完直接按数量排序输出,一眼就能看出项目的代码构成。如果你拿到一个陌生代码库,这个统计结果还能帮你快速判断它是不是前端项目(大量.js、.ts、.json)、还是数据处理项目(大量.csv、.ipynb),非常方便。
4.3 生成目录树文本
递归遍历还可以用来生成可保存的目录树文本文件,方便在文档中列目录结构:
import os def gen_tree(path, prefix="", tree_lines=None, max_depth=3): if tree_lines is None: tree_lines = [] try: items = sorted(os.listdir(path)) except (PermissionError, OSError): return tree_lines # 过滤掉隐藏文件和符号链接,让目录树更干净 items = [i for i in items if not i.startswith('.') and not os.path.islink(os.path.join(path, i))] dirs = [i for i in items if os.path.isdir(os.path.join(path, i))] files = [i for i in items if i not in dirs] # 按顺序排列目录在前,文件在后 ordered = dirs + files for index, item in enumerate(ordered): is_last = (index == len(ordered) - 1) connector = "└── " if is_last else "├── " tree_lines.append(prefix + connector + item) full_path = os.path.join(path, item) if os.path.isdir(full_path) and max_depth > 0: next_prefix = prefix + (" " if is_last else "│ ") gen_tree(full_path, next_prefix, tree_lines, max_depth - 1) return tree_lines tree_lines = gen_tree("./my_project", max_depth=2) print("\n".join(tree_lines))max_depth参数控制目录树的展示深度,避免整个目录树过长刷屏。prefix参数记录每一行前面的缩进和连接线字符,逐层传递,最终拼出完整的树形结构。这个函数生成的文本可以直接放进项目文档,比截图好看也更容易维护。
5. 实战应用场景与效果展示
5.1 场景一:批量获取文件清单
我在处理一个客户移交的旧项目时,对方只给了一个文件夹,里边夹杂着源码、文档、图片、数据库备份,乱七八糟三百多个文件。我要先弄清楚里面到底有什么,才能做迁移方案。这时直接用递归函数把所有文件路径输出到一个清单文件:
import os def dump_file_list(root_path, output_file): _, all_files = scan_filtered(root_path) with open(output_file, 'w', encoding='utf-8') as f: for file_path in sorted(all_files): f.write(file_path + '\n') print(f"共 {len(all_files)} 个文件,清单已写入 {output_file}") dump_file_list("./handover_files", "file_list.txt")清单生成后,我可以用文本编辑器的查找功能快速定位特定文件,也可以在Excel里打开做分类筛选。比起用文件管理器一层层点开看,这个方法把所有路径拍平到一行行文本里,处理效率完全不是一个量级。用encoding='utf-8'打开输出文件,是为了避免Windows下默认编码导致中文路径乱码,这个细节我踩过几次坑后已经成了肌肉记忆。
5.2 场景二:统计文件夹体积
系统的属性窗口能看当前文件夹大小,但没法直接告诉你最大的子目录是哪几个。用递归把所有文件大小收集起来,再按目录聚合并排序,很快就能找到占用空间最大的目录:
import os def get_dir_sizes(root_path): """递归统计每个目录的总大小""" dir_sizes = {} file_sizes = {} def walk(path): total = 0 try: items = os.listdir(path) except (PermissionError, OSError): return 0 for item in items: full_path = os.path.join(path, item) if os.path.islink(full_path): continue if os.path.isdir(full_path): child_size = walk(full_path) dir_sizes[full_path] = child_size total += child_size else: try: size = os.path.getsize(full_path) except OSError: size = 0 file_sizes[full_path] = size total += size return total root_total = walk(root_path) dir_sizes[root_path] = root_total # 按大小排序,取前10个最大的目录 top_dirs = sorted(dir_sizes.items(), key=lambda x: x[1], reverse=True)[:10] for dir_path, size in top_dirs: size_mb = size / 1024 / 1024 print(f"{size_mb:10.2f} MB {dir_path}") get_dir_sizes("./server_data")这个函数里再定义了一个内部递归函数walk,好处是dir_sizes和file_sizes这两个字典在内部函数里可以直接引用,不需要层层传递参数。每个目录的总大小等于它内部所有文件大小加上所有子目录大小之和,官方叫“自底向上”的递归累加,理解起来也不难。用这个扫描完发现有一个日志目录占了总空间的一半,清理之后磁盘压力立刻小了很多。
5.3 场景三:查找最近修改的文件
递归遍历还能结合文件元数据做筛选,比如找出一周内被修改过的文件:
import os import time def find_recent_files(root_path, days=7): """找出指定天数内修改过的文件""" threshold = time.time() - days * 24 * 3600 recent = [] _, all_files = scan_filtered(root_path) for f in all_files: try: mtime = os.path.getmtime(f) except OSError: continue if mtime >= threshold: recent.append((f, time.strftime('%Y-%m-%d %H:%M', time.localtime(mtime)))) return recent recent_files = find_recent_files("./active_project", days=3) for path, modified_time in recent_files: print(f"{modified_time} {path}")os.path.getmtime返回的是Unix时间戳(从1970年1月1日到现在的秒数),直接比较大小即可判断新旧。time.strftime把它格式化成人能看懂的字符串。这个功能在排查“某个文件为什么在线上表现不对”时特别好用,一眼就能看出最近被改过的文件是哪些,结合代码提交记录就能快速锁定问题版本。
6. 性能优化与踩坑记录
6.1 用os.scandir替代os.listdir提升性能
os.listdir先获取目录下所有条目的名字,然后每次os.path.isdir都要发起一次系统调用去查看文件类型。如果一个目录下有几万个文件,这几万次系统调用加起来就非常耗时。更高效的做法是用os.scandir,它一次调用就能拿到条目名和文件类型,尤其是是否目录的信息:
import os def scan_folder_fast(path, dirs=None, files=None): if dirs is None: dirs = [] if files is None: files = [] try: with os.scandir(path) as it: entries = list(it) except (PermissionError, OSError): return dirs, files # 按名字排序,保证输出稳定 entries.sort(key=lambda e: e.name) for entry in entries: if entry.is_symlink(): continue if entry.is_dir(): dirs.append(entry.path) scan_folder_fast(entry.path, dirs, files) else: files.append(entry.path) return dirs, filesos.scandir返回的DirEntry对象有.is_dir()、.is_file()、.is_symlink()这些方法,大部分情况下不需要额外系统调用就能直接得到结果,比listdir + isdir快不少。我在一个包含几万个缓存文件的目录上对比过,listdir版本耗时约4秒,scandir版本只要1秒出头,差距很明显。官方在Python 3.5之后也推荐用scandir做目录操作。
不过注意一个小坑:os.scandir返回的迭代器在退出with块之后就不能用了,所以要么在with块内完成遍历,要么像我代码里这样先list(it)转成列表再退出。直接遍历迭代器并在循环体内递归调用自身,如果迭代器会被关闭就会出问题,先转成列表最稳妥。
6.2 递归深度限制与迭代替代方案
Python默认的递归深度上限是1000层。虽然一般项目目录不会嵌套到1000层,但极端情况下(比如自动生成的临时目录结构、文件系统出问题导致的重复嵌套)还是有可能触顶。触顶时会抛出RecursionError: maximum recursion depth exceeded,程序直接崩溃。
两个解决方案:
第一,检查系统是否真的需要那么深的遍历。大多数场景下把sys.setrecursionlimit(5000)调高一点就够用,但不要调到几万,否则可能导致解释器栈溢出崩溃,那不是Python能捕获的异常了:
import sys sys.setrecursionlimit(5000)第二,把递归改写成显式栈的循环遍历。用列表模拟栈,循环遍历所有目录,效果一样但不受递归深度限制:
import os def scan_folder_stack(root_path): """使用显式栈遍历,避免递归深度限制""" dirs = [] files = [] stack = [root_path] while stack: current = stack.pop() try: items = sorted(os.listdir(current)) except (PermissionError, OSError): continue for item in items: full_path = os.path.join(current, item) if os.path.islink(full_path): continue if os.path.isdir(full_path): dirs.append(full_path) stack.append(full_path) else: files.append(full_path) return dirs, filesstack.pop()默认从列表尾部取出元素,所以后放入的目录会先被处理,这其实是一种类似深度优先的顺序。如果你想先处理先发现的目录,用collections.deque双端队列配合popleft()就能实现广度优先。显式栈方式在代码上比递归多几行,但对深层嵌套场景更安全,生产环境处理不可控数据时我倾向用这种写法。
6.3 编码问题的避坑
中文文件名在不同系统上有不同的编码方式。Linux默认UTF-8,Windows老系统可能用GBK。如果直接打印或者写入文件时不注意编码,轻则乱码,重则UnicodeEncodeError崩溃。
写文件统一用encoding='utf-8':
with open('file_list.txt', 'w', encoding='utf-8') as f: f.write('\n'.join(all_files))如果程序在Windows控制台输出中文文件名时报UnicodeEncodeError,可以在代码开头强制控制台输出编码:
import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')errors='replace'让无法编码的字符用占位符替代而不是直接崩掉。不过这种强制改控制台编码的办法是在特殊环境下才需要,平时在代码里尽量用UTF-8读写文件就够了。
6.4 路径分隔符的兼容性
把遍历结果里的路径直接拼接成新路径时,Windows的路径分隔符是\,Linux和macOS是/,直接手写字符串拼接很容易出问题。跨平台的推荐做法是:
# 不要用: path + "/" + item # 也不要用: path + "\\" + item # 要用: full_path = os.path.join(path, item)os.path.join会根据当前操作系统自动选用正确的分隔符。如果你的代码要同时跑在Windows和Linux上,这一点尤其重要。另外,Linux和macOS环境下文件名区分大小写,Windows不区分,这也会导致一些莫名其妙的“文件明明在那里却找不到”的问题,主要发生在程序在Windows开发、Linux部署的场景,写遍历逻辑时如果涉及文件名比较,建议统一转成小写再比较。
7. 常见报错与排查实战
7.1PermissionError权限不足
报错场景:遍历系统目录(如C:\Windows、/root)时,部分子目录不允许当前用户访问。
排查思路:先确认是不是所有目录都报错,还是只有个别目录。如果是后者,用异常处理跳过这些目录即可。关键要区分“该跳过”和“不该跳过”——不是所有权限不足都必须终止程序,对于遍历场景,记录日志继续跑往往更有价值。
# 在递归函数内捕获并记录,而不是向上抛出 try: items = os.listdir(path) except PermissionError as e: print(f"[警告] 跳过无权限目录: {path}") return dirs, files7.2RecursionError递归深度超限
报错场景:目录嵌套层数超过Python默认的1000层上限,或者代码里缺少结束条件导致无限递归。
排查思路:先看报错信息中的调用栈末尾,是不是在同一个函数上反复横跳,如果是,基本可以确定递归条件写错了;如果栈很深但确实在往下走,那就用上一节提到的显式栈方案替代递归。
常见的错误写法是递归调用时忘记修改参数:
# 错误:full_path 每次都一样,永远走不到结束条件 def bad_scan(path): for item in os.listdir(path): full_path = os.path.join(path, item) if os.path.isdir(full_path): bad_scan(path) # 这里应该是 full_path!这种错误在实际调试时很隐蔽,因为代码不会立刻报错,而是等到消耗完内存或栈深度才崩溃。排查时加一个打印语句输出每次递归传入的路径,立刻就能看出问题。
7.3FileNotFoundError文件不存在
报错场景:遍历过程中某个文件被其他程序删除或移动,导致用os.path.getsize或os.path.isdir操作时报错。
排查思路:文件系统是动态的,遍历过程中其他进程可能在删除文件,这不是程序逻辑错误。在单一文件操作处加防御性判断即可:
if os.path.exists(full_path): # 执行操作 pass else: # 文件已不存在,跳过 continue# 反模式:不要用这种方式检查状态后再读取,存在时间窗口 import os if os.path.exists(file_path): with open(file_path) as f: # 这里可能已被删除 pass正确做法是直接用try-except包裹读取操作,让异常处理来处理这种竞态条件,而不是先检查再加操作,因为检查和操作之间并非原子性的。
7.4 符号链接死循环的判断方法
报错场景:遍历过程中发现文件夹数量异常得多,或者程序最终因为递归深度限制崩溃。
排查思路:优先检查目录树中是否存在指向祖先目录的符号链接。手工查找效率低,直接写个快速检测:
import os def find_symlink_loops(root_path): """找出可能形成循环引用的符号链接""" loops = [] for dirpath, dirnames, _ in os.walk(root_path): for name in dirnames: full_path = os.path.join(dirpath, name) if os.path.islink(full_path): real = os.path.realpath(full_path) # 如果符号链接指向它自己的祖先目录,必定造成循环 if real.startswith(os.path.realpath(dirpath)): loops.append((full_path, real)) return loops for link, target in find_symlink_loops("./my_folder"): print(f"疑似循环链接: {link} -> {target}")os.walk默认不会主动跟踪符号链接目录,但你把符号链接当成普通目录进行递归时就要特别小心。用上面这个函数扫一遍,找到问题链接后删除或排除它,遍历就恢复正常了。
8. 扩展思路与我的使用建议
8.1 配合其他方法做二次过滤
递归遍历得到所有文件列表后,后续处理就完全自由了。比如结合正则表达式过滤符合规则的文件名:
import os import re _, all_files = scan_folder("./data") # 匹配所有 .log 文件,且文件名中包含日期(如 access_2024-01-15.log) pattern = re.compile(r'access_\d{4}-\d{2}-\d{2}\.log$') log_files = [f for f in all_files if pattern.search(f)]把“遍历”和“筛选”分两步做的好处是逻辑清晰,遍历函数保持通用性,业务筛选逻辑独立在外面,后续调整筛选条件时不需要改动遍历代码。这个思路在写脚本时非常重要——一个函数只做一件事,组合起来才能灵活应对需求变化。
8.2 按需返回而不要贪多
有些时候你只需要目录列表或者只要文件列表,但写成一个函数总是两个都返回。我习惯让函数返回完整信息,由调用方自行选择要使用哪部分,因为实际使用中经常出现“开始时只要目录,后来又要文件”的需求变化,返回完整数据避免了反复改函数签名。
如果担心返回两个大列表占用内存,可以考虑改成生成器(yield),但生成器在递归场景下写起来更绕,可以先不用考虑。几千上万个路径对的列表在内存中也就几MB,远不是瓶颈。
8.3 实际项目中的组合经验
我处理一个需要清理的老项目时,用过组合方案:先scan_filtered拿到所有文件列表,用count_extensions了解文件构成,再用get_dir_sizes找到最占空间的目录,最后结合文件修改时间筛出几年没动过的文件。四个功能组合在一块,一次性解决了清理方案需要的信息。
递归遍历文件夹这个能力本身不难,但它是很多高级操作的底座。文件清单生成、磁盘空间分析、项目结构展示、缓存清理、日志归集、备份策略,全都扎根在这一个基本功上。把这一块吃透,后面写脚本就会顺手很多。
8.4 最后两个小建议
第一个建议:写递归函数时,在函数开头固定加一行注释说明递归的终止条件是什么。递归比普通循环难读,主要原因就是“什么时候停下来”对阅读者不直观。注释写明“遇到无权限目录时返回”“遇到符号链接时跳过”,三个月后回来看代码就不用重新推导一遍。
第二个建议:先把参数最少的简单版本跑通,再逐步加过滤条件、加异常处理、加统计逻辑。我见过不少朋友一上来就想写一个万能遍历函数,结果要调试的东西太多,全是并发错误,根本定位不清。递归遍历这种基础功,最好一步一个脚印,每次加一个功能,跑一次验证,确认无误再继续,效率反而更高。
希望这份经验对你有用。如果你在实际使用中遇到了其它解法,哪怕是完全不一样的思路,也欢迎交流——毕竟Python生态大,同一件事往往能有三四种写法,互相碰撞总能发现更优的方案。