24·模块六:文件与IO进阶

现代文件系统操作(pathlib 模块)

pathlibfile-system

第24课 - 现代文件系统操作(pathlib 模块)

学习目标

完成本课学习后,你将能够:

  1. 理解 pathlib 模块的核心优势,并能在新项目中优先选择它而非 os.path
  2. 熟练创建、拼接和操作路径对象,完成常见的文件系统任务
  3. 使用 pathlib 进行文件搜索、遍历和属性检查,高效管理文件资源
  4. 掌握 pathlib 与传统 os 模块的协作方法,实现平滑过渡

核心概念

想象一下,你每天都要整理一个巨大的文件柜。传统的 os.path 模块就像是一套工具箱,你每次操作文件(比如"找到A文件夹里的B文件")都需要从箱子里拿出不同的工具(函数)来组合使用。而 pathlib 模块则像是一个智能的文件柜管理机器人,它把路径本身当作一个对象,这个对象自带了"寻找"、"复制"、"查看信息"等所有功能。你只需要告诉这个"路径对象"你想做什么,它就知道该如何完成。

pathlib 的三大优势

  1. 面向对象:路径不是一个字符串,而是一个 Path 对象,拥有自己的方法和属性。
  2. 可读性强:代码更直观,更接近自然语言。
  3. 跨平台:自动处理 Windows(\)和 macOS/Linux(/)路径分隔符的差异。

快速对比:os.path vs pathlib

import os
from pathlib import Path

# 传统方式:拼接路径
old_path = os.path.join('documents', 'project', 'data.csv')
# pathlib方式:
new_path = Path('documents') / 'project' / 'data.csv'
print(old_path)  # documents/project/data.csv (在Unix上)
print(new_path)  # documents/project/data.csv

# 检查文件是否存在
os.path.exists(old_path)
new_path.exists()

# 获取文件名
os.path.basename(old_path)
new_path.name

看到区别了吗?pathlib 使用 / 运算符来拼接路径,非常直观,而且所有操作都是对 Path 对象的方法调用。

代码示例

让我们通过一个完整的例子来学习 pathlib 的核心功能。这个例子会创建一个模拟的项目目录结构,并演示如何操作它。

from pathlib import Path
import shutil

# 1. 创建一个Path对象
# Path() 可以接受一个或多个字符串参数,也可以接受一个现有的Path对象
base_dir = Path('my_project') # 相对路径
# 如果你想创建一个绝对路径,可以使用 resolve()
# absolute_path = base_dir.resolve()

print(f"创建的路径对象: {base_dir}")
print(f"这是绝对路径吗? {base_dir.is_absolute()}")

# 2. 创建目录和文件
# mkdir() 创建目录。parents=True 会创建所有必要的父目录(类似 mkdir -p)
base_dir.mkdir(parents=True, exist_ok=True)

# 使用 / 运算符拼接子路径,然后创建文件并写入内容
src_dir = base_dir / 'src'
src_dir.mkdir(exist_ok=True)

main_py = src_dir / 'main.py'
main_py.write_text("print('Hello, World!')\n", encoding='utf-8')

config_file = base_dir / 'config.json'
config_file.write_text('{"name": "demo"}\n', encoding='utf-8')

# 3. 遍历目录
print("\n遍历 'my_project' 目录:")
for item in base_dir.iterdir():
    # item 本身就是一个 Path 对象
    if item.is_file():
        print(f"  文件: {item.name} (大小: {item.stat().st_size} bytes)")
    elif item.is_dir():
        print(f"  目录: {item.name}/")

# 4. 搜索文件 (使用 glob 或 rglob)
print("\n查找所有 .json 文件:")
json_files = list(base_dir.glob('*.json'))  # 只在当前目录查找
for file in json_files:
    print(f"  {file}")

print("\n递归查找所有 .py 文件 (包含子目录):")
py_files = list(base_dir.rglob('*.py'))  # rglob 递归查找
for file in py_files:
    print(f"  {file}")

# 5. 读取和修改文件
print(f"\n读取 {main_py} 的内容:")
content = main_py.read_text(encoding='utf-8')
print(content)

# 重命名文件
new_main_py = src_dir / 'app.py'
main_py.rename(new_main_py)
print(f"文件已重命名为: {new_main_py}")

# 6. 其他有用的属性和方法
print(f"\n文件 {new_main_py} 的属性:")
print(f"  文件名: {new_main_py.name}")            # app.py
print(f"  文件后缀: {new_main_py.suffix}")         # .py
print(f"  不带后缀的文件名: {new_main_py.stem}")   # app
print(f"  父目录: {new_main_py.parent}")           # my_project/src

# 7. 清理:删除我们创建的所有内容
# 注意:在真实项目中要非常小心地使用删除操作!
print("\n正在清理创建的模拟目录...")
# 使用 shutil.rmtree 来删除整个目录树
shutil.rmtree(base_dir)
print("清理完成。")

运行结果示例 (输出顺序可能略有不同):

创建的路径对象: my_project
这是绝对路径吗? False

遍历 'my_project' 目录:
  文件: config.json (大小: 19 bytes)
  目录: src/

查找所有 .json 文件:
  my_project/config.json

递归查找所有 .py 文件 (包含子目录):
  my_project/src/main.py

读取 my_project/src/main.py 的内容:
print('Hello, World!')

文件已重命名为: my_project/src/app.py

文件 my_project/src/app.py 的属性:
  文件名: app.py
  文件后缀: .py
  不带后缀的文件名: app
  父目录: my_project/src

正在清理创建的模拟目录...
清理完成。

实践练习

练习 1:路径创建与文件写入(初级)

要求:使用 pathlib,在当前工作目录下创建一个名为 practice_24 的目录,在该目录中创建一个 notes.txt 文件,并写入今天的日期。 预期输出:目录和文件被成功创建,文件内容为类似 2023-10-27 的日期字符串。

练习 2:文件整理模拟器(中级)

要求:给定一个包含多种类型文件(如 .jpg, .txt, .docx, .pdf)的文件夹路径(你可以手动创建几个测试文件)。编写一个函数,使用 pathlib 遍历该文件夹,并将文件移动到以它们扩展名命名的子文件夹中(例如,所有 .jpg 文件移动到 images 子目录,.txt 移动到 texts 子目录)。 预期输出

移动 picture1.jpg -> ./images/picture1.jpg
移动 report.txt -> ./texts/report.txt
移动 essay.docx -> ./documents/essay.docx

练习 3:项目文件扫描仪(高级)

要求:编写一个程序,扫描一个指定的项目目录(例如,包含代码和文档的目录),生成一份简单的报告。报告应列出:

  1. 总文件数和总目录数。
  2. 按文件扩展名分类的文件数量统计(例如:.py: 15, .txt: 3)。
  3. 找到的最大和最小的文件(按文件大小)的路径和大小。 预期输出(格式自拟):
=== 项目扫描报告 ===
根目录: /path/to/your/project
总文件数: 25
总目录数: 8
文件类型统计:
  .py: 15
  .md: 5
  .txt: 5
最大文件: /path/to/your/project/data/big_file.bin (1024 KB)
最小文件: /path/to/your/project/config.ini (12 bytes)

常见错误

  1. 混淆相对路径与绝对路径

    p = Path('file.txt')  # 这是相对路径,相对于当前工作目录
    p.absolute()          # 这会将其转换为绝对路径,但不改变原对象
    p = p.resolve()       # resolve() 也能处理符号链接,更推荐用于获取绝对路径
    

    建议:在需要明确位置时,使用 resolve() 确保路径的确定性。

  2. 忘记检查文件/目录是否存在

    p = Path('nonexistent_file.txt')
    # p.read_text()  # 这会直接引发 FileNotFoundError
    if p.exists():
        content = p.read_text()
    else:
        print("文件不存在")
    

    建议:在进行读取、删除等操作前,养成使用 exists()is_file()is_dir() 检查的习惯。

  3. 路径拼接时使用字符串加法

    p = Path('folder')
    wrong_path = p + '/file.txt'  # 错误!会导致 TypeError
    correct_path = p / 'file.txt' # 正确!使用 / 运算符
    

    建议:始终使用 / 运算符或 Pathjoinpath() 方法。

  4. 编码问题

    p = Path('file_with_chinese.txt')
    # p.read_text()  # 如果文件编码不是UTF-8,可能会失败
    p.read_text(encoding='gbk')  # 指定正确的编码
    

    建议:读写文本文件时,明确指定 encoding 参数。

  5. 递归操作时的风险

    # 删除当前目录下所有名为 'temp' 的目录(非常危险!)
    for temp_dir in Path('.').rglob('temp'):
        if temp_dir.is_dir():
            shutil.rmtree(temp_dir)  # 极其危险,可能误删重要数据
    

    建议:递归操作前先打印出所有将要受影响的路径,确认无误。考虑使用 dry-run 模式。

小结

  1. 核心思想pathlib 将文件系统路径视为 对象,而非字符串。这带来了更优雅、更 Pythonic 的代码风格。
  2. 关键操作
    • 创建与拼接Path('base') / 'sub' / 'file.ext'
    • 目录操作mkdir(), iterdir(), rmdir() (删除空目录)
    • 文件操作read_text(), write_text(), read_bytes(), write_bytes()
    • 检查与查询exists(), is_file(), is_dir(), name, suffix, stat()
    • 搜索glob('pattern') (非递归), rglob('pattern') (递归)
  3. 最佳实践:对于新项目,优先使用 pathlib。它与 shutil(用于复制、移动整个目录树)等标准库配合良好,是现代 Python 文件操作的首选。
  4. 下一步预告:在掌握了如何用 pathlib 这个"智能工具"操作文件系统后,我们将进入 面向对象编程 的世界。你会更深刻地理解 Path 本身就是类的一个实例,而你将学习如何创建自己的类来构建更复杂的程序。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「面向对象编程:类与对象」 以巩固所学知识。