Python 标准库 Day 1 回顾:从遍历出发
1. 遍历的两个维度
写遍历代码前先在脑子里确定两件事:
深度
│
一层 ─────┼───── 递归
│
┌───────────────┼───────────────┐
│ │
只看文件 只看目录
│ │
└───────────┬───────────────────┘
│
看全部
(默认)
筛选条件
(扩展名、大小、时间、模式)
每次写遍历,先回答两个问题:
- 要不要递归?(一层 vs 整棵树)
- 要什么类型?(文件/目录/全部)
剩下的就是筛选条件(扩展名、大小、时间……)。
2. 五种遍历场景的标准写法
场景 A:遍历一层目录(不递归)
最基础的需求:" 列出 data/ 里的所有东西 "。
from pathlib import Path
for entry in Path("data").iterdir():
print(entry)
iterdir() 返回完整 Path 对象,不像 os.listdir 只给名字。
老代码风格(你会看到):
import os
for name in os.listdir("data"):
full = os.path.join("data", name) # 必须自己拼路径
print(full)
场景 B:递归遍历整棵目录树
最常见的需求:" 找出项目里所有的某类文件 "。
for entry in Path("project").rglob("*"):
print(entry)
for entry in Path("project").glob("**/*"):
print(entry)
注意:rglob("*") 返回文件 + 目录,要单一类型还得过滤(见下面 C/D)。
老代码风格(你会经常看到):
import os
for dirpath, dirnames, filenames in os.walk("project"):
for name in filenames:
full = os.path.join(dirpath, name)
print(full)
os.walk 老但强大,下面会讲它独有的能力。
场景 C:只遍历文件
for f in Path("data").iterdir():
if f.is_file():
print(f, f.stat().st_size)
for f in Path("project").rglob("*"):
if f.is_file():
print(f)
带筛选(最常见的真实需求):
for f in Path("project").rglob("*.py"):
# rglob 已经按模式筛了,但还要确保是文件(防止有同名目录)
if f.is_file():
print(f)
extensions = {".jpg", ".png", ".gif"}
for f in Path("photos").rglob("*"):
if f.is_file() and f.suffix.lower() in extensions:
print(f)
ONE_MB = 1024 * 1024
for f in Path("data").rglob("*"):
if f.is_file() and f.stat().st_size > 100 * ONE_MB:
print(f, "is big")
import time
recent = time.time() - 7 * 24 * 3600
for f in Path("logs").rglob("*"):
if f.is_file() and f.stat().st_mtime > recent:
print(f)
场景 D:只遍历目录
for d in Path("project").iterdir():
if d.is_dir():
print(d)
for d in Path("project").rglob("*"):
if d.is_dir():
print(d)
典型用途:
for d in Path("project").rglob("__pycache__"):
if d.is_dir():
print(d)
for d in Path("project").rglob("*"):
if d.is_dir() and not any(d.iterdir()):
print(f"空目录: {d}")
场景 E:遍历时一次拿到所有信息(性能优化)
如果你要对每个条目做 .is_file() + .stat().st_size + .name 这种多次查询,每次都是一次系统调用,遍历几万个文件会非常慢。
这时候用 os.scandir:
import os
with os.scandir("data") as it:
for entry in it:
# 一次拿到所有信息,不需要再调 stat
print(entry.name) # 文件名
print(entry.path) # 完整路径
print(entry.is_file()) # 类型(缓存的)
print(entry.is_dir())
print(entry.stat().st_size) # 第一次调 stat 后会缓存
为什么 scandir 快? listdir 只返回名字,要拿类型和元信息还得再调 os.stat——每个文件一次系统调用。scandir 一次系统调用就把文件类型和部分元信息带回来了,缓存在 DirEntry 对象里,后续访问不再发系统调用。
性能差异:
for f in Path("big_dir").iterdir():
if f.is_file() and f.stat().st_size > 1000:
...
with os.scandir("big_dir") as it:
for entry in it:
if entry.is_file() and entry.stat().st_size > 1000:
...
Maybe Useful:pathlib.Path.iterdir() 的设计哲学是 " 返回 Path 对象 ",每个 Path 不缓存元信息——美观但损失性能。os.scandir 设计哲学是 " 性能优先 ",返回缓存了元信息的 DirEntry。遍历几千个以下用 iterdir 没差,几万以上的目录考虑 scandir。
3. 筛选模式速查
实际业务里 " 遍历 " 基本都伴随 " 筛选 "。下面是高频筛选条件:
按扩展名
Path(".").rglob("*.py")
for f in Path(".").rglob("*"):
if f.suffix.lower() in {".jpg", ".png", ".gif"}:
...
for f in Path(".").rglob("*"):
if f.is_file() and f.suffix not in {".pyc", ".log"}:
...
按文件名模式
Path("tests").rglob("test_*.py")
for f in Path(".").iterdir():
if not f.name.startswith((".", "_")):
...
按大小
ONE_MB = 1024 * 1024
[f for f in Path(".").rglob("*") if f.is_file() and f.stat().st_size > 100 * ONE_MB]
[f for f in Path(".").rglob("*") if f.is_file() and f.stat().st_size == 0]
按时间
import time
from datetime import datetime, timedelta
now = time.time()
[f for f in Path(".").rglob("*") if f.is_file() and (now - f.stat().st_mtime) < 86400]
[f for f in Path(".").rglob("*") if f.is_file() and (now - f.stat().st_mtime) > 30 * 86400]
排除某些目录(剪枝)
pathlib.rglob 没有内置的 " 剪枝 " 机制,要排除目录有两种办法:
办法 1:遍历后过滤(简单但浪费)
for f in Path(".").rglob("*.py"):
if any(part in {".git", "__pycache__", ".venv"} for part in f.parts):
continue
print(f)
办法 2:用 os.walk 剪枝
EXCLUDE = {".git", "__pycache__", ".venv"}
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in EXCLUDE]
for name in filenames:
if name.endswith(".py"):
process(os.path.join(dirpath, name))
dirnames[:] = ... 是原地修改(不是新赋值),os.walk 看到修改后就不会进入被剔除的目录。这是 os.walk 比 rglob唯一明确的优势——剪枝。
4. 三种遍历方式终极对比
Path.iterdir() | Path.rglob() | os.scandir() | os.walk() | |
|---|---|---|---|---|
| 递归 | 不支持 | 支持 | 不支持 | 支持 |
| 返回 | Path 对象 | Path 对象 | DirEntry(缓存元信息) | (dir, dirs, files)三元组 |
| 元信息性能 | 慢 | 慢 | 快 | 快(已经在元组里) |
| 模式匹配 | 不支持 | 支持(glob) | 不支持 | 不支持(自己 endswith) |
| 剪枝 | 不支持 | 不支持 | 不支持 | 支持(修改 dirnames) |
| 易读性 | 高 | 高 | 中 | 低 |
| 推荐度 | 一层默认选它 | 递归默认选它 | 大目录性能选它 | 需要剪枝时选它 |
5. 决策树:遇到遍历需求怎么选
要遍历目录
│
├─ 只一层?
│ └─ 用 Path.iterdir()
│ └─ 文件特别多 (>10000)?→ 改用 os.scandir()
│
└─ 要递归
│
├─ 需要排除某些目录(如 .git, node_modules)?
│ └─ 用 os.walk(),原地修改 dirnames 剪枝
│
├─ 需要按扩展名/模式筛选?
│ └─ 用 Path.rglob("*.xxx") (最简洁)
│
└─ 普通递归
└─ 用 Path.rglob("*")
└─ 文件特别多?性能敏感?→ 改用 os.walk()
6. 经典遍历代码模板
把下面 5 个模板背下来,应付 90% 的批量处理需求:
模板 1:递归找特定扩展名的文件
from pathlib import Path
for f in Path(root).rglob("*.py"):
if f.is_file():
process(f)
模板 2:递归遍历但排除某些目录
import os
EXCLUDE_DIRS = {".git", "__pycache__", ".venv", "node_modules"}
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
for name in filenames:
if name.endswith(".py"):
full = os.path.join(dirpath, name)
process(full)
模板 3:高性能扫描大目录
import os
with os.scandir(root) as it:
for entry in it:
if entry.is_file() and entry.name.endswith(".log"):
process(entry.path, entry.stat().st_size)
模板 4:递归收集所有文件信息(先收集后处理)
from pathlib import Path
files = []
for f in Path(root).rglob("*"):
if f.is_file():
files.append({
"path": f,
"size": f.stat().st_size,
"mtime": f.stat().st_mtime,
})
files.sort(key=lambda x: x["size"], reverse=True)
print(f"共 {len(files)} 个文件,最大: {files[0]}")
模板 5:递归算目录总大小
from pathlib import Path
total = sum(f.stat().st_size for f in Path(root).rglob("*") if f.is_file())
print(f"总大小: {total / 1024**2:.2f} MB")
7. 三个高频陷阱
陷阱 1:rglob("*") 包含目录
for f in Path(".").rglob("*"):
print(f.stat().st_size) # 目录的 size 不是它内容大小!
for f in Path(".").rglob("*"):
if f.is_file():
print(f.stat().st_size)
陷阱 2:边遍历边修改
for f in Path(".").rglob("*.tmp"):
f.unlink() # 在某些文件系统上行为未定义
正确做法:先收集再操作。
to_delete = list(Path(".").rglob("*.tmp"))
for f in to_delete:
f.unlink()
我在前几节强调过的 " 先收集再执行 " 模式,根本原因之一就在这里——除了支持 dry run,还能避免遍历器和文件系统状态冲突。
陷阱 3:忘记 is_file() 检查导致权限错误
for f in Path(".").rglob("*.py"):
content = f.read_text() # 如果 f 是目录,这里抛 IsADirectoryError
养成习惯:操作前先判断类型。rglob 的模式只看名字,不区分文件目录。
遍历需求速查(补充)
| 需求 | 选择 |
|---|---|
| 列一层 | Path.iterdir() |
| 递归列全部 | Path.rglob("*") |
| 只列文件 | 加 if .is_file()过滤 |
| 只列目录 | 加 if .is_dir()过滤 |
| 按扩展名递归找 | Path.rglob("*.ext") |
| 排除某些目录 | os.walk+ 修改 dirnames |
| 高性能扫描 | os.scandir |
| 拿大小/时间 | entry.stat().st_size/ .st_mtime |