文件夹一大,os.listdir() 这种写法就开始露怯了。
测试目录里放几十个文件,看着没问题;线上一跑,里面有子目录、有软链接、有没权限的目录、有半截上传的临时文件,脚本扫到一半直接挂。更麻烦的是,有些人喜欢先把所有文件塞进 list,再慢慢处理,目录稍微深一点,内存就被自己玩没了。
遍历文件夹这事,别写得太“干净”。真实环境里,它就不是一个干净活。
我一般不会一上来就写:
for name in os.listdir(path):
print(name)
这只能看一层目录。你想找日志、图片、备份文件、过期临时文件,基本都不够用。
更稳一点的写法,是用 pathlib。路径处理舒服,跨平台也少一点奇怪问题。
比如现在有个需求:扫一个目录,把所有 .log 文件找出来,超过 100MB 的单独打印出来,后面好让运维去清。
我会这样写:
from pathlib import Path
ROOT_DIR = Path("/data/app")
LIMIT_SIZE = 100 * 1024 * 1024
defwalk_logs(root: Path):
for item in root.rglob("*.log"):
ifnot item.is_file():
continue
try:
size = item.stat().st_size
except OSError as e:
print(f"[skip] stat failed: {item},err={e}")
continue
if size >= LIMIT_SIZE:
print(f"[large-log] {item} size={size / 1024 / 1024:.2f}MB")
walk_logs(ROOT_DIR)
这段代码没啥花活,但现场够用。
这里我不太喜欢直接 list(root.rglob("*.log"))。看着就一行,挺优雅,但目录特别大时,会先把结果全攒起来。你只是想边扫边处理,没必要把所有路径都塞内存里。
rglob() 返回的是一个可迭代对象,边走边吐文件,这才像干活的脚本。
不过 rglob() 也不是万能的。
有一次扫备份目录,脚本跑着跑着停住了,不报错,也不结束。后来一看,目录里有软链接,绕回了上层目录。虽然很多情况下 pathlib 不会默认钻进目录软链接,但我现在写遍历脚本,还是会把软链接单独拎出来判断。因为你不知道目录是谁建的,也不知道上一个脚本干过什么。
如果要控制得更细,我会用 os.scandir() 自己递归。代码稍微土一点,但可控。
import os
from pathlib import Path
defscan_files(root: Path, suffix: str):
stack = [root]
while stack:
current = stack.pop()
try:
with os.scandir(current) as entries:
for entry in entries:
path = Path(entry.path)
if entry.is_symlink():
print(f"[skip] symlink: {path}")
continue
if entry.is_dir(follow_symlinks=False):
stack.append(path)
continue
if entry.is_file(follow_symlinks=False) and path.name.endswith(suffix):
yield path
except PermissionError:
print(f"[deny] no permission: {current}")
except FileNotFoundError:
print(f"[gone] removed when scanning: {current}")
except OSError as e:
print(f"[error] scan failed: {current},err={e}")
for file_path in scan_files(Path("/data/app"), ".log"):
print(file_path)
这段我更愿意放到生产脚本里。
原因很简单:它遇到没权限的目录不会直接死,遇到扫描过程中被删掉的目录也不会死。线上目录就是这样,你扫的时候,别的进程可能正在清理、轮转、移动文件。脚本要接受这种脏现场。
如果只是写个小工具,遍历后顺便统计一下文件数量和大小,可以再封一层。
from pathlib import Path
defcollect_summary(root: Path, suffix: str):
total_count = 0
total_bytes = 0
for file_path in scan_files(root, suffix):
try:
size = file_path.stat().st_size
except OSError:
continue
total_count += 1
total_bytes += size
return total_count, total_bytes
count, used = collect_summary(Path("/data/app"), ".log")
print(f"log_count={count}, log_size={used / 1024 / 1024:.2f}MB")
这类脚本最容易被忽略的不是“怎么遍历”,而是边界。
路径不存在怎么办?
目录没权限怎么办?
文件扫描到一半被删了怎么办?
软链接要不要跟进去?
文件名大小写要不要兼容?
这些不提前想,脚本在测试环境永远正常,到服务器上就开始抽风。
还有一个小习惯:不要在遍历函数里直接写业务逻辑。
遍历就负责吐文件:
yield path
业务逻辑放外面处理。今天你要删过期日志,明天你要统计图片大小,后天你要找空文件,遍历代码都不用动。
比如清理 7 天前的临时文件:
import time
from pathlib import Path
EXPIRE_SECONDS = 7 * 24 * 3600
now = time.time()
for tmp_file in scan_files(Path("/data/upload_tmp"), ".tmp"):
try:
age = now - tmp_file.stat().st_mtime
if age > EXPIRE_SECONDS:
print(f"[delete] {tmp_file}")
tmp_file.unlink()
except OSError as e:
print(f"[skip] delete failed: {tmp_file},err={e}")
真正跑删除脚本时,我一般还会先只打印不删除。看一遍输出,确认路径没扫错,再打开 unlink()。别嫌麻烦,删错目录这种事,一次就够记很久。
遍历文件夹,用 Python 写不难。
难的是别把它当课堂题写。目录是活的,文件是会变的,权限是不讲道理的。代码里多几个判断,看着不漂亮,但半夜少接一次电话。