跳至主要内容

使用 Python 的 pathlib 进行文件系统操作的完整指南

通过探索路径对象、路径组件和常见路径操作,了解 Python pathlib 相较于 os 模块的优势,用于与文件系统交互。
· 9分钟 阅读

使用 AI 探索

直到最近,在 Python 中操作文件系统一直令人头疼。开发者常常被不正确的文件路径所困扰,因为这类路径需要以冗长的字符串作为输入,极易出错。此外,不同操作系统之间的不一致也常常导致代码频繁崩溃。

好在从 Python 3.4 版本开始,开发者将 pathlib 模块加入了标准库。pathlib 以久违的面向对象方式优雅地处理文件系统路径,并确保跨平台一致的行为。

本教程将全面讲解 pathlib 模块的功能,帮助您在日常工作中高效与文件系统交互。借助 pathlib,您将获得更顺畅的工作流程与更便捷的数据检索。多年来,pathlib 日趋成熟,我们也持续跟进更新,让您省心省力。让我们开始吧。

Python os 模块 vs. pathlib

在 Python 3.4 之前,处理文件路径的传统方式是使用 os 模块。尽管 os 模块曾经非常高效,但如今已显老态。

我们可以通过数据科学中的一个常见任务来展示 pathlib 的独特价值:如何在指定目录及其所有子目录中查找所有 png 文件。

如果使用 os 模块,我们可能会写出如下代码:

import os
dir_path = "/home/user/documents"
files = [
os.path.join(dir_path, f)
for f in os.listdir(dir_path)
if os.path.isfile(os.path.join(dir_path, f)) and f.endswith(".png")
]

尽管这段代码可以完成查找 png 文件的即时任务,但也暴露了 os 模块的几个主要缺点。首先,代码冗长且几乎不可读,这很可惜,因为这是个相对简单的操作。其次,我们的代码默认读者掌握列表推导式,这并非理所当然。再次,代码涉及字符串操作,容易出错。除此之外,代码也不够简洁。 

如果改用 pathlib 模块,代码会简洁很多。正如我们所说,pathlib 以面向对象的方式处理文件系统路径。请看:

from pathlib import Path

# Create a path object
dir_path = Path(dir_path)

# Find all text files inside a directory
files = list(dir_path.glob("*.png"))

这种面向对象的编程将代码围绕对象及其交互组织起来,使代码更模块化、可复用且易维护。如果您还不熟悉面向对象编程,值得通过我们的Python 面向对象编程课程进行学习。 

在 Python 中使用路径对象

pathlib 库以所谓的 Path 对象为核心,这些对象以结构化且跨平台的方式表示文件系统路径。

在本教程前面部分,我们通过以下代码将 pathlib 模块中的 Path 类引入当前命名空间:

from pathlib import Path

调用 pathlib 的 Path 类后,我们可以通过多种方式创建 Path 对象,包括基于字符串、基于其他 Path 对象、基于当前工作目录,以及基于主目录。

我们依次来看每一种方式。

从字符串创建路径对象

我们可以通过将表示文件系统路径的字符串传给变量来创建一个 Path 对象。这会把文件路径的字符串表示转换为 Path 对象。

file_path_str = "data/union_data.csv"

data_path = Path(file_path_str)

从其他路径对象创建路径对象

现有的 Path 对象可以作为构建新路径的基石。 

我们通过将基础路径、数据目录和文件名组合成一个完整的文件路径来实现。在需要扩展 Path 对象时,请记得在合适的位置使用正斜杠。

base_path = Path("/home/user")
data_dir = Path("data")

# Combining multiple paths
file_path = base_path / data_dir / "prices.csv"  
print(file_path)
'/home/user/data/prices.csv'

基于当前工作目录创建路径对象

这里我们使用 Path.cwd() 方法将当前工作目录赋给变量 cwd。随后即可获取脚本运行所在的当前工作目录路径。 

cwd = Path.cwd()

print(cwd)
'/home/bexgboost/articles/2024/4_april/8_pathlib'

基于主目录创建路径对象

我们可以通过将主目录与其他子目录组合来构造路径。此处我们将主目录与子目录 "downloads" 和 "projects" 进行组合。 

home = Path.home()

home / "downloads" / "projects"
PosixPath('/home/bexgboost/downloads/projects')

重要说明:Path 类本身不执行任何文件系统操作,例如路径校验、创建目录或文件。它的职责是表示和操作路径。若要真正与文件系统交互(检查存在性、读写文件),我们需要使用 Path 对象的专用方法;在一些高级场景下,还需要借助 os 模块。

在 Python 中使用路径组件

文件路径属性是指文件路径的各种特性和组成部分,它们有助于在文件系统中定位和管理文件与目录。就像一个物理地址包含门牌号、城市、国家和邮编等部分一样,文件系统路径也可以拆分为更小的组件。pathlib 允许我们通过点号语法访问和操作这些路径属性。

使用根目录

根目录是文件系统中最顶层的目录。在类 Unix 系统中以正斜杠(/)表示;在 Windows 中通常是带冒号的盘符,如 C:。

image_file = home / "downloads" / "midjourney.png"

image_file.root
'/'

使用父目录

父目录包含当前文件或目录。它相对于当前目录或文件高一级。 

image_file.parent
PosixPath('/home/bexgboost/downloads')

使用文件名

该属性返回完整文件名(包含扩展名),类型为字符串。

image_file.name
'midjourney.png'

使用文件后缀

suffix 属性返回文件扩展名(包含点),类型为字符串(若无扩展名则为空字符串)。

image_file.suffix
'.png'

使用文件主名(stem)

stem 返回不带扩展名的文件名。在进行文件格式转换时,处理 stem 会很有用。

image_file.stem
'midjourney'

注意:在 Mac 上,文件路径区分大小写,因此 /Users/username/Documents 与 /users/username/documents 会被视为不同路径。 

pathlib 的 parts 属性

我们可以使用 .parts 属性将一个 Path 对象拆分为其组成部分。

image_file.parts
('/', 'home', 'bexgboost', 'downloads', 'midjourney.png')

pathlib 的 parents 属性

parents 属性会返回一个生成器,将这些组件转换为 Path 对象。 

list(image_file.parents)
[PosixPath('/home/bexgboost/downloads'),
PosixPath('/home/bexgboost'),
PosixPath('/home'),
PosixPath('/')]

使用 pathlib 的常见路径操作

Path 对象拥有许多方法,能让您高效地与目录及其内容进行交互。下面我们来看一些最常用的操作。

列出目录

iterdir() 方法允许您遍历文件夹中的所有文件和子目录。它在需要处理目录中所有文件或对每个条目执行操作时尤其有用。

cwd = Path.cwd()
 for entry in cwd.iterdir():
# Process the entry here
...
# print(entry)

由于 iterdir() 返回一个迭代器,条目会在循环过程中按需获取。

is_dir() 方法

is_dir() 方法在路径指向目录时返回 True,否则返回 False。

for entry in cwd.iterdir():
   if entry.is_dir():
      print(entry.name)
.ipynb_checkpoints
data
images

is_file() 方法

.is_file() 方法在路径指向文件时返回 True,否则返回 False。

for entry in cwd.iterdir():
   if entry.is_file():
      print(entry.suffix)
.ipynb
.txt

exists() 方法

由于 Path 对象只表示路径,有时需要使用 .exists() 方法检查路径是否存在:

.exists() 方法用于检查路径是否存在。由于 Path 对象可以表示文件系统中可能存在或不存在的文件和目录,因此该方法非常实用。

image_file.exists()
False

创建与删除路径

pathlib 还提供了创建与删除文件和目录的功能。我们来看看如何使用。

mkdir() 方法

mkdir() 方法会在指定路径创建新目录。默认情况下,它会在当前工作目录下创建目录。

from pathlib import Path

data_dir = Path("new_data_dir")

# Create the directory 'new_data_dir' in the current working directory
data_dir.mkdir()

mkdir(parents=True) 方法

mkdir(parents=True) 方法在希望创建包含尚不存在的父目录的目录结构时尤其有用。将 parents=True 设为真可确保沿途所有必要的父目录一并创建。

sub_dir = Path("data/nested/subdirectory")

# Create 'data/nested/subdirectory', even if 'data' or 'nested' don't exist
sub_dir.mkdir(parents=True)

请注意,如果同名目录已存在,mkdir() 会抛出异常。

Path('data').mkdir()
FileExistsError: [Errno 17] File exists: 'data'

unlink() 方法

unlink() 方法会永久删除由 Path 对象表示的文件。为避免出错,建议在运行该方法前检查文件是否存在。

to_delete = Path("data/prices.csv")

if to_delete.exists():
   to_delete.unlink()
   print(f"Successfully deleted {to_delete.name}")
Successfully deleted prices.csv

rmdir() 方法

rmdir() 方法用于删除空目录。请记住,rmdir() 只能删除空目录。删除非空目录的最简便方法是使用 shutil 库或终端。

empty_dir = Path("new_data_dir")

empty_dir.rmdir()

注意:使用 unlink() 或 rmdir() 时请务必谨慎,因为其结果是永久性的。

高级路径操作

接下来我们来看一些高级路径操作概念,以及如何用 pathlib 在实践中应用它们。

相对路径 vs. 绝对路径

我们先来理解绝对路径与相对路径的区别,因为这类问题经常出现。

相对路径

相对路径是相对于当前目录来指定文件或目录的位置,因此称为“相对”。它们在项目内简短且灵活,但如果更改了工作目录,就可能引发混淆。

例如,我在当前工作目录中有一个 images 文件夹,其中包含 midjourney.png 文件。

image = Path("images/midjourney.png")

image
PosixPath('images/midjourney.png')

上述代码现在可行,但如果我把正在使用的笔记本移动到其他位置,该代码片段就会失效,因为 images 文件夹并未随笔记本一起移动。

绝对路径

绝对路径从文件系统根目录开始,指定文件或目录的完整位置。它们不依赖当前目录,为系统中任意用户提供清晰的参照点。

image_absolute = Path("/home/bexgboost/articles/2024/4_april/8_pathlib/images/midjourney.png")

image_absolute
PosixPath('/home/bexgboost/articles/2024/4_april/8_pathlib/images/midjourney.png')

如您所见,绝对路径可能很长,尤其是在具有嵌套树状结构的复杂项目中。因此,多数人更偏好更为简短的相对路径。

resolve 方法

pathlib 提供了 resolve() 方法,用于将相对路径转换为绝对路径。

relative_image = Path("images/midjourney.png")

absolute_image = relative_image.resolve()

absolute_image
PosixPath('/home/bexgboost/articles/2024/4_april/8_pathlib/images/midjourney.png')

我们也可以反向操作:若已是绝对路径,则可基于某个参考目录将其转换为相对路径。

relative_path = Path.cwd()

absolute_image.relative_to(relative_path)
PosixPath('images/midjourney.png')

通配匹配(Globbing)

为演示通配匹配,我们回到本文开头的示例:查找给定目录中的所有 png 文件。

files = list(dir_path.glob("*.png"))

pathlib 使用内置的 .glob() 模块高效搜索任意目录中与特定模式匹配的文件。在处理具有相似名称或扩展名的文件时,该模块非常有用。

glob 方法通过接收包含通配符的模式字符串作为输入来工作,并返回一个生成器对象,按需产出匹配的 Path 对象:

  • *:匹配零个或多个字符。

  • ?:匹配任意单个字符。

  • []:匹配方括号内的字符范围(例如 [a-z] 匹配任意小写字母)。

举例来说,我们尝试在我的 articles 目录中查找所有 Jupyter 笔记本。

articles_dir = Path.home() / "articles"

# Find all scripts
notebooks = articles_dir.glob("*.ipynb")

# Print how many found
print(len(list(notebooks)))
0

.glob() 方法没有找到任何笔记本,乍看之下令人惊讶,因为我写过 150 多篇文章。原因在于 .glob() 只在给定目录内搜索,不会搜索其子目录。

我们可以通过递归搜索来解决这个问题,需要使用语法相似的 rglob() 方法:

notebooks = articles_dir.rglob("*.ipynb")

print(len(list(notebooks)))
357

这一次,我们的代码找到了全部 357 个文件。

处理文件

如前所述,Path 对象只表示文件本身,并不对其执行操作。但它们确实提供了一些常见文件操作的方法。本节将介绍如何使用这些方法。

读取文件

在许多 Python 应用中,读取文件内容是基础操作。pathlib 提供了便捷的速记方法来以文本或原始字节的形式读取文件。

read_text() 方法可让我们读取文本文件内容并关闭文件。

file = Path("file.txt")

print(file.read_text())
This is sample text.

对于二进制文件,我们可以改用 read_bytes() 方法。

image = Path("images/midjourney.png")

image.read_bytes()[:10]
b'\x89PNG\r\n\x1a\n\x00\x00'

请记住,使用 read_* 方法时,错误处理很重要:

nonexistent_file = Path("gibberish.txt")

try:
   contents = nonexistent_file.read_text()
except FileNotFoundError:
   print("No such thing.")
No such thing.

写入文件

写文件与读文件一样简单。写文件我们使用 write_text() 方法。

file = Path("file.txt")

file.write_text("This is new text.")
17
file.read_text()
'This is new text.'

如我们所见,write_text() 会覆盖文本。尽管 write_text() 没有追加模式,但我们可以结合使用 read_text() 与 write_text(),在文件末尾追加文本。

old_text = file.read_text() + "\n"
final_text = "This is the final text."

# Combine old and new texts and write them back
file.write_text(old_text + final_text)

print(file.read_text())
This is new text.

This is the final text.

write_bytes() 的工作方式与之类似。为演示,我们先将 midjourney.png 图片复制为一个新名称。

original_image = Path("images/midjourney.png")

new_image = original_image.with_stem("duplicated_midjourney")

new_image
PosixPath('images/duplicated_midjourney.png')

with_stem() 方法返回一个主名(文件名不含扩展名)被替换后的文件路径(后缀保持不变)。这使我们可以读取原图像并将其内容写入新图像。 

new_image.write_bytes(original_image.read_bytes())
1979612

文件重命名与移动

除了使用 with_stem() 修改文件主名外,pathlib 还提供了 rename() 方法以更完整地重命名。 

file = Path("file.txt")

target_path = Path("new_file.txt")

file.rename(target_path)
PosixPath('new_file.txt')

rename() 接受目标路径,既可以是字符串,也可以是另一个路径对象。

要移动文件,可以使用 replace() 函数,它同样接受目标路径:

# Define the file to be moved
source_file = Path("new_file.txt")

# Define the location to put the file
destination = Path("data/new/location")

# Create the directories if they don't exist
destination.mkdir(parents=True)

# Move the file
source_file.replace(destination / source_file.name)
PosixPath('data/new/location/new_file.txt')

创建空白文件

pathlib 允许我们使用 touch 方法创建空白文件:

# Define new file path
new_dataset = Path("data/new.csv")

new_dataset.exists()
False
new_dataset.touch()

new_dataset.exists()
True

touch 方法最初用于更新文件的修改时间,因此也可用于已存在的文件。

original_image.touch()

当您需要预留一个稍后使用的文件名,但暂时没有任何内容可写入时,可以使用 touch 创建一个空文件。该方法的灵感来自 Unix 的 touch 终端命令。

权限与文件系统信息

最后,我们来学习如何使用 .stat() 方法获取文件特征。如果您熟悉 os,会注意到该方法的输出与 os.stat() 相同。

image_stats = original_image.stat()

image_stats
os.stat_result(st_mode=33188, st_ino=1950175, st_dev=2080, st_nlink=1, st_uid=1000, st_gid=1000, st_size=1979612, st_atime=1714664562, st_mtime=1714664562, st_ctime=1714664562)

我们也可以通过点号语法获取文件大小。

image_size = image_stats.st_size

# File size in megabytes
image_size / (1024**2)
1.8879051208496094

结语

Python 3.4 引入的 pathlib 模块大幅简化了开发者对文件系统的操作。通过以面向对象的方式处理文件路径,pathlib 提供了结构化且直观的路径表示方式。同时 pathlib 具有跨平台特性,可在不同操作系统间一致地处理路径分隔符,从而避免代码在新机器上“翻车”。最后,正如我们所见,pathlib 还为常见的文件系统操作提供了大量简洁而富于表达力的方法。 

请记住,pathlib 只是 Python 众多强大内置库中的一个。通过学习我们的 Python 数据科学家职业路径、Python 编程技能路径以及数据科学入门之 Python课程,您将掌握广泛的内置库,成长为出色的 Python 程序员。

感谢您的阅读!


Bexruz (Bex) Tuychiev

作者

Bexruz (Bex) Tuychiev

我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。 

主题