Python 文件和目录路径操作

PythonBeginner
立即练习

介绍

在这个 Lab 中,你将学习如何使用现代的 pathlib 模块来处理 Python 中的文件和目录路径。虽然较旧的 os.path 模块仍然很常见,但 pathlib 提供了一种面向对象的方法,使路径操作更加直观和易读。你将学习如何以一种适用于 Windows、macOS 和 Linux 等不同操作系统的方式来创建、检查、验证和操作路径、文件和目录。

完成本 Lab 后,你将熟练掌握以下技能:

  • 正确地连接路径组件。
  • 创建和管理目录。
  • 区分绝对路径和相对路径。
  • 验证路径的存在性和类型。
  • 执行常见的文件操作,如复制、移动和删除。
  • 遍历目录树以查找文件和文件夹。

基本路径操作

在这一步,你将学习使用 pathlib 模块进行路径操作的基础知识。这包括连接路径组件、获取当前工作目录以及创建新目录。

pathlib 模块提供了一种优雅的、跨平台的方式来处理文件系统路径。在 Windows 上,路径使用反斜杠 (\);而在类 Unix 系统(Linux, macOS)上,它们使用正斜杠 (/)。使用 pathlib 可以确保你的代码在任何平台上都能正常工作,无需手动调整。

注意:如果你不熟悉使用 WebIDE(如 VS Code),请先在 这个 Lab 学习。

首先,我们来看看如何连接路径组件。/ 运算符是 joinpath() 的一个直观的快捷方式。

  1. 从左侧的文件浏览器中打开 main.py 文件。
  2. 将 main.py 的内容替换为以下代码,以连接路径组件并打印结果。
from pathlib import Path

## 使用 / 运算符连接路径组件
my_path = Path('usr') / 'bin' / 'spam'
print(my_path)
  1. 从终端运行脚本:
python3 main.py

你将看到以下输出,它演示了 pathlib 如何正确地形成一个类 Unix 风格的路径:

usr/bin/spam

接下来,我们使用 Path.cwd() 来获取当前工作目录。

  1. 将 main.py 的内容替换为以下代码(这将替换掉之前的代码):
from pathlib import Path

## 获取当前工作目录
cwd = Path.cwd()
print(cwd)
  1. 再次运行脚本:
python3 main.py

输出将显示当前工作目录,在此环境中是 /home/labex/project:

/home/labex/project

最后,我们来创建一个新目录。如果你尝试在父目录不存在的情况下创建目录,会引发 FileNotFoundError。为避免这种情况,请使用 mkdir(parents=True),它会创建所有必需的父目录。

  1. 将 main.py 的内容替换为以下代码,以创建一个嵌套的目录结构:
from pathlib import Path

## 获取当前工作目录
cwd = Path.cwd()

## 定义新嵌套目录的路径
new_dirs = cwd / 'delicious' / 'walnut' / 'waffles'

## 创建目录,包括任何缺失的父目录
new_dirs.mkdir(parents=True, exist_ok=True)

print(f"Successfully created: {new_dirs}")

exist_ok=True 参数可以防止目录已存在时引发错误。这意味着你可以多次运行脚本而不会出错。

  1. 运行脚本:
python3 main.py

脚本将创建嵌套的目录并打印确认信息:

Successfully created: /home/labex/project/delicious/walnut/waffles

你可以通过在终端中运行 ls -R delicious 来验证目录是否创建成功。

绝对路径与相对路径

在这一步,你将学习区分和使用绝对路径与相对路径。

  • 绝对路径 (absolute path) 从文件系统的根目录开始(例如,Linux 上的 / 或 Windows 上的 C:\)。
  • 相对路径 (relative path) 是从当前工作目录定义的。

pathlib 提供了检查路径是否为绝对路径、将路径解析为其绝对形式以及从起始点查找相对路径的方法。

首先,我们使用 is_absolute() 来检查路径是否为绝对路径。

  1. 将 main.py 的内容替换为以下代码:
from pathlib import Path

## 检查路径是否为绝对路径
print(f"Path('/') is absolute: {Path('/').is_absolute()}")
print(f"Path('..') is absolute: {Path('..').is_absolute()}")
  1. 运行脚本:
python3 main.py

输出确认 / 是一个绝对路径,而 ..(父目录)是相对路径。

Path('/') is absolute: True
Path('..') is absolute: False

接下来,你可以使用 resolve() 将相对路径转换为其完整的绝对路径形式。这对于将 .(当前目录)或 ..(父目录)等相对路径转换为其完整、明确的形式非常有用。

  1. 将 main.py 的内容替换为以下代码:
from pathlib import Path

## 将相对路径解析为其绝对形式
## '..' 指向当前工作目录的父目录
resolved_path = Path('..').resolve()

print(f"Current working directory: {Path.cwd()}")
print(f"Resolved path of '..': {resolved_path}")
  1. 执行脚本:
python3 main.py

输出将显示当前目录及其父目录的解析后的绝对路径。

Current working directory: /home/labex/project
Resolved path of '..': /home/labex

最后,你可以使用 relative_to() 从一个路径计算到另一个路径的相对路径。

  1. 将 main.py 的内容替换为以下代码,以计算相对路径:
from pathlib import Path

## 从一个起始点获取相对路径
## 这显示了相对于根目录 '/' 的 '/etc/passwd' 的路径
relative_path = Path('/etc/passwd').relative_to('/')
print(f"Path to '/etc/passwd' relative to '/': {relative_path}")
  1. 运行脚本:
python3 main.py

输出显示相对于根目录的路径:

Path to '/etc/passwd' relative to '/': etc/passwd

路径验证与检查

在这一步,你将学习如何检查路径的属性,例如它是否存在,以及它是一个文件还是一个目录。你还将学习如何检索文件元数据,比如文件大小。

一个常见的任务是在尝试访问文件或目录之前验证其是否存在。exists() 方法非常适合这个目的。

  1. 打开 main.py 并将其内容替换为以下代码。这将检查当前目录 (.)、一个文件 (setup.py) 和一个不存在的文件是否存在。
from pathlib import Path

## 检查路径是否存在
print(f"Path('.') exists: {Path('.').exists()}")
print(f"Path('setup.py') exists: {Path('setup.py').exists()}")
print(f"Path('nonexistentfile') exists: {Path('nonexistentfile').exists()}")
  1. 运行脚本:
python3 main.py

输出将确认哪些路径存在:

Path('.') exists: True
Path('setup.py') exists: True
Path('nonexistentfile') exists: False

你也可以更具体地使用 is_file() 检查路径是否指向一个文件,或使用 is_dir() 检查是否指向一个目录。

  1. 将 main.py 的内容替换为以下代码以执行这些检查:
from pathlib import Path

## 检查路径是否为文件
print(f"Path('setup.py').is_file(): {Path('setup.py').is_file()}")
print(f"Path('/home').is_file(): {Path('/home').is_file()}")

## 检查路径是否为目录
print(f"Path('/').is_dir(): {Path('/').is_dir()}")
print(f"Path('setup.py').is_dir(): {Path('setup.py').is_dir()}")
  1. 执行脚本:
python3 main.py

输出将正确识别每种路径的类型:

Path('setup.py').is_file(): True
Path('/home').is_file(): False
Path('/').is_dir(): True
Path('setup.py').is_dir(): False

最后,要获取详细的文件信息,例如文件大小,你可以使用 stat() 方法。它返回一个包含各种属性的对象,其中包括以字节为单位的文件大小 st_size。

  1. 将 main.py 的内容替换为以下代码,以获取系统文件的大小:
from pathlib import Path
import os

## 定义文件路径
file_path = Path('/bin/python3')

## 获取文件统计信息
stat_info = file_path.stat()

## 打印以字节为单位的大小
print(f"File: {file_path}")
print(f"Size: {stat_info.st_size} bytes")
  1. 运行脚本:
python3 main.py

输出将显示 Python 可执行文件的大小(以字节为单位)。确切大小可能会有所不同。

File: /bin/python3
Size: 10024 bytes

文件和文件夹管理

在这一步,你将学习如何使用 shutil 模块结合 pathlib 来执行常见的文件系统操作,例如复制、移动、重命名和删除文件和文件夹。

shutil 模块为文件操作提供了高级函数。我们从复制文件开始。shutil.copy() 将单个文件复制到新的位置。

  1. 从左侧的文件浏览器中打开 manage_files.py 文件。
  2. 将 manage_files.py 的内容替换为以下代码。这将把 spam.txt 复制到 /tmp/delicious 目录中。
import shutil
from pathlib import Path

## 定义源和目标
source_file = Path('/tmp/spam.txt')
destination_dir = Path('/tmp/delicious')

## 复制文件
shutil.copy(source_file, destination_dir)

print(f"Copied '{source_file}' to '{destination_dir}'")

## 你也可以在复制时重命名文件
shutil.copy('/tmp/eggs.txt', '/tmp/delicious/eggs2.txt')
print("Copied and renamed 'eggs.txt' to 'eggs2.txt'")
  1. 运行脚本:
python3 manage_files.py

要复制整个目录树,请使用 shutil.copytree()。

  1. 将以下行添加到 manage_files.py 的末尾(追加到现有代码之后):
## 复制整个目录树
shutil.copytree('/tmp/bacon', '/tmp/bacon_backup')
print("Copied directory '/tmp/bacon' to '/tmp/bacon_backup'")

注意: 如果你多次运行此脚本,shutil.copytree() 将会失败,因为目标目录已经存在。这是预期的行为。目前,只需运行一次即可。

  1. 再次运行脚本以查看效果。

接下来,我们使用 shutil.move() 来移动和重命名文件。此函数可以移动文件到不同的目录或重命名它。

  1. 用以下代码替换 manage_files.py 的全部内容,以移动 bacon.txt:
import shutil
from pathlib import Path

## 如果目标目录不存在,则创建它
Path('/tmp/eggs').mkdir(parents=True, exist_ok=True)

## 移动文件到目录(仅当源文件存在时)
bacon_source = Path('/tmp/bacon.txt')
if bacon_source.exists():
    shutil.move('/tmp/bacon.txt', '/tmp/eggs')
    print("Moved '/tmp/bacon.txt' to '/tmp/eggs'")
else:
    print("'/tmp/bacon.txt' not found (may have been moved already)")

## 移动并重命名文件
## 首先,我们重新创建要移动的文件
Path('/tmp/bacon.txt').touch()
shutil.move('/tmp/bacon.txt', '/tmp/eggs/new_bacon.txt')
print("Moved and renamed '/tmp/bacon.txt' to '/tmp/eggs/new_bacon.txt'")

注意: 代码在移动文件之前会检查源文件是否存在,因此你可以安全地多次运行此脚本。第二部分将始终创建新文件并将其移动。

  1. 运行脚本。

最后,我们来看一下删除文件和文件夹的操作。

  • Path.unlink() 删除单个文件。
  • Path.rmdir() 删除空目录。
  • shutil.rmtree() 删除目录及其所有内容。
  1. 将 manage_files.py 的全部内容替换为以下内容:
from pathlib import Path
import shutil

## 创建一个要删除的文件
file_to_delete = Path('/tmp/deleteme.txt')
file_to_delete.touch()
print(f"Created '{file_to_delete}'")

## 删除文件
file_to_delete.unlink()
print(f"Deleted '{file_to_delete}'")

## 创建一个要删除的目录
dir_to_delete = Path('/tmp/temp_dir')
dir_to_delete.mkdir(exist_ok=True)
## rmdir() 只对空目录有效
dir_to_delete.rmdir()
print(f"Deleted empty directory '{dir_to_delete}'")

## 使用 shutil.rmtree() 删除目录及其所有内容
## 注意:如果目录不存在,这将失败,但对于此示例来说没问题
if Path('/tmp/bacon_backup').exists():
    shutil.rmtree('/tmp/bacon_backup')
    print("Deleted directory '/tmp/bacon_backup' and all its contents")
else:
    print("Directory '/tmp/bacon_backup' does not exist (may have been deleted already)")

注意: 如果你多次运行此脚本,某些操作可能会失败,因为文件或目录已被删除。这是正常的,它表明这些操作不是幂等的(不能安全地重复执行)。

  1. 运行脚本以执行删除操作。

目录遍历

在这一步,你将学习如何列出目录的内容并递归地遍历目录树。

要列出目录的内容,你可以使用 iterdir() 方法,它会返回一个包含路径中所有项(文件和目录)的迭代器。

  1. 首先,我们设置一个示例目录结构。在终端中运行此命令:
mkdir -p ~/project/data/level1 && touch ~/project/data/file0.txt ~/project/data/level1/file1.txt
  1. 现在,打开 main.py 并用以下代码替换其内容,以列出 /usr/bin 目录的内容。
from pathlib import Path

## 列出目录内容
print("Contents of '/usr/bin':")
count = 0
for f in Path('/usr/bin').iterdir():
    if count < 5: ## 仅打印前 5 个项以保持输出简短
        print(f)
        count += 1
    else:
        break
  1. 运行脚本:
python3 main.py

输出将显示 /usr/bin 中前几个文件和目录。确切的列表可能会有所不同。

Contents of '/usr/bin':
/usr/bin/[
/usr/bin/addpart
/usr/bin/apt
/usr/bin/apt-cache
/usr/bin/apt-cdrom

你可以将 iterdir() 与 stat() 结合使用,以计算目录中所有文件的大小总和。

  1. 将 main.py 的内容替换为以下代码。请注意,此代码仅累加直接子项的大小,不会递归到子目录中。
from pathlib import Path

total_size = 0
directory = Path('/usr/bin')

for sub_path in directory.iterdir():
    if sub_path.is_file(): ## 确保只累加文件的大小
        total_size += sub_path.stat().st_size

print(f"Total size of files in '{directory}': {total_size} bytes")
  1. 执行脚本。它将打印出直接位于 /usr/bin 中所有文件的大小总和。

对于更复杂的任务,你可能需要“遍历”(walk)整个目录树,访问所有子目录和文件。rglob() 方法非常适合此任务。它会递归地查找所有匹配特定模式的路径。使用 * 作为模式可以匹配所有内容。

  1. 首先,为本示例创建一个更复杂的目录结构。在终端中运行此命令:
mkdir -p /tmp/delicious/cats /tmp/delicious/walnut/waffles
touch /tmp/delicious/spam.txt /tmp/delicious/cats/catnames.txt /tmp/delicious/walnut/waffles/butter.txt

注意:你可以安全地多次运行这些命令。如果目录已存在,mkdir -p 命令不会失败;如果文件已存在,touch 会更新文件时间戳。

  1. 将 main.py 的内容替换为以下代码,以遍历 /tmp/delicious 目录:
from pathlib import Path

p = Path('/tmp/delicious')
print(f"Walking directory tree for: {p}")

for i in p.rglob('*'):
    print(i)
  1. 运行脚本:
python3 main.py

输出将列出 /tmp/delicious 及其子目录中的每一个文件和文件夹,演示了 rglob() 如何遍历整个树。

Walking directory tree for: /tmp/delicious
/tmp/delicious/spam.txt
/tmp/delicious/eggs2.txt
/tmp/delicious/cats
/tmp/delicious/walnut
/tmp/delicious/cats/catnames.txt
/tmp/delicious/walnut/waffles
/tmp/delicious/walnut/waffles/butter.txt

总结

恭喜你完成了本次实验!你已经学会了如何使用现代的 pathlib 模块在 Python 中有效地管理文件和目录路径。

在本次实验中,你练习了:

  • 使用 Path 对象和 / 运算符以跨平台的方式创建和连接路径。
  • 获取当前工作目录,并使用 mkdir(parents=True) 创建嵌套目录。
  • 使用 is_absolute()、resolve() 和 relative_to() 区分绝对路径和相对路径,并在它们之间进行转换。
  • 使用 exists()、is_file() 和 is_dir() 验证路径,并使用 stat() 检查文件元数据。
  • 使用 shutil 模块和 pathlib 方法执行复制、移动和删除等基本文件管理任务。
  • 使用 iterdir() 列出目录内容,并使用 rglob() 递归查找项,从而遍历目录结构。

这些技能对于编写与文件系统交互的健壮且可维护的 Python 应用程序至关重要。