前言
文件操作大家讲得多,目录操作反而常被忽略,结果一到真实项目就卡壳:日志目录不存在、输出目录已存在、临时文件忘了删、递归遍历把隐藏目录也扫了进来。这些场景都属于「目录」这一层的操作。
一个典型误解是「建目录就用os.mkdir」。os.mkdir只能建一层,父目录不存在照样报错;要逐级创建得用os.makedirs。类似地,「删目录」也分三种:os.rmdir只删空目录,os.removedirs自底向上删空链,shutil.rmtree才删整棵树。
本文围绕目录展开:创建与删除、用pathlib做遍历和筛选、临时目录的正确用法,最后给一个「安全清理过期文件」的实战脚本。示例面向 Python 3.8+,用到更高版本特性时会注明。
一、创建、删除与重命名目录
os这一侧的接口:
# 适用于 Python 3.8+
import os
os.mkdir("out") # 建一层;父目录不在就抛 FileNotFoundError
os.makedirs("logs/2026/10", exist_ok=True) # 逐级建;exist_ok=True 已存在不报错
os.rmdir("out") # 只能删空目录
os.removedirs("a/b/c") # 自底向上删空目录,遇到非空就停os.makedirs的签名是os.makedirs(name, mode=0o777, exist_ok=False)。注意exist_ok只对「目标目录已存在」放行,如果目标存在但是文件,照样抛错。mode决定权限位,但会被进程的 umask 修正,所以实际权限可能比0o777更严。
pathlib一侧:
# 适用于 Python 3.8+
from pathlib import Path
p = Path("logs") / "2026" / "10"
p.mkdir(parents=True, exist_ok=True)
q = Path("out")
q.rmdir() # 同样只能删空目录Path.mkdir(parents=True, exist_ok=True)这一个方法就覆盖了os.mkdir和os.makedirs两种需求,这是pathlib比较顺手的地方。
重命名目录用os.rename/os.replace,pathlib里是Path.rename()/Path.replace():
| 操作 | os 写法 | pathlib 写法 |
|---|
| 建一级目录 | os.mkdir(p) | Path(p).mkdir() |
| 建多级目录 | os.makedirs(p, exist_ok=True) | Path(p).mkdir(parents=True, exist_ok=True) |
| 删空目录 | os.rmdir(p) | Path(p).rmdir() |
| 删整树 | shutil.rmtree(p) | shutil.rmtree(p) |
| 改名 | os.rename(src, dst) | Path(src).rename(dst) |
| 覆盖式改名 | os.replace(src, dst) | Path(src).replace(dst) |
二、用 pathlib 遍历与筛选目录
Path.iterdir()相当于os.listdir(),但返回的是Path对象而不是字符串,省掉反复拼接:
# 适用于 Python 3.8+
from pathlib import Path
for entry in Path(".").iterdir():
kind = "目录" if entry.is_dir() else "文件"
print(kind, entry.name)要做模式匹配,用Path.glob()(当前层)和Path.rglob()(递归):
# 适用于 Python 3.8+
from pathlib import Path
# 当前目录下所有 .csv
for p in Path("data").glob("*.csv"):
print(p)
# 递归找所有 .py,等价于 glob("**/*.py")
for p in Path(".").rglob("*.py"):
print(p)glob和rglob返回的都是生成器,不会一次性把所有匹配项算出来。若路径不存在,glob只是产出空结果,不报错——这既是优点也容易掩盖「目录写错了」这种低级问题。
筛选时另一个常见需求是「只看文件、跳过隐藏项」:
# 适用于 Python 3.8+
from pathlib import Path
files = [p for p in Path(".").iterdir()
if p.is_file() and not p.name.startswith(".")]
print(len(files), "个可见文件")注意Path.iterdir()返回的条目顺序取决于文件系统,需要稳定顺序一定要自己sorted()。
三、临时文件与临时目录
tempfile模块负责「用完就删」的临时资源。首选上下文管理器写法,退出时自动清理:
# 适用于 Python 3.8+
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as tmp:
work = Path(tmp) / "stage.txt"
work.write_text("临时内容", encoding="utf-8")
print("工作目录:", tmp)
# 离开 with 后,整个目录连同内容一起被删除需要临时文件并且要让别的程序也能按名字打开时,用NamedTemporaryFile:
# 适用于 Python 3.8+
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt",
encoding="utf-8", delete=True) as fp:
fp.write("hello")
fp.flush() # 让内容真正落盘,别人才读得到
print(fp.name)几个准确的事实:NamedTemporaryFile的签名是NamedTemporaryFile(mode='w+b', buffering=-1, encoding=None, newline=None, suffix=None, prefix=None, dir=None, delete=True, *, errors=None, delete_on_close=True);delete_on_close是Python 3.12才加入的参数,在此之前只有delete;TemporaryDirectory的delete参数同样是 3.12 加入的。写兼容 3.8~3.11 的代码时不要用这两个参数。
底层接口tempfile.mkdtemp(suffix=None, prefix=None, dir=None)直接建一个临时目录并返回它的路径,但不会自动删除,清理责任在使用者身上:
# 适用于 Python 3.8+
import shutil
import tempfile
d = tempfile.mkdtemp(prefix="build_")
try:
print("临时目录:", d)
finally:
shutil.rmtree(d, ignore_errors=True)tempfile.gettempdir()返回系统用来放临时文件的目录;TemporaryDirectory()不传dir时就落在这里。
实战:清理超过 N 天的日志文件
下面脚本演示「目录创建 + 递归筛选 + 文件信息 + 删除」的组合,重点是删除前先确认目标是文件:
# 适用于 Python 3.8+
import time
from pathlib import Path
def clean_logs(root="logs", days=7, dry_run=True):
root = Path(root)
if not root.is_dir():
raise NotADirectoryError(f"{root} 不是目录")
deadline = time.time() - days * 86400
removed = 0
for path in root.rglob("*.log"):
if not path.is_file():
continue
if path.stat().st_mtime < deadline:
print(("将删除 " if dry_run else "删除 ") + str(path))
if not dry_run:
path.unlink()
removed += 1
return removed
if __name__ == "__main__":
print("命中", clean_logs(), "个文件(预演模式)")脚本默认dry_run=True,先打印再决定是否真删,这是处理删除逻辑的稳妥做法。path.unlink()只删文件,遇到目录会抛IsADirectoryError,正好当作一道保险。
常见坑点
1. 用os.mkdir建多级目录
❌os.mkdir("a/b/c"),a不存在时直接抛FileNotFoundError。 ✅os.makedirs("a/b/c", exist_ok=True)或Path("a/b/c").mkdir(parents=True, exist_ok=True)。
2. 以为rmdir能删非空目录
❌os.rmdir("build")里还有文件,报OSError: Directory not empty。 ✅ 明确要整树删除时用shutil.rmtree("build"),并二次确认路径。
3. 重复执行脚本被FileExistsError打断
❌Path("out").mkdir()第二次运行报错。 ✅ 加exist_ok=True。
4. 遍历目录时顺序不确定
❌ 假设iterdir()返回「按名字排好序」的列表,用它做分页或对比。 ✅sorted(Path(".").iterdir()),或sorted(p.name for p in ...)。
5. 临时目录不用上下文管理器
❌tempfile.mkdtemp()之后忘了删,临时目录越堆越多。 ✅mkdtemp用try/finally配shutil.rmtree,或直接用TemporaryDirectory()。
6.glob匹配不到就以为代码错了
❌Path("data").glob("*.csv")因为data不存在而静默返回空,误判成「没有数据」。 ✅ 匹配前先Path("data").is_dir(),路径不对就尽早报错。
7. Windows 上删除仍被占用的文件
❌shutil.rmtree(tmp)报PermissionError,因为里面的文件还开着。 ✅ 先确保文件句柄已关闭;必要时用TemporaryDirectory(ignore_cleanup_errors=True)。
8. 递归遍历撞上符号链接成环
❌rglob在存在循环符号链接的目录上可能反复进出。 ✅ 检查path.is_symlink()并跳过,或改用不跟随链接的遍历方式。
总结
| 场景 | 推荐写法 | 注意 |
|---|
| 建多级目录 | Path(p).mkdir(parents=True, exist_ok=True) | 目标为文件时仍报错 |
| 删空目录 | os.rmdir/Path.rmdir | 非空会失败 |
| 删整树 | shutil.rmtree | 危险,先确认路径 |
| 列目录 | Path.iterdir() | 顺序不保证 |
| 模式匹配 | Path.glob/Path.rglob | 路径不存在时静默为空 |
| 临时目录 | tempfile.TemporaryDirectory() | 自动清理 |
| 临时文件 | tempfile.NamedTemporaryFile() | 需要落盘时先flush() |
目录操作的难点不在 API,而在「已存在」「非空」「被占用」「路径写错」这几种边界。把exist_ok用起来、把删除做成可选、把路径先判断再使用,脚本的健壮性会立刻上一个台阶。