文章
合集Python 标准库第 2 / 8 篇

Python 标准库 Day 1 回顾:从遍历出发

1. 遍历的两个维度

写遍历代码前先在脑子里确定两件事:

深度
                   │
        一层 ─────┼───── 递归
                   │
   ┌───────────────┼───────────────┐
   │                               │
 只看文件                       只看目录
   │                               │
   └───────────┬───────────────────┘
               │
            看全部
              (默认)
              
              筛选条件
              (扩展名、大小、时间、模式)

每次写遍历,先回答两个问题:

  1. 要不要递归?(一层 vs 整棵树)
  2. 要什么类型?(文件/目录/全部)

剩下的就是筛选条件(扩展名、大小、时间……)。

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