pathlib文件路径处理
现代化的文件系统路径操作
一、概述:为什么使用 pathlib
pathlib 是 Python 3.4 引入的标准库模块,提供了一套面向对象的文件系统路径操作接口。自 Python 3.6 起,pathlib 被正式认定为标准库的核心组成部分,在 Python 3.9 及之后版本中,Path 类已经可以作为 open() 等内置函数的直接参数使用。
在 pathlib 出现之前,Python 开发者主要依赖 os.path 模块进行路径操作。os.path 使用字符串来表示路径,所有操作都以函数调用的方式进行,代码可读性较差且容易出错。pathlib 的诞生彻底改变了这一现状——它将路径抽象为对象,所有操作都封装在对象的方法和属性中,代码更加直观、简洁且跨平台。
pathlib 的核心优势
- 面向对象: 路径是对象而非字符串,拥有丰富的属性和方法
- 跨平台: 自动处理 POSIX(Linux/macOS)与 Windows 的路径分隔符差异
- 可组合: 使用 / 运算符即可拼接路径,简洁直观
- 功能完备: 覆盖了文件读写、目录遍历、路径解析、文件信息查询等全场景
- 与标准库兼容: Python 3.6+ 中可与 open()、os 等无缝配合
一个直观的对比
先看一个简单的例子,对比传统 os.path 方式和 pathlib 方式的差异:
# -------- os.path 方式(传统) --------
import os
base_dir = "/home/user/projects"
file_path = os.path.join(base_dir, "data", "config.json")
if os.path.exists(file_path):
with open(file_path, "r") as f:
content = f.read()
name, ext = os.path.splitext(os.path.basename(file_path))
# -------- pathlib 方式(现代)--------
from pathlib import Path
base_dir = Path("/home/user/projects")
file_path = base_dir / "data" / "config.json"
if file_path.exists():
content = file_path.read_text()
name = file_path.stem
ext = file_path.suffix
可以清晰地看到,pathlib 方式代码更短、语义更清晰。/ 运算符直接用于路径拼接,read_text() 一步完成读取,stem 和 suffix 直接作为属性访问,无需调用函数组合。
二、PurePath 与 Path 的区别
pathlib 模块的类体系分为两大层次:纯路径(PurePath)和具体路径(Path)。理解二者的区别是掌握 pathlib 的第一步。
PurePath —— 纯路径对象
PurePath 是路径的抽象表示,只进行字符串级别的路径操作,不涉及任何实际的 I/O 操作。它永远不会访问文件系统,因此即使路径指向一个不存在的文件,也能正常创建和使用。它在底层有两个子类:
- PurePosixPath —— 适用于 POSIX(Linux/macOS)风格的路径(使用
/ 分隔符)
- PureWindowsPath —— 适用于 Windows 风格的路径(使用
\ 分隔符,含盘符)
Path —— 具体路径对象
Path 继承自 PurePath,在纯路径的基础上增加了所有涉及文件系统 I/O 的操作,包括文件读写、目录创建删除、文件信息查询、符号链接操作等。Path 同样有两个子类:
- PosixPath —— Unix/Linux 环境下的具体路径
- WindowsPath —— Windows 环境下的具体路径
PurePath 与 Path 的核心差异
- PurePath 无 I/O: 只能做路径字符串层面的操作,如拼接、拆分、比较等
- Path 全功能: 在 PurePath 的基础上增加了文件系统访问能力
- 实例化规则: 在 Windows 上运行 Path() 得到 WindowsPath,在 Linux 上得到 PosixPath
- 使用建议: 日常开发中绝大多数场景直接用 Path 即可;仅在需要纯字符串层面处理路径且避免 I/O 开销时,才考虑使用 PurePath
# PurePath 主要用于路径字符串操作
from pathlib import PurePath, PurePosixPath, PureWindowsPath
p = PurePosixPath("/usr/local/bin/python3")
print(p.parent) # /usr/local/bin
print(p.name) # python3
print(p.suffix) # 空(无扩展名)
pw = PureWindowsPath("C:\\Users\\Admin\\Documents\\report.txt")
print(pw.drive) # C:
print(pw.parent) # C:\Users\Admin\Documents
# PurePath 对象可以跨平台比较,即使运行在 Windows 上
PurePosixPath("/a/b") == PurePosixPath("/a/b") # True
# Path 拥有 PurePath 的全部能力 + 文件 I/O
from pathlib import Path
p = Path("." )
print(p.exists()) # True(当前目录一定存在)
print(p.is_dir()) # True
print(p.absolute()) # 获取绝对路径,如 /home/user/project
简而言之,PurePath 是路径的"语法层面"抽象,Path 是路径的"语义+语法"抽象。如果你只需要操作路径字符串而不需要访问文件系统(比如在配置解析、URL 路径处理中),PurePath 更加轻量;否则直接使用 Path 即可。
三、路径创建与拼接
3.1 创建 Path 对象
Path 对象的创建方式非常灵活,支持字符串、多个路径片段组合、以及与其他 Path 对象组合:
from pathlib import Path
# 方式一:从字符串创建
p1 = Path("/etc/config/app.ini")
# 方式二:从多个片段创建
p2 = Path("/etc", "config", "app.ini")
# 方式三:从当前目录创建
p3 = Path(".") # 相对路径
p4 = Path.cwd() # 当前工作目录的绝对路径
p5 = Path.home() # 用户家目录
# 方式四:空参数表示当前目录
p6 = Path() # 等价于 Path(".")
3.2 使用 / 运算符拼接路径
这是 pathlib 最令人喜爱的一个特性——重载 / 运算符用于路径拼接,代码书写就像在终端中使用 cd 命令一样自然:
from pathlib import Path
base = Path("/home/user")
# 使用 / 拼接路径(最推荐的方式)
docs = base / "Documents" / "projects"
print(docs) # /home/user/Documents/projects
# / 运算符可以组合 Path 对象和字符串
sub = Path("subdir")
file = base / sub / "file.txt"
print(file) # /home/user/subdir/file.txt
# 连续拼接
deep = base / "a" / "b" / "c" / "d" / "file.md"
print(deep) # /home/user/a/b/c/d/file.md
3.3 使用 joinpath 拼接
joinpath() 方法提供了一种函数式调用的拼接方式,适合参数动态传入的场景:
from pathlib import Path
base = Path("/data")
# joinpath 可以接受多个参数
path = base.joinpath("2025", "05", "logs", "access.log")
print(path) # /data/2025/05/logs/access.log
# 动态拼接——当目录层级不确定时特别有用
parts = ["archive", "images", "photos", "vacation.jpg"]
full_path = Path("/home").joinpath(*parts)
print(full_path) # /home/archive/images/photos/vacation.jpg
3.4 相对路径与绝对路径的拼接规则
当拼接路径时,如果右侧的路径片段以根目录开头(如 /etc),它会重置左侧的基础路径:
from pathlib import Path
base = Path("/home/user/projects")
# 正常拼接
p1 = base / "src"
print(p1) # /home/user/projects/src
# 右侧以 / 开头时,左侧被替换(注意这个行为!)
p2 = base / "/etc"
print(p2) # /etc (/home/user/projects 被丢弃)
# 使用 resolve() 可以获取规范化的绝对路径
relative = Path("../docs")
p3 = base / relative
print(p3.resolve()) # /home/user/docs
注意
当使用 / 运算符拼接路径时,如果右侧字符串以 / 开头(在 POSIX 上)或包含盘符(在 Windows 上),左侧的基础路径将被完全替换。这是 PurePath 的路径语义决定的,在使用动态输入拼接路径时需要特别注意。
四、路径属性
Path 对象提供了丰富的属性来获取路径的不同组成部分,远比 os.path 的字符串拆分操作要方便:
from pathlib import Path
p = Path("/home/user/projects/hello_world.py")
print("name :", p.name) # hello_world.py —— 完整文件名
print("stem :", p.stem) # hello_world —— 文件名(不含后缀)
print("suffix:", p.suffix) # .py —— 文件后缀(含点)
print("parent:", p.parent) # /home/user/projects —— 父目录
# suffixes 处理多后缀文件(如 .tar.gz)
p2 = Path("archive.tar.gz")
print(p2.suffixes) # ['.tar', '.gz'] —— 后缀列表
print(p2.stem) # archive —— 去除所有后缀
# parts 以元组形式返回路径的每一级
print(p.parts)
# ('/', 'home', 'user', 'projects', 'hello_world.py')
# drive 和 root(Windows 下特别有用)
pw = Path("C:\\Users\\Admin\\file.txt")
print(pw.drive) # C:
print(pw.root) # \
print(pw.anchor) # C:\ —— drive + root 的组合
路径属性速查表
| 属性 |
返回 |
示例(对 /home/user/file.txt) |
| name |
完整文件名 |
file.txt |
| stem |
不含后缀的文件名 |
file |
| suffix |
文件扩展名(含点) |
.txt |
| suffixes |
多后缀列表 |
['.tar', '.gz'] |
| parent |
父目录 Path |
/home/user |
| parents |
所有父目录的可迭代对象 |
[Path('/home/user'), Path('/home'), Path('/')] |
| parts |
各级路径元组 |
('/', 'home', 'user', 'file.txt') |
| drive |
盘符(仅Windows) |
C: |
| root |
根目录分隔符 |
/ 或 \ |
| anchor |
路径锚点(drive+root) |
C:\ |
parents 属性是一个特殊的可迭代对象,可以像 parent 的链式调用一样逐级向上访问父目录,但更加简洁:
from pathlib import Path
p = Path("/a/b/c/d/file.txt")
# 逐级向上
print(p.parent) # /a/b/c/d
print(p.parent.parent) # /a/b/c
print(p.parent.parent.parent) # /a/b
# 使用 parents 索引更加简洁
print(p.parents[0]) # /a/b/c/d
print(p.parents[1]) # /a/b/c
print(p.parents[2]) # /a/b
print(p.parents[3]) # /a
print(p.parents[4]) # /
# 遍历所有父目录
for parent in p.parents:
print(parent)
# /a/b/c/d
# /a/b/c
# /a/b
# /a
# /
在日常开发中,name 和 parent 是最常用的两个属性,stem 和 suffix 在需要分离文件名和扩展名时非常方便,比传统 os.path.splitext() 更加直观。
五、文件操作
pathlib 将文件的常见读写操作直接封装在 Path 对象上,无需额外使用 open() 内置函数。以下是核心文件操作方法:
5.1 读取文件
from pathlib import Path
p = Path("notes.txt")
# 读取全部文本(自动处理编码)
text = p.read_text(encoding="utf-8")
print(text)
# 读取全部字节
data = p.read_bytes()
print(len(data)) # 文件字节数
# 逐行读取(使用 open() 上下文管理器)
with p.open("r", encoding="utf-8") as f:
for line in f:
print(line.strip())
5.2 写入文件
from pathlib import Path
p = Path("output.txt")
# 写入文本(覆盖模式,自动创建父目录吗?不!)
p.write_text("Hello, pathlib!\n第二行内容。\n", encoding="utf-8")
# 以追加模式写入(需要使用 open)
with p.open("a", encoding="utf-8") as f:
f.write("追加的内容\n")
# 写入二进制数据
p2 = Path("data.bin")
p2.write_bytes(b"\x00\x01\x02\x03\xff")
# 写文件前确保父目录存在
target = Path("subdir/nested/file.txt")
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text("Content")
重要提醒
- write_text() 和 write_bytes() 不会自动创建父目录,写入前需要手动调用 parent.mkdir(parents=True, exist_ok=True)
- write_text() 默认使用系统编码,显式指定 encoding="utf-8" 是最佳实践
- 这两个方法都是覆盖写入,文件原有内容会被清空
5.3 重命名与删除
from pathlib import Path
p = Path("old_name.txt")
# rename —— 重命名文件或目录
p.rename("new_name.txt")
# 也可以移动到其他目录(类似 mv 命令)
p.rename(Path("/backup/old_name.txt"))
# unlink —— 删除文件(不能用于目录)
file = Path("temp.txt")
file.unlink() # 删除文件
file.unlink(missing_ok=True) # 文件不存在时不报错(Python 3.8+)
# rmdir —— 删除空目录(目录非空则会抛出 OSError)
empty_dir = Path("empty_folder")
empty_dir.rmdir()
# 删除非空目录 —— 需要使用 shutil 模块
import shutil
shutil.rmtree(Path("non_empty_folder"))
5.4 创建目录
from pathlib import Path
# mkdir —— 创建目录
Path("new_folder").mkdir() # 创建单个目录
Path("a/b/c/d").mkdir(parents=True) # 递归创建不存在的父目录
Path("existing").mkdir(exist_ok=True) # 目录已存在时不报错
Path("a/b/c").mkdir(parents=True, exist_ok=True) # 常见组合
# touch —— 创建空文件(类似 Linux touch 命令)
Path("new_file.txt").touch() # 创建空文件
Path("existing.txt").touch(exist_ok=True) # 文件已存在时更新修改时间而非报错
六、目录遍历
目录遍历是文件系统操作中最常见的需求之一。pathlib 提供了多个层级的方法来满足不同粒度的遍历需求。
6.1 iterdir —— 遍历直接子项
iterdir() 返回一个生成器,逐个产生当前目录下的所有直接子项(不递归),每个子项都是一个 Path 对象:
from pathlib import Path
data_dir = Path("./data")
for item in data_dir.iterdir():
if item.is_file():
print(f"[文件] {item.name} (大小: {item.stat().st_size} 字节)")
elif item.is_dir():
print(f"[目录] {item.name}")
# 使用列表推导式快速分类
files = [f for f in data_dir.iterdir() if f.is_file()]
dirs = [d for d in data_dir.iterdir() if d.is_dir()]
6.2 glob —— 模式匹配
glob() 使用 Unix shell 风格的模式匹配来筛选文件,只搜索当前目录的直接子项(不递归):
from pathlib import Path
base = Path("./project")
# 查找所有 .py 文件
for py in base.glob("*.py"):
print(py)
# 查找以 "test_" 开头的文件
tests = base.glob("test_*")
# glob 支持单层子目录模式匹配(但不是递归)
configs = base.glob("*/config.yaml")
# 统计某类型文件数量
img_count = len(list(base.glob("*.png")))
print(f"找到 {img_count} 个 PNG 文件")
6.3 rglob —— 递归模式匹配
rglob() 是 glob() 的递归版本,搜索所有子目录(深度不限)。这是最常用的目录遍历方法之一,特别适合于查找项目中所有满足条件的文件:
from pathlib import Path
base = Path("./project")
# 递归查找所有 .py 文件(无论嵌套多深)
for py in base.rglob("*.py"):
print(py)
# 查找所有 __init__.py 文件
init_files = list(base.rglob("__init__.py"))
print(f"找到 {len(init_files)} 个 __init__.py")
# 查找所有包含 "test" 的目录
test_dirs = [d for d in base.rglob("*test*") if d.is_dir()]
# 使用 ** 在 glob 中表达递归(效果与 rglob 相同)
# 注意:glob("**/*.py") 和 rglob("*.py") 等价
py_files_1 = list(base.glob("**/*.py"))
py_files_2 = list(base.rglob("*.py"))
print(py_files_1 == py_files_2) # True
# 进阶:排除某些模式
# 查找所有 .txt 文件但排除在 node_modules 中的
for txt in base.rglob("*.txt"):
if "node_modules" not in txt.parts:
print(txt)
6.4 walk —— Python 3.12+ 的新方法
Python 3.12 为 Path 增加了 walk() 方法,功能类似于 os.walk(),但返回的是 Path 对象,使用更加方便。它按自顶向下或自底向上的顺序遍历目录树:
from pathlib import Path
base = Path("./project")
# walk() 返回三元组 (当前目录, 子目录列表, 文件列表)
for root, dirs, files in base.walk():
print(f"目录: {root}")
print(f" 子目录: {', '.join(d.name for d in dirs)}")
print(f" 文件数: {len(files)}")
# top_down=False 表示自底向上遍历
for root, dirs, files in base.walk(top_down=False):
# 自底向上时,可以先删除文件再删除目录
for f in files:
f.unlink()
root.rmdir() # 目录已空,可以删除
# walk() vs rglob() 的选择:
# - 需要同时处理目录和文件时 → walk()
# - 只需要匹配特定模式的文件时 → rglob()
遍历方法的选择指南
- iterdir(): 只需要当前目录的直接子项,不需要筛选
- glob(): 需要当前目录下匹配特定模式的文件(不递归)
- rglob(): 需要递归查找所有子目录中匹配特定模式的文件(最常用)
- walk(): Python 3.12+,需要同时访问目录和文件的完整信息,控制遍历方向
七、文件信息与元数据
Path 对象通过 stat() 方法获取文件的详细元数据,该方法返回 os.stat_result 对象:
from pathlib import Path
import os
import stat
import time
p = Path("example.py")
st = p.stat()
print("文件大小:", st.st_size, "字节")
print("最后修改时间:", time.ctime(st.st_mtime))
print("最后访问时间:", time.ctime(st.st_atime))
print("创建时间:", time.ctime(st.st_ctime))
print("文件权限:", oct(st.st_mode))
print("inode编号:", st.st_ino)
# 判断文件类型(基于 stat 结果)
if stat.S_ISREG(st.st_mode):
print("这是普通文件")
elif stat.S_ISDIR(st.st_mode):
print("这是目录")
elif stat.S_ISLNK(st.st_mode):
print("这是符号链接")
便捷的路径检查方法
from pathlib import Path
p = Path("some_path")
p.exists() # 路径是否存在
p.is_file() # 是否是文件(且存在)
p.is_dir() # 是否是目录(且存在)
p.is_symlink() # 是否是符号链接
p.is_socket() # 是否是 socket 文件
p.is_fifo() # 是否是 FIFO(命名管道)
p.is_block_device() # 是否是块设备
p.is_char_device() # 是否是字符设备
p.is_mount() # 是否是挂载点(Python 3.7+)
# stat 的快捷方法
p.stat().st_size # 文件大小(字节)
p.lstat().st_size # 如果是符号链接,获取链接本身的元数据而非目标
owner 和 group —— 查询文件所有者
Path 还提供了两个跨平台的方法来查询文件的所有者和所属组(在 Windows 上返回用户名和域名):
from pathlib import Path
p = Path("/etc/hosts")
print("所有者:", p.owner()) # root(POSIX)
print("所属组:", p.group()) # root(POSIX)
# lchmod —— 修改符号链接的权限(而非目标文件)
# chmod —— 修改文件权限
p.chmod(0o644) # rw-r--r--
八、路径解析与转换
在实际开发中,我们经常需要在相对路径和绝对路径之间转换,或者计算两个路径之间的相对关系。pathlib 提供了清晰的 API 来完成这些任务。
8.1 resolve —— 解析为规范化的绝对路径
resolve() 将路径解析为绝对路径,并自动处理 .. 和 . 以及符号链接:
from pathlib import Path
# 相对路径转绝对路径
rel = Path("../docs/./guide/../api/config.yaml")
abs_path = rel.resolve()
print(abs_path) # 如 /home/user/projects/api/config.yaml
# resolve() 还可以解析符号链接
link = Path("/usr/bin/python3") # 假设这是符号链接
real = link.resolve()
print(real) # 如 /usr/bin/python3.10(符号链接的目标真实路径)
8.2 absolute —— 获取绝对路径(不解析符号链接)
absolute() 仅将相对路径改为绝对路径,不会展开 .. 和 .,也不会解析符号链接:
from pathlib import Path
rel = Path("../docs/config.yaml")
abs_path = rel.absolute()
print(abs_path) # 如 /home/user/projects/../docs/config.yaml
# 注意:absolute() 保留了 ..,而 resolve() 会去掉它
# absolute() 和 resolve() 的差异
p = Path("./subdir/../file.txt")
print(p.absolute()) # /home/user/subdir/../file.txt (不规范化)
print(p.resolve()) # /home/user/file.txt (规范化)
absolute() vs resolve()
- absolute(): 仅在路径前拼接当前工作目录,不做任何规范化处理,不访问文件系统,不会失败
- resolve(): 将路径规范化为标准绝对路径(去除
.. 和 .),解析符号链接,要求路径存在(否则在某些系统上可能会失败或返回非预期结果)
8.3 relative_to —— 计算相对路径
relative_to() 计算从一个路径到另一个路径的相对关系,相当于 os.path.relpath() 的 pathlib 版本:
from pathlib import Path
p = Path("/home/user/projects/src/main.py")
# 相对于某个基准路径
rel = p.relative_to("/home/user/projects")
print(rel) # src/main.py
# 在项目目录结构中非常有用
project_root = Path("/home/user/myapp")
file_path = project_root / "src/utils/helper.py"
module_path = file_path.relative_to(project_root)
module_name = ".".join(module_path.with_suffix("").parts)
print(module_name) # src.utils.helper (可用于动态 import)
# 如果路径不在基准路径下,会抛出 ValueError
try:
p.relative_to("/other/path")
except ValueError as e:
print(e) # '...' does not start with '...'
8.4 路径类型转换与校验
from pathlib import Path, PurePosixPath, PureWindowsPath
# Path ↔ 字符串
p = Path("/data/file.txt")
s = str(p) # "/data/file.txt"
p2 = Path(s) # Path("/data/file.txt")
# 跨平台路径转换
posix = PurePosixPath("/home/user/file.txt")
win = PureWindowsPath("C:\\Users\\Admin\\file.txt")
# as_posix —— 将 Windows 路径转为 POSIX 风格(字符串)
print(win.as_posix()) # C:/Users/Admin/file.txt
# as_uri —— 将绝对路径转为 file:// URI
print(p.as_uri()) # file:///data/file.txt
# with_name / with_stem / with_suffix —— 替换路径的组成部分
p = Path("/data/report.txt")
print(p.with_name("summary.txt")) # /data/summary.txt
print(p.with_stem("summary")) # /data/summary.txt (Python 3.9+)
print(p.with_suffix(".md")) # /data/report.md
# 文件后缀替换的常见用法:日志轮转
log_path = Path("app.log")
archive = log_path.with_suffix(".log.1")
print(archive) # app.log.1
# 注意:with_suffix 会替换最后一个后缀
九、Path vs os.path 深度对比
对于有 Python 3 开发经验的开发者,了解 pathlib 与 os.path 之间的对应关系,可以帮助高效迁移旧代码。以下是常用操作的全面对照表:
| 功能 |
os.path / os 方式 |
pathlib 方式 |
| 当前目录 |
os.getcwd() |
Path.cwd() |
| 用户目录 |
os.path.expanduser("~") |
Path.home() |
| 路径拼接 |
os.path.join(a, b) |
Path(a) / b 或 Path(a).joinpath(b) |
| 路径存在 |
os.path.exists(p) |
Path(p).exists() |
| 是否是文件 |
os.path.isfile(p) |
Path(p).is_file() |
| 是否是目录 |
os.path.isdir(p) |
Path(p).is_dir() |
| 文件名 |
os.path.basename(p) |
Path(p).name |
| 父目录 |
os.path.dirname(p) |
Path(p).parent |
| 文件名无后缀 |
os.path.splitext(os.path.basename(p))[0] |
Path(p).stem |
| 文件后缀 |
os.path.splitext(p)[1] |
Path(p).suffix |
| 绝对路径 |
os.path.abspath(p) |
Path(p).resolve() |
| 相对路径 |
os.path.relpath(p, start) |
Path(p).relative_to(start) |
| 拆分路径 |
os.path.split(p) |
Path(p).parent, Path(p).name |
| 创建目录 |
os.makedirs(d, exist_ok=True) |
Path(d).mkdir(parents=True, exist_ok=True) |
| 读取文本 |
open(p).read() |
Path(p).read_text() |
| 写入文本 |
open(p, "w").write(s) |
Path(p).write_text(s) |
| 删除文件 |
os.remove(p) |
Path(p).unlink() |
| 重命名 |
os.rename(a, b) |
Path(a).rename(b) |
| 文件大小 |
os.path.getsize(p) |
Path(p).stat().st_size |
| 修改时间 |
os.path.getmtime(p) |
Path(p).stat().st_mtime |
| 列表目录 |
os.listdir(d) |
Path(d).iterdir() |
| 模式匹配 |
glob.glob("**/*.py") |
Path().rglob("*.py") |
| 环境变量展开 |
os.path.expandvars(s) |
PurePath 不支持,需结合 os.path.expandvars |
迁移建议
- 新项目直接使用 pathlib: 所有涉及路径操作的新代码都应优先使用 pathlib
- 旧项目逐步替换: 从 os.path.join 和文件读写部分开始,逐步替换为 pathlib 风格
- 混合使用无问题: Path 对象可以传给 os 模块的函数(Python 3.6+),也可以被 str() 转换回字符串,过渡期间可以混合使用
- 性能差异: pathlib 作为纯 Python 实现,在极高频调用的场景下(如每秒数万次操作)可能比 C 语言实现的 os.path 稍慢,但对于绝大多数应用此差异可以忽略
十、实际应用场景
场景一:项目配置路径管理
from pathlib import Path
class ProjectPaths:
"""项目路径管理器"""
def __init__(self, root: str | Path):
self.root = Path(root).resolve()
self.src = self.root / "src"
self.tests = self.root / "tests"
self.data = self.root / "data"
self.logs = self.root / "logs"
def ensure_dirs(self):
"""确保所有必需的目录存在"""
for d in [self.src, self.tests, self.data, self.logs]:
d.mkdir(parents=True, exist_ok=True)
def find_configs(self):
"""递归查找所有 .yaml 和 .json 配置文件"""
configs = list(self.src.rglob("*.yaml"))
configs.extend(self.src.rglob("*.json"))
return configs
def log_path(self, name: str) -> Path:
"""生成日志文件路径(自动添加日期)"""
from datetime import date
today = date.today().isoformat()
return self.logs / f"{today}_{name}.log"
# 使用示例
paths = ProjectPaths("/home/user/myapp")
paths.ensure_dirs()
print(paths.find_configs())
log_file = paths.log_path("worker")
log_file.write_text("进程启动\n", encoding="utf-8")
场景二:批量重命名文件
from pathlib import Path
def rename_files(directory: str, prefix: str, ext: str):
"""批量重命名指定扩展名的文件"""
dir_path = Path(directory)
for i, file in enumerate(dir_path.glob(f"*.{ext}"), 1):
new_name = f"{prefix}_{i:03d}.{ext}"
file.rename(file.parent / new_name)
print(f"重命名: {file.name} → {new_name}")
# rename_files("downloads", "photo", "jpg")
场景三:文件系统监控辅助
from pathlib import Path
import time
def find_recently_modified(directory: str, minutes: int = 60) -> list[Path]:
"""查找指定时间内修改过的文件"""
cutoff = time.time() - minutes * 60
dir_path = Path(directory)
recent = []
for f in dir_path.rglob("*"):
if f.is_file() and f.stat().st_mtime > cutoff:
recent.append(f)
return sorted(recent, key=lambda x: x.stat().st_mtime, reverse=True)
# 获取最近 1 小时内修改过的文件
print(find_recently_modified(".", minutes=60))
十一、核心要点总结
- pathlib 是 Python 文件路径操作的首选方式: 自 Python 3.4 引入,3.6 起成为核心库,提供了面向对象的路径操作接口,远比传统的 os.path 字符串操作更加直观和简洁
- PurePath vs Path: PurePath 仅做字符串级别的路径操作不涉及 I/O;Path 继承自 PurePath 并增加了所有文件系统操作能力。日常开发使用 Path 即可
/ 运算符拼接路径: pathlib 最亮眼的特性,代码书写如终端命令般自然。注意右侧以 / 开头时会重置左侧路径
- 路径属性丰富: name、stem、suffix、parent、parts 等属性覆盖了所有路径拆解需求
- 文件操作一站式: read_text()、write_text()、rename()、unlink() 等方法直接封装在 Path 对象上
- 目录遍历三剑客: iterdir() 遍历直接子项、glob() 模式匹配不递归、rglob() 递归匹配(最常用)。Python 3.12+ 新增 walk() 方法
- 路径解析清晰: resolve() 规范化绝对路径、absolute() 简单拼接工作目录、relative_to() 计算相对关系
- 告别 os.path: 新项目全部使用 pathlib,旧项目逐步迁移。pathlib 与现有 os 模块可以无缝混用,过渡成本极低
十二、进一步思考与练习
实践练习
- 文件分类器: 编写一个脚本,使用 Path 遍历某个目录,按文件扩展名将文件分类到不同的子目录中(如
.jpg/.png → images/,.mp3/.wav → audio/)
- 日志清理工具: 使用 Path.stat().st_mtime 查找并删除指定目录中超过 N 天未修改的日志文件
- 项目脚手架: 编写一个函数,接收项目名称和目录结构描述(嵌套字典),自动创建完整的项目目录树和必要的
__init__.py 文件
- 重复文件查找器: 使用 rglob() 遍历目录树,通过文件名和文件大小检测可能的重复文件
- 路径表达式解析: 使用 PurePath 的 parts 属性,实现一个函数将文件系统路径转换为 Python 模块导入路径
补充阅读建议
- 官方文档: Python pathlib 模块文档 — 权威参考
- PEP 428: pathlib 模块的最初设计提案,了解设计思路和取舍
- Python 3.12 更新: 关注 Path.walk() 和 pathlib.Path. 相关的 changelog