首页 / 资讯中心 / 文章详情

Python递归遍历文件夹:从os.walk到自定义实现

Python递归遍历文件夹:从os.walk到自定义实现 ★ FEATURED ARTICLE
开头直接切入用一个实际的开发痛点引出主题。我在处理一批项目文件时经常需要把某个目录下所有层级的文件夹和文件都找出来做批量改名、统计大小、或者整理归档。一开始我图省事用现成的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, dirsNone, filesNone): 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, depth0, resultNone): 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, dirsNone, filesNone): 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, dirsNone, filesNone): 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, dirsNone, filesNone, suffix_filterNone, exclude_dirsNone, depth0): 带过滤功能的递归遍历 参数: 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_linesNone, max_depth3): 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_depth2) 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, encodingutf-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里打开做分类筛选。比起用文件管理器一层层点开看这个方法把所有路径拍平到一行行文本里处理效率完全不是一个量级。用encodingutf-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(), keylambda x: x[1], reverseTrue)[: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, days7): 找出指定天数内修改过的文件 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, days3) 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, dirsNone, filesNone): 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(keylambda 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-8Windows老系统可能用GBK。如果直接打印或者写入文件时不注意编码轻则乱码重则UnicodeEncodeError崩溃。写文件统一用encodingutf-8with open(file_list.txt, w, encodingutf-8) as f: f.write(\n.join(all_files))如果程序在Windows控制台输出中文文件名时报UnicodeEncodeError可以在代码开头强制控制台输出编码import sys import io sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8, errorsreplace)errorsreplace让无法编码的字符用占位符替代而不是直接崩掉。不过这种强制改控制台编码的办法是在特殊环境下才需要平时在代码里尽量用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(raccess_\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生态大同一件事往往能有三四种写法互相碰撞总能发现更优的方案。
阅读完成 · 觉得有帮助?
咨询建站