Python 的路径操作 pathlib

不会吧?不会吧?不会有人还在用 os.path 等模块来操作文件系统路径吧?难写又难读,看着就头痛。

今天我们将介绍 pathlib 模块。它同 os.path 模块一样也是 Python 标准库的一员。它提供了面向对象的路径操作,简洁易用、语义明确,让你真正体验到「人生苦短,我用 Python」的路径操作。

模块概览

pathlib 模块中最重要的内容就是 Path 类,我们可以简单地使用一个路径字符串构造它,例如 Path('~/Downloads')。同时它提供了许多易于理解和使用的方法。这里将不逐一说明,使用时可在官方文档检索方法的详细说明。以下提供了它们与命令式风格的模块提供的函数的简单对照,但功能并不完全一致,可供参考。

构造操作

以下操作可以用于构造 Path 实例。

legacy pathlib
os.path.join() PurePath.joinpath(), PurePath.__truediv__()
os.getcwd() Path.cwd()
os.path.expanduser() Path.expanduser(), Path.home()
os.path.abspath() Path.resolve()
os.readlink() Path.readlink()
os.listdir() Path.iterdir()
glob.glob(), glob.iglob() Path.glob(), Path.rglob()
N/A Path.with_name(), Path.with_stem(), Path.with_suffix()

查询操作

以下操作可以用于查询 Path 实例对应的文件或目录的相关属性。

legacy pathlib
os.path.basename() PurePath.name, PurePath.stem
os.path.dirname() PurePath.parent, PurePath.parents
os.path.splitext() PurePath.suffix, PurePath.suffixes
os.path.isabs() PurePath.is_absolute()
os.path.isdir() Path.is_dir()
os.path.isfile() Path.is_file()
os.path.islink() Path.is_symlink()
os.path.samefile() Path.samefile()
os.path.exists() Path.exists()
os.stat() Path.stat(), Path.lstat(), Path.owner(), Path.group()

更改操作

以下操作可以用于更改(包括创建、修改和删除)Path 实例对应的文件或目录。

legacy pathlib
os.mkdir(), os.makedirs() Path.mkdir()
os.link() Path.link_to()
os.symlink() Path.symlink_to()
os.rename() Path.rename()
os.replace() Path.replace()
os.chmod() Path.chmod(), Path.lchmod()
os.rmdir() Path.rmdir()
os.remove(), os.unlink() Path.unlink()
N/A Path.touch()

输入输出操作

以下操作可以用于对(包括创建、修改和删除)Path 实例对应的文件进行输入输出。

legacy pathlib
open() Path.open()
open().read() Path.read_bytes(), Path.read_text()
open().write() Path.write_bytes(), Path.write_text()

实战演练

在这一部分,我们将在现实使用场景中使用 pathlib 做一些有实际意义的事情,以展示其强大的功能。

收集图片

collect_images.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from itertools import chain
from pathlib import Path

def collect_files_to_lines(source_folder, target_file, formats):
source_folder = Path(source_folder)
file_paths = (source_folder.rglob(f'*.{f}') for f in formats)
file_paths = sorted(chain.from_iterable(file_paths))
Path(target_file).write_text('\n'.join(map(str, file_paths)))
return len(file_paths)

if __name__ == '__main__':
num_images = collect_files_to_lines(
'images', 'images.txt', ['png', 'jpg', 'gif', 'bmp'])
print(f'{num_images = }')

collect_images.py 脚本将收集 images 目录下的所有以 pngjpggifbmp 为后缀的图片文件,按字典序排序后写入 images.txt 文件中。使用了 rglobwrite_text 方法。

拷贝文件

copy_files.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from itertools import chain
from pathlib import Path
import shutil

def copy_files_to_folder(source_folder, target_folder, formats):
source_folder, target_folder = Path(source_folder), Path(target_folder)
file_paths = (source_folder.rglob(f'*.{f}') for f in formats)
file_paths = sorted(chain.from_iterable(file_paths))
for source_path in file_paths:
relative_path = source_path.relative_to(source_folder)
target_path = target_folder.joinpath(relative_path)
target_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(source_path, target_path)
return len(file_paths)

if __name__ == '__main__':
num_copied = copy_files_to_folder('source', 'target', ['txt'])
print(f'{num_copied = }')

copy_files.py 脚本将收集 source 目录下的所有以 txt 为后缀的文件,在保持原来的目录结构的情况下拷贝到 target 目录下。

脚本中使用了 relative_to 方法来获取相对路径以及 mkdir 方法的 parentsexist_ok 参数来方便地创建目录结构。另外,target_folder.joinpath(relative_path) 也可以等价地写成 target_folder / relative_path

随着 Python 版本的迭代,很多如 shutil.copy 这样接受路径字符串的函数,甚至包括一些第三方包(如 NumPy 和 Pillow)中的某些函数(如 numpy.savenumpy.loadPIL.Image.open),现在也可以接受路径对象了。