pathlib
pathlib 模块提供了表示文件系统路径的类,并且对路径的抽象用于兼容不同的操作系统。
模块架构
上图就是 pathlib 的模块架构。整个操作系统包含两种类型的路径:
- PosixPath: 用于 MacOS、Linux 等类 Unix 操作系统,他们的特点就是
/
是根目录,而路径通过/
分割 - WindowsPath: 用于 Windows 操作系统,他使用
C:\
作为操作符, 而且使用\
作为路径的分割
该模块屏蔽了他们之间的区别提供了两种路径抽象:
- PurePath: 纯路径
- Path: 真实路径
之所以要抽象一个纯路径是为了兼容性,下面的几种用例中就比较适合使用纯路径:
- 如果要在 Unix 设备上操作 Windows 路径(或者相反),此时就可以使用纯路径来实现
- 如果只想操作路径对象而不想实际访问系统(通常是一个 URL 类的路径),此时就可以实例化一个纯路径
Note
在大多数时候都只需要构造 Path 对象就好了。它会自动根据当前操作系统来调用 PosixPath 或 WindowsPath 来构造路径对象。
Tips
更具体的实现思路可以参看PEP 428: pathlib 模块 和 PEP 355: Path,PEP355 被拒绝了,他 PEP428 是他的替代
路径构成
┌─────────────────────┬────────────┐
│ dir │ base │
├──────┬ ├──────┬─────┤
│ root │ │ name │ ext │
" / home/user/dir / file .txt "
└──────┴──────────────┴──────┴─────┘
# 上图是 nodejs Path 中的路径分割,Python 的也大致如此不过他们的名字有区别
Nodejs | Python | 说明 |
---|---|---|
root | anchor | 驱动器和根的联合 |
- | drive | 驱动器 |
- | root | 根 |
dir | patent | 父路径 |
base | name | 路径最后组件的字符串 |
name | stem | 最后组件去除后缀 |
ext | suffix | 后缀 |
# 驱动器算是 Windows 特有的
PureWindowsPath('c:/Program Files/').drive # 'c:'
PureWindowsPath('/Program Files/').drive # ''
PurePosixPath('/etc').drive # ''
# UNC 共享也被认为是驱动器
PureWindowsPath('//host/share/foo.txt').drive # '\\\\host\\share'
# root 表示根路径
PureWindowsPath('c:/Program Files/').root # '\\'
PurePosixPath('/etc').root # '/'
PureWindowsPath('//host/share').root # '\\'
# anchor 是驱动器和根路径的集合
# patent 用于表示符路径
PurePosixPath('/a/b/c/d').patent # PurePosixPath('/a/b/c')
# 父路径不能超过 anchor 或空路径
PurePosixPath('/').parent # PurePosixPath('/')
PurePosixPath('.').parent # PurePosixPath('.')
# 还有一个比较特殊的 patents 来返回祖先列表
p = PureWindowsPath('c:/foo/bar/setup.py')
p.parents[0] # PureWindowsPath('c:/foo/bar')
p.parents[1] # PureWindowsPath('c:/foo')
p.parents[2] # PureWindowsPath('c:/')
# name 表示路径的最后一个组件(不一定是文件)
PurePosixPath('my/library/setup.py').name # 'setup.py'
# 不能超过 anchor
PureWindowsPath('//some/share').name # ''
# name 还能够拆封为 stem 和 suffix
# 只返回最后一个后缀,有一个 suffixes 会返回所有后缀
PurePosixPath('my/library.tar.gz').suffix # '.gz'
PurePosixPath('my/library.tar.gar').suffixes # ['.tar', '.gar']
# stem 同样只剔除最后一个后缀
PurePosixPath('my/library.tar.gz').stem # 'library.tar'
修改特定组件
除了通过 /
来拼接路径外,pathlib 还提供了几个方法来修改组件:
方法 | 说明 |
---|---|
Path.joinpath(*pathsegments) |
等价于 / 运算符 |
Path.with_name(name) |
返回一个新的路径并修改 name,需要注意如果原始路径没有 name 会抛出 ValueError |
Path.with_stem(stem) |
返回一个带有修改后 stem 的新路径,注意如果原始路径没有 name 会抛出 ValueError |
Path.with_suffix(suffix) |
返回一个带有修改后的 suffix 的新路径,如果原来的没有后缀,新的后缀会添加,如果是空字符串则原有路径被删除 |
路径对象都是 hashable 的,所有修改的方法都是返回一个新的路径对象。
通过 / 运算符来拼接路径
Path 还将 /
操作符进行了重载,允许直接拼接路径:
p = PurePath('/etc')
# PurePosixPath('/etc')
p / 'init.d' / 'apache2' # 直接使用 / 来进行拼接
# PurePosixPath('/etc/init.d/apache2')
q = PurePath('bin') # 相对路径
'/usr' / q
# PurePosixPath('/usr/bin')
p / '/an_absolute_path' # 两个绝对路径进行拼接,后者覆盖前者
# PurePosixPath('/an_absolute_path')
PureWindowsPath('c:/Windows', '/Program Files')
# PureWindowsPath('c:/Program Files')
相对路径处理
pathlib 支持相对路径,他提供了判断、获取相对路径的相关方法:
# 判断是否是相对路径
PurePosixPath('/a/b').is_absolute() # True
# 判断路径是否相对于某个路径
p = PurePath('/etc/passwd')
p.is_relative_to('/etc') # True
p.is_relative_to('/usr') # False
# 计算此路径相对于某个路径的相对路径,如果不能计算引发 ValueError
# 返回的结果是一个相对路径
p = PurePosixPath('/etc/passwd')
p.relative_to('/') # PurePosixPath('etc/passwd')
p.relative_to('/etc') # PurePosixPath('passwd')
p.relative_to('/usr') # ValueError
特殊路径扩展
有些相对路径会包含 .
、..
和 ~
(这个不应该算相对路径)这样的符号,他们需要调用特定的函数来返回绝对路径:
# 对于 ~
PosixPath('~/films/Monty Python').expanduser() # PosixPath('/home/eric/films/Monty Python')
# 将路径绝对化,解析任何符号
p = Path() # PosixPath('.')
p.resolve() # PosixPath('/home/antoine/pathlib')
# 会相对于工作目录并解析任何符号来返回绝对路径
Path('docs/../setup.py').resolve() # PosixPath('/home/antoine/pathlib/setup.py')
还有两个命令来获取特殊目录:
# 返回家目录
Path.home() # PosixPath('/home/antoine')
# 返回当前目录,和 os.getcwd() 一样,不过返回 Path 而不是字符串
Path.cwd() # PosixPath('/home/antoine/pathlib')
文件接口
我们构建路径的核心就是用于定位操作系统中的文件,Path 上绑定了一些方法来更方便的对文件进行增删改查。
文件属性
使用 Path.stat()
能够返回文件的属性,他的返回值是 os.stat_result
对象,其中主要包括:
属性 | 说明 |
---|---|
st_mode | 文件权限 |
st_uid | 文件所有者的用户 ID |
st_gid | 文件所有者的用户组 ID |
st_size | 文件大小(字节单位) |
st_rsize | 文件实际大小(字节单位) |
st_rsize | 文件实际大小(字节单位) |
st_atime | 最近访问时间时间戳 |
st_mtime | 最近修改时间时间戳 |
st_birthtime | 最近创建时间时间戳 |
st_fstype | 文件所在的文件系统类型 |
判断路径存在
有一个统一的 Path.exists()
来判断路径是否指向一个已经存在的文件或目录:
Path('.').exists() #True
Path('setup.py').exists() #True
Path('/etc').exists() #True
Path('nonexistentfile').exists() #False
Path 还提供了一系列的 is_xxx()
方法来判断指定路径是否是对应类型文件:
Path.is_file()
: 判断当前路径是否是一个存在的文件Path.is_dir()
: 判断当前路径是否是一个存在的目录
创建文件或目录
他们对应了 shell 中的 touch 和 mkdir 命令:
Path.touch(mode=0o666, exist_ok=True)
: 创建文件,mode 指定文件全新,exist_ok=True
会在文件存在时更新其修改时间,如果为 False 文件存在时引发FileExistsError
异常,通常不怎么用这个方法,更多的是直接 Path 提供的IO 接口Path.mkdir(mode=0o777, parents=False, exist_ok=False)
: 创建目录,如果patents=True
则父路径不能存在时创建(即 -p 递归创建)
还有一个比较特殊的就是创建连接:
Path.symlink_to(target:Path|str)
: 创建一个指向 target 的符号连接Path.hardlink_to(target:Path|str)
: 创建一个指向 target 的硬连接
修改文件和目录
修改包括重命名、修改权限、还有删除:
Path.unlink(missing_ok=False)
: 删除文件或符号连接,如果路径指向目录等价于Path.rmdir()
Path.rmdir()
: 移除空目录Path.chmod(mode)
: 修改权限Path.rename(target)
: 根据 target 重命名,这个相当于移动命令,在类 Unix 如果 target 存在则默认替换,在 Windows 下如果文件存在则引发 FileExistsErrorPath.replace(target)
: 根据 target 移动,他会被默认替换
Tips
rename 和 replace 在 Linux 下仅仅是语意的不同。target 还可以是相对路径,他会使用当前工作目录补全。
遍历当前目录
Path 同样提供了类似于 ls 这样的命令,来读取目录下的所有文件:
Path.iterdir()
: 返回路径下的所有文件的迭代器,条目顺序是任意产生的,不包含特殊的.
和..
,如果该路径不是一个目录会引发 OSErrorPath.glob(pattern)
: 解析相对于 pattern 通配符的路径Path.rglob(pattern)
: 相当于Path.glob
中的通配符以**/
开头,即递归获取
glob 支持通配符获取文件,他由fnmatch模块的类 Unix 的通配符风格:
*
: 匹配所有?
: 匹配单个字符[seq]
: 匹配 seq 中的任意字符[!seq]
: 匹配不在 seq 中的任意字符**/
递归匹配
因此要想递归匹配目录下的所有文件就需要 **/*
,而 rglob 是一个快捷方法可以只指定 *
。
IO 接口
Path 上直接绑定了 open 等方法来读取文件的内容:
Path.open(mode='r')
: 打开路径指向的文件,等价于open(Path)
,同样支持上下文管理Path.read_text(encoding=None)
: 以字符串形式返回路径指向的文件Path.read_bytes()
: 以字节对象形式返回路径指向的文件Path.write_text(data, encoding=None, errors=None, newline=None)
: 将文件以文件模式打开('w'),写入 data 后关闭Path.write_bytes(data)
: 将文件以二进制模式打开('wb'),写入 data 后关闭