Skip to main content
Septvean's Documents
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

pathlib.Path

一、什么是 pathlib

pathlib 是 Python 标准库中的路径处理模块。

导入:

from pathlib import Path

传统路径经常使用字符串:

file_path = "data/stocks/600519.json"

使用 Path

file_path = Path("data/stocks/600519.json")

Path 不只是保存路径字符串,还提供了大量文件和目录操作方法:

file_path.exists()
file_path.is_file()
file_path.read_text()
file_path.write_text()
file_path.rename()
file_path.unlink()

使用 Path 的优点:

  • 路径拼接更清晰
  • 自动适配 Windows、macOS 和 Linux
  • 文件读写更方便
  • 目录遍历更直观
  • 减少手动处理路径分隔符
  • 代码可读性更好

二、创建 Path 对象

1. 使用字符串创建

from pathlib import Path

file_path = Path("data.txt")

目录路径:

folder_path = Path("data")

多级路径:

file_path = Path(
    "data/stocks/600519.json"
)

2. 分段创建路径

Path 可以接收多个路径部分:

file_path = Path(
    "data",
    "stocks",
    "600519.json",
)

等价于:

file_path = Path(
    "data/stocks/600519.json"
)

3. 查看对象类型

在 macOS 或 Linux:

print(type(Path("data.txt")))

结果通常是:

<class 'pathlib.PosixPath'>

在 Windows:

<class 'pathlib.WindowsPath'>

平时只需要使用:

Path

不需要手动区分操作系统。


三、使用斜杠拼接路径

Path 重载了 / 运算符,可以用来拼接路径。

folder = Path("data")
file_path = folder / "stocks.json"

print(file_path)

结果:

data/stocks.json

多级拼接:

file_path = (
    Path("data")
    / "stocks"
    / "daily"
    / "600519.json"
)

推荐:

file_path = base_dir / "data" / "stocks.json"

不推荐手动拼接字符串:

file_path = (
    str(base_dir)
    + "/data/stocks.json"
)

手动使用 /\,容易出现跨平台问题。


四、当前目录和用户目录

1. 当前工作目录

current_dir = Path.cwd()

print(current_dir)

当前工作目录是程序运行时所在的目录。

它不一定是 Python 文件所在目录。

例如:

cd /Users/martin
python projects/demo/main.py

此时:

Path.cwd()

通常是:

/Users/martin

而不是:

/Users/martin/projects/demo

2. 用户主目录

home_dir = Path.home()

print(home_dir)

macOS 可能是:

/Users/martin

Windows 可能是:

C:\Users\martin

3. 展开波浪号

下面不会自动展开 ~

path = Path("~/Documents")

应使用:

path = Path(
    "~/Documents"
).expanduser()

结果类似:

/Users/martin/Documents

五、获取当前脚本目录

在普通 Python 文件中,可以使用:

BASE_DIR = Path(
    __file__
).resolve().parent

含义:

__file__     当前 Python 文件路径
resolve()    转换为规范化绝对路径
parent       获取所在目录

项目示例:

project/
├── data/
│   └── stocks.json
└── main.py

main.py

from pathlib import Path


BASE_DIR = Path(
    __file__
).resolve().parent

DATA_FILE = (
    BASE_DIR
    / "data"
    / "stocks.json"
)

print(DATA_FILE)

这种方式不受当前工作目录影响。

注意,在某些交互式环境中,可能不存在:

__file__

例如:

  • Python 交互解释器
  • 部分 Notebook 环境

这些环境通常使用:

Path.cwd()

六、绝对路径与相对路径

1. 判断绝对路径

path = Path(
    "/Users/martin/data.txt"
)

print(path.is_absolute())

Windows 示例:

path = Path(
    r"C:\Users\martin\data.txt"
)

2. absolute

path = Path("data.txt")

print(path.absolute())

absolute() 将相对路径转换成绝对路径。

但它不会完整解析:

  • ..
  • 符号链接
  • 路径规范化问题

3. resolve

path = Path(
    "data/../data/stocks.json"
)

resolved_path = path.resolve()

print(resolved_path)

resolve() 会:

  • 转为绝对路径
  • 处理 ...
  • 尽可能解析符号链接

默认:

path.resolve(strict=False)

即使目标不存在,也通常可以得到规范化路径。

要求路径必须存在:

path.resolve(strict=True)

不存在时会抛出:

FileNotFoundError

七、获取路径组成部分

file_path = Path(
    "data/stocks/600519.daily.json"
)

1. 文件名

print(file_path.name)

结果:

600519.daily.json

2. 主文件名

print(file_path.stem)

结果:

600519.daily

stem 只移除最后一个扩展名。


3. 最后一个扩展名

print(file_path.suffix)

结果:

.json

4. 所有扩展名

print(file_path.suffixes)

结果:

[
    ".daily",
    ".json",
]

另一个例子:

path = Path("archive.tar.gz")

print(path.stem)
print(path.suffix)
print(path.suffixes)

结果:

archive.tar
.gz
['.tar', '.gz']

5. 父目录

print(file_path.parent)

结果:

data/stocks

继续向上:

print(file_path.parent.parent)

结果:

data

6. 所有父目录

for parent in file_path.parents:
    print(parent)

7. 路径组成部分

print(file_path.parts)

结果类似:

(
    "data",
    "stocks",
    "600519.daily.json",
)

八、修改路径但不操作文件

这些方法只生成新路径,不会立即修改磁盘文件。

1. 修改文件名

path = Path(
    "data/stocks.txt"
)

new_path = path.with_name(
    "new_stocks.txt"
)

print(new_path)

结果:

data/new_stocks.txt

2. 修改主文件名

new_path = path.with_stem(
    "stock_list"
)

结果:

data/stock_list.txt

3. 修改扩展名

new_path = path.with_suffix(
    ".csv"
)

结果:

data/stocks.csv

去掉扩展名:

new_path = path.with_suffix("")

4. 生成同目录新文件

source = Path(
    "data/stocks.json"
)

backup = source.with_name(
    f"{source.stem}_backup"
    f"{source.suffix}"
)

print(backup)

结果:

data/stocks_backup.json

九、判断路径状态

1. 判断路径是否存在

path = Path("data.txt")

if path.exists():
    print("路径存在")

2. 判断是否为文件

if path.is_file():
    print("这是文件")

3. 判断是否为目录

folder = Path("data")

if folder.is_dir():
    print("这是目录")

4. 判断是否为符号链接

if path.is_symlink():
    print("这是符号链接")

5. 判断是否为挂载点

if path.is_mount():
    print("这是挂载点")

6. 判断是否相对于某目录

path = Path(
    "/Users/martin/project/data/a.txt"
)

base = Path(
    "/Users/martin/project"
)

print(
    path.is_relative_to(base)
)

结果:

True

十、创建目录

1. 创建单层目录

folder = Path("data")

folder.mkdir()

如果目录已经存在,会抛出:

FileExistsError

2. 目录存在时不报错

folder.mkdir(
    exist_ok=True,
)

3. 创建多层目录

folder = Path(
    "data/stocks/daily"
)

folder.mkdir(
    parents=True,
    exist_ok=True,
)

参数含义:

parents=True
自动创建缺失的父目录

exist_ok=True
目录已经存在时不报错

项目中最常见的写法:

output_dir.mkdir(
    parents=True,
    exist_ok=True,
)

十一、创建空文件

file_path = Path("data.txt")

file_path.touch()

文件不存在时,会创建空文件。

文件已存在时,会更新文件时间。

防止覆盖或触碰已有文件:

file_path.touch(
    exist_ok=False,
)

文件存在时会抛出:

FileExistsError

十二、读取文本文件

1. read_text

file_path = Path("data.txt")

content = file_path.read_text(
    encoding="utf-8",
)

print(content)

返回值类型:

str

适合较小文本文件。


2. 使用 open 方法

with file_path.open(
    "r",
    encoding="utf-8",
) as file:
    content = file.read()

适合:

  • 按行读取
  • 大文件处理
  • 需要控制打开参数
  • 需要文件对象

3. 按行读取

with file_path.open(
    "r",
    encoding="utf-8",
) as file:
    for line in file:
        print(
            line.rstrip("\n")
        )

获取行号:

with file_path.open(
    "r",
    encoding="utf-8",
) as file:
    for line_number, line in enumerate(
        file,
        start=1,
    ):
        print(
            line_number,
            line.rstrip("\n"),
        )

十三、写入文本文件

1. write_text

file_path = Path("output.txt")

count = file_path.write_text(
    "Hello Python",
    encoding="utf-8",
)

print(count)

返回写入字符数量。

注意,write_text() 会覆盖原文件。


2. 写入多行

lines = [
    "第一行",
    "第二行",
    "第三行",
]

content = "\n".join(lines) + "\n"

file_path.write_text(
    content,
    encoding="utf-8",
)

3. 追加内容

Path.write_text() 不支持追加模式。

追加应使用:

with file_path.open(
    "a",
    encoding="utf-8",
) as file:
    file.write("新增内容\n")

十四、二进制文件

1. 读取二进制

image_path = Path("image.jpg")

data = image_path.read_bytes()

print(type(data))

结果:

<class 'bytes'>

2. 写入二进制

target_path = Path(
    "image_copy.jpg"
)

target_path.write_bytes(data)

适合较小二进制文件。

大文件应使用分块读取。


3. 分块读取大文件

def read_chunks(
    file_path: Path,
    *,
    chunk_size: int = 1024 * 1024,
):
    with file_path.open(
        "rb"
    ) as file:
        while chunk := file.read(
            chunk_size
        ):
            yield chunk

使用:

for chunk in read_chunks(
    Path("large_file.bin")
):
    print(len(chunk))

十五、遍历目录

1. iterdir

folder = Path("data")

for path in folder.iterdir():
    print(path)

只遍历当前目录中的直接子项。

不会递归进入子目录。

区分文件和目录:

for path in folder.iterdir():
    if path.is_file():
        print("文件:", path)
    elif path.is_dir():
        print("目录:", path)

2. glob

查找当前目录中的 TXT 文件:

for file_path in folder.glob(
    "*.txt"
):
    print(file_path)

匹配 JSON:

folder.glob("*.json")

匹配固定开头:

folder.glob(
    "stock_*.csv"
)

3. rglob

递归查找:

for file_path in folder.rglob(
    "*.json"
):
    print(file_path)

会搜索当前目录及所有子目录。


4. 双星号递归

for file_path in folder.glob(
    "**/*.json"
):
    print(file_path)

通常下面更清晰:

folder.rglob("*.json")

十六、Python 3.12 的 Path.walk

Python 3.12 为 Path 增加了 walk() 方法。

folder = Path("data")

for root, directories, files in (
    folder.walk()
):
    print("当前目录:", root)
    print("子目录:", directories)
    print("文件:", files)

其中:

root         当前 Path 目录
directories  当前目录下的子目录名称列表
files        当前目录下的文件名称列表

注意,directoriesfiles 中通常是名称字符串,不是完整 Path

组合完整路径:

for root, directories, files in (
    folder.walk()
):
    for file_name in files:
        file_path = (
            root
            / file_name
        )

        print(file_path)

1. 自上而下遍历

默认:

folder.walk(
    top_down=True
)

先处理父目录,再处理子目录。


2. 自下而上遍历

for root, directories, files in (
    folder.walk(
        top_down=False
    )
):
    print(root)

先处理最深层子目录。

删除目录树时,自下而上更有用。


3. 过滤目录

for root, directories, files in (
    folder.walk()
):
    directories[:] = [
        name
        for name in directories
        if name not in {
            ".git",
            "__pycache__",
            ".venv",
        }
    ]

    for file_name in files:
        print(root / file_name)

必须使用:

directories[:]

原地修改列表,才能影响后续递归。


十七、路径排序

files = sorted(
    Path("data").glob("*.txt")
)

按文件名:

files = sorted(
    Path("data").glob("*.txt"),
    key=lambda path: path.name,
)

忽略大小写:

files = sorted(
    Path("data").glob("*"),
    key=lambda path: (
        path.name.lower()
    ),
)

按文件大小:

files = sorted(
    Path("data").glob("*"),
    key=lambda path: (
        path.stat().st_size
    ),
)

按修改时间:

files = sorted(
    Path("data").glob("*"),
    key=lambda path: (
        path.stat().st_mtime
    ),
    reverse=True,
)

十八、获取文件信息

file_path = Path("data.txt")

info = file_path.stat()

1. 文件大小

print(info.st_size)

单位是字节。


2. 修改时间

print(info.st_mtime)

这是时间戳。

转换为日期时间:

from datetime import datetime


modified_time = datetime.fromtimestamp(
    info.st_mtime
)

print(modified_time)

3. 访问时间

print(info.st_atime)

4. 状态变化时间

print(info.st_ctime)

注意,st_ctime 在不同操作系统上的含义可能不同:

  • Windows:通常接近创建时间
  • Unix/macOS:通常表示元数据状态变化时间

不能统一把它理解为创建时间。


5. 格式化文件大小

def format_size(
    size: int,
) -> str:
    units = [
        "B",
        "KB",
        "MB",
        "GB",
        "TB",
    ]

    value = float(size)

    for unit in units:
        if value < 1024:
            return (
                f"{value:.2f} {unit}"
            )

        value /= 1024

    return f"{value:.2f} PB"

使用:

size = file_path.stat().st_size

print(format_size(size))

十九、生成相对路径

base = Path(
    "/Users/martin/project"
)

file_path = Path(
    "/Users/martin/project/data/a.txt"
)

relative_path = file_path.relative_to(
    base
)

print(relative_path)

结果:

data/a.txt

如果目标不在 base 内,会抛出:

ValueError

安全使用:

if file_path.is_relative_to(base):
    relative_path = (
        file_path.relative_to(base)
    )

二十、比较路径

1. 普通相等比较

path1 = Path(
    "data/stocks.json"
)

path2 = Path(
    "data/stocks.json"
)

print(path1 == path2)

结果:

True

这比较的是路径表示。


2. samefile

判断两个路径是否指向同一个实际文件:

path1 = Path("data.txt")
path2 = Path("./data.txt")

print(
    path1.samefile(path2)
)

路径必须存在。

samefile() 可以识别:

  • 不同相对路径
  • 符号链接
  • 硬链接
  • 同一实际文件

二十一、重命名文件或目录

old_path = Path("old.txt")
new_path = Path("new.txt")

result_path = old_path.rename(
    new_path
)

print(result_path)

可以同时移动到另一个目录:

source = Path("data.txt")
target = Path(
    "archive/data.txt"
)

target.parent.mkdir(
    parents=True,
    exist_ok=True,
)

source.rename(target)

不同文件系统之间移动时,rename() 可能失败。

复杂移动建议使用:

shutil.move()

二十二、replace

source = Path("new.txt")
target = Path("old.txt")

source.replace(target)

replace() 通常允许覆盖已经存在的目标文件。

适合:

  • 临时文件替换正式文件
  • 安全写入后的最终替换
  • 明确允许覆盖目标的场景

二十三、复制和移动

Python 3.12 中,复制和复杂移动通常配合 shutil

from pathlib import Path
import shutil

1. 复制文件

source = Path("data.txt")
target = Path(
    "backup/data.txt"
)

target.parent.mkdir(
    parents=True,
    exist_ok=True,
)

shutil.copy2(
    source,
    target,
)

copy2() 会尽可能保留文件元数据。


2. 移动文件

shutil.move(
    source,
    target,
)

3. 复制目录

shutil.copytree(
    Path("source_folder"),
    Path("target_folder"),
)

目标目录允许已经存在:

shutil.copytree(
    source,
    target,
    dirs_exist_ok=True,
)

二十四、删除文件和目录

1. 删除文件

file_path = Path("data.txt")

file_path.unlink()

文件不存在时:

FileNotFoundError

忽略不存在:

file_path.unlink(
    missing_ok=True
)

2. 删除空目录

folder = Path("empty_folder")

folder.rmdir()

目录必须为空。


3. 删除非空目录

import shutil

shutil.rmtree(
    Path("output")
)

这是破坏性操作,会递归删除所有内容。

建议加入严格检查:

def safe_remove_tree(
    folder: Path,
) -> None:
    resolved = folder.resolve()

    forbidden = {
        Path.home().resolve(),
        Path("/").resolve(),
    }

    if resolved in forbidden:
        raise ValueError(
            f"禁止删除危险目录:"
            f"{resolved}"
        )

    if resolved.exists():
        shutil.rmtree(resolved)

二十五、符号链接

1. 创建符号链接

target = Path("data.txt")
link = Path("data_link.txt")

link.symlink_to(target)

目录符号链接:

link.symlink_to(
    target_folder,
    target_is_directory=True,
)

Windows 创建符号链接可能需要额外权限。


2. 判断符号链接

print(link.is_symlink())

3. 读取符号链接目标

print(link.readlink())

二十六、硬链接

source = Path("data.txt")
hard_link = Path(
    "data_hard_link.txt"
)

hard_link.hardlink_to(source)

硬链接和原文件指向相同底层文件数据。

删除其中一个名称,不一定会删除实际数据,只要仍有其他硬链接存在。


二十七、文件权限

1. 修改权限

path = Path("script.sh")

path.chmod(0o755)

在 Unix/macOS 中:

755
所有者可读、写、执行
其他用户可读、执行

Windows 权限模型不同,行为可能有限。


2. 获取所有者

在支持的平台上:

print(path.owner())

获取所属组:

print(path.group())

这些方法在不同操作系统上的支持情况可能不同。


二十八、Path 与字符串转换

1. Path 转字符串

path = Path("data.txt")

text = str(path)

部分旧库只接受字符串路径:

legacy_function(
    str(path)
)

不过现代 Python 大多数文件相关函数都支持 Path


2. 使用 os.fspath

import os

path_text = os.fspath(path)

它返回系统可接受的路径表示。


3. 不要过早转换字符串

推荐:

path = (
    Path("data")
    / "stocks.json"
)

path.read_text(
    encoding="utf-8"
)

不推荐:

path = str(
    Path("data")
    / "stocks.json"
)

过早转字符串会失去 Path 的便利方法。


二十九、Windows 路径注意事项

Windows 路径包含反斜杠:

C:\Users\martin\data.txt

普通字符串中,反斜杠可能是转义符。

例如:

path = "C:\new\test.txt"

其中:

\n
\t

可能被解释为换行和制表符。

可以使用原始字符串:

path = Path(
    r"C:\new\test.txt"
)

或者使用正斜杠:

path = Path(
    "C:/new/test.txt"
)

更推荐分段:

path = Path(
    "C:/",
    "new",
    "test.txt",
)

三十、PurePath

Path 会访问实际文件系统。

如果只想处理路径字符串,而不访问磁盘,可以使用:

from pathlib import PurePath
path = PurePath(
    "data/stocks.json"
)

print(path.name)
print(path.stem)
print(path.suffix)
print(path.parent)

专门处理 Windows 路径:

from pathlib import PureWindowsPath

path = PureWindowsPath(
    r"C:\Users\martin\data.txt"
)

专门处理 POSIX 路径:

from pathlib import PurePosixPath

path = PurePosixPath(
    "/home/martin/data.txt"
)

适用场景:

  • 跨平台路径字符串转换
  • 不依赖当前操作系统
  • 处理远程系统路径
  • 只进行路径计算

普通文件操作直接使用:

Path

三十一、处理 JSON 文件

from pathlib import Path
import json

读取:

def read_json(
    file_path: Path,
) -> object:
    text = file_path.read_text(
        encoding="utf-8"
    )

    return json.loads(text)

写入:

def write_json(
    file_path: Path,
    data: object,
) -> None:
    file_path.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    text = json.dumps(
        data,
        ensure_ascii=False,
        indent=2,
    )

    file_path.write_text(
        text,
        encoding="utf-8",
    )

使用:

stocks = [
    {
        "code": "600519",
        "name": "贵州茅台",
    },
    {
        "code": "000001",
        "name": "平安银行",
    },
]

write_json(
    Path("data/stocks.json"),
    stocks,
)

三十二、处理 CSV 文件

from pathlib import Path
import csv

读取:

def read_csv(
    file_path: Path,
) -> list[dict[str, str]]:
    rows: list[
        dict[str, str]
    ] = []

    with file_path.open(
        "r",
        encoding="utf-8",
        newline="",
    ) as file:
        reader = csv.DictReader(file)

        for row in reader:
            rows.append(dict(row))

    return rows

写入:

def write_csv(
    file_path: Path,
    rows: list[
        dict[str, object]
    ],
    field_names: list[str],
) -> None:
    file_path.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    with file_path.open(
        "w",
        encoding="utf-8-sig",
        newline="",
    ) as file:
        writer = csv.DictWriter(
            file,
            fieldnames=field_names,
        )

        writer.writeheader()
        writer.writerows(rows)

三十三、批量重命名

例如,将 .jpeg 改为 .jpg

folder = Path("images")

for file_path in folder.glob(
    "*.jpeg"
):
    new_path = (
        file_path.with_suffix(
            ".jpg"
        )
    )

    file_path.rename(new_path)

1. 添加前缀

for file_path in folder.iterdir():
    if not file_path.is_file():
        continue

    new_path = file_path.with_name(
        f"backup_{file_path.name}"
    )

    file_path.rename(new_path)

2. 按顺序编号

files = sorted(
    folder.glob("*.jpg")
)

for index, file_path in enumerate(
    files,
    start=1,
):
    new_name = (
        f"image_{index:03d}"
        f"{file_path.suffix.lower()}"
    )

    new_path = (
        file_path.with_name(
            new_name
        )
    )

    print(
        file_path,
        "→",
        new_path,
    )

实际修改:

file_path.rename(new_path)

批量重命名建议先预览,不要立即修改。


三十四、避免文件名冲突

def get_unique_path(
    target: Path,
) -> Path:
    if not target.exists():
        return target

    index = 1

    while True:
        candidate = target.with_name(
            f"{target.stem}_{index}"
            f"{target.suffix}"
        )

        if not candidate.exists():
            return candidate

        index += 1

使用:

target = get_unique_path(
    Path("output/data.txt")
)

print(target)

可能得到:

output/data_1.txt

三十五、目录整理案例

按扩展名分类文件:

from pathlib import Path
import shutil
CATEGORIES = {
    "images": {
        ".jpg",
        ".jpeg",
        ".png",
        ".webp",
        ".gif",
    },
    "documents": {
        ".txt",
        ".pdf",
        ".docx",
        ".xlsx",
    },
    "audio": {
        ".mp3",
        ".wav",
        ".flac",
        ".m4a",
    },
    "video": {
        ".mp4",
        ".mov",
        ".mkv",
    },
}
def get_category(
    file_path: Path,
) -> str:
    suffix = (
        file_path.suffix.lower()
    )

    for category, suffixes in (
        CATEGORIES.items()
    ):
        if suffix in suffixes:
            return category

    return "others"
def organize_folder(
    folder: Path,
    *,
    dry_run: bool = True,
) -> None:
    if not folder.is_dir():
        raise NotADirectoryError(
            f"目录不存在:{folder}"
        )

    for file_path in list(
        folder.iterdir()
    ):
        if not file_path.is_file():
            continue

        category = get_category(
            file_path
        )

        target_dir = (
            folder
            / category
        )

        target_path = (
            target_dir
            / file_path.name
        )

        target_path = get_unique_path(
            target_path
        )

        print(
            f"{file_path.name} "
            f"→ {target_path}"
        )

        if dry_run:
            continue

        target_dir.mkdir(
            parents=True,
            exist_ok=True,
        )

        shutil.move(
            file_path,
            target_path,
        )

预览:

organize_folder(
    Path("downloads"),
    dry_run=True,
)

实际执行:

organize_folder(
    Path("downloads"),
    dry_run=False,
)

三十六、搜索目录中的文本

def search_text_files(
    folder: Path,
    keyword: str,
    *,
    pattern: str = "*.txt",
) -> list[
    tuple[Path, int, str]
]:
    results: list[
        tuple[Path, int, str]
    ] = []

    for file_path in folder.rglob(
        pattern
    ):
        try:
            with file_path.open(
                "r",
                encoding="utf-8",
            ) as file:
                for line_number, line in (
                    enumerate(
                        file,
                        start=1,
                    )
                ):
                    if keyword in line:
                        results.append(
                            (
                                file_path,
                                line_number,
                                line.rstrip(
                                    "\n"
                                ),
                            )
                        )
        except UnicodeDecodeError:
            continue

    return results

使用:

results = search_text_files(
    Path("notes"),
    "股票",
)

for path, line_number, line in results:
    print(
        path,
        line_number,
        line,
    )

三十七、寻找最新文件

def find_latest_file(
    folder: Path,
    pattern: str = "*",
) -> Path | None:
    files = [
        path
        for path in folder.glob(
            pattern
        )
        if path.is_file()
    ]

    if not files:
        return None

    return max(
        files,
        key=lambda path: (
            path.stat().st_mtime
        ),
    )

使用:

latest_file = find_latest_file(
    Path("data"),
    "*.xlsx",
)

print(latest_file)

三十八、统计目录大小

def calculate_directory_size(
    folder: Path,
) -> int:
    total = 0

    for path in folder.rglob("*"):
        if not path.is_file():
            continue

        try:
            total += (
                path.stat().st_size
            )
        except OSError:
            continue

    return total

使用:

size = calculate_directory_size(
    Path("data")
)

print(format_size(size))

三十九、安全写入文件

重要文件可以先写临时文件,再替换正式文件。

from pathlib import Path
import os
import tempfile
def safe_write_text(
    file_path: Path,
    content: str,
    *,
    encoding: str = "utf-8",
) -> None:
    file_path.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    with tempfile.NamedTemporaryFile(
        mode="w",
        encoding=encoding,
        dir=file_path.parent,
        delete=False,
    ) as temp_file:
        temp_file.write(content)

        temp_path = Path(
            temp_file.name
        )

    try:
        os.replace(
            temp_path,
            file_path,
        )
    except Exception:
        temp_path.unlink(
            missing_ok=True,
        )
        raise

适合:

  • 配置文件
  • JSON 数据
  • 交易记录
  • 重要计算结果
  • 不能接受部分写入的文件

四十、异常处理

常见文件异常:

FileNotFoundError
FileExistsError
PermissionError
IsADirectoryError
NotADirectoryError
UnicodeDecodeError
OSError

示例:

def read_text_file(
    file_path: Path,
) -> str:
    try:
        return file_path.read_text(
            encoding="utf-8"
        )
    except FileNotFoundError as error:
        raise FileNotFoundError(
            f"文件不存在:"
            f"{file_path}"
        ) from error
    except IsADirectoryError as error:
        raise ValueError(
            f"路径是目录:"
            f"{file_path}"
        ) from error
    except PermissionError as error:
        raise PermissionError(
            f"无权读取文件:"
            f"{file_path}"
        ) from error
    except UnicodeDecodeError as error:
        raise ValueError(
            f"文件不是 UTF-8 编码:"
            f"{file_path}"
        ) from error

四十一、常见错误

1. 误认为相对路径相对于脚本

Path("data/stocks.json")

通常相对于:

Path.cwd()

不是一定相对于脚本文件。

需要稳定项目路径时:

BASE_DIR = Path(
    __file__
).resolve().parent

2. 把 Path 当字符串拼接

错误:

path = Path("data")
path = path + "/stocks.json"

正确:

path = (
    Path("data")
    / "stocks.json"
)

3. 对目录调用 read_text

Path("data").read_text()

会抛出:

IsADirectoryError

可以先判断:

if path.is_file():
    content = path.read_text(
        encoding="utf-8"
    )

4. write_text 会覆盖文件

path.write_text(
    "新内容",
    encoding="utf-8",
)

会覆盖原内容。

追加内容应使用:

with path.open(
    "a",
    encoding="utf-8",
) as file:
    file.write("追加内容")

5. 批量删除没有预览

危险:

for path in folder.rglob("*"):
    path.unlink()

建议先输出:

for path in folder.rglob("*"):
    print("计划删除:", path)

并使用:

dry_run=True

6. 忽略大小写扩展名

只判断:

path.suffix == ".jpg"

无法匹配:

.JPG
.Jpg

推荐:

path.suffix.lower() == ".jpg"

7. 只判断 exists

if path.exists():

不能说明它是文件还是目录。

根据需求使用:

path.is_file()
path.is_dir()

8. 对不存在的路径调用 samefile

path1.samefile(path2)

要求路径实际存在。

否则可能抛出:

FileNotFoundError

四十二、Path 核心速查

创建路径:

path = Path("data.txt")

拼接路径:

path = (
    Path("data")
    / "stocks.json"
)

当前目录:

Path.cwd()

用户目录:

Path.home()

脚本目录:

Path(__file__).resolve().parent

展开用户目录:

Path("~/data").expanduser()

规范化绝对路径:

path.resolve()

路径信息:

path.name
path.stem
path.suffix
path.suffixes
path.parent
path.parents
path.parts

判断:

path.exists()
path.is_file()
path.is_dir()
path.is_symlink()
path.is_absolute()
path.is_relative_to(base)

创建:

path.mkdir(
    parents=True,
    exist_ok=True,
)

path.touch()

读取:

path.read_text(
    encoding="utf-8"
)

path.read_bytes()

写入:

path.write_text(
    content,
    encoding="utf-8"
)

path.write_bytes(data)

遍历:

folder.iterdir()
folder.glob("*.txt")
folder.rglob("*.txt")
folder.walk()

修改路径名称:

path.with_name("new.txt")
path.with_stem("new")
path.with_suffix(".json")

实际重命名:

path.rename(new_path)

替换:

path.replace(target)

删除文件:

path.unlink(
    missing_ok=True
)

删除空目录:

path.rmdir()

获取信息:

path.stat()

相对路径:

path.relative_to(base)

比较实际文件:

path1.samefile(path2)

四十三、练习题

练习一

创建目录:

data/stocks/daily

参考答案:

Path(
    "data/stocks/daily"
).mkdir(
    parents=True,
    exist_ok=True,
)

练习二

查找目录中所有 Excel 文件。

参考答案:

files = [
    path
    for path in Path(
        "data"
    ).rglob("*")
    if (
        path.is_file()
        and path.suffix.lower()
        in {
            ".xlsx",
            ".xls",
        }
    )
]

练习三

获取文件名、主名和扩展名。

path = Path(
    "data/report.xlsx"
)

print(path.name)
print(path.stem)
print(path.suffix)

练习四

把所有 .txt 文件复制到备份目录。

import shutil


source_dir = Path("data")
backup_dir = Path("backup")

backup_dir.mkdir(
    parents=True,
    exist_ok=True,
)

for file_path in source_dir.rglob(
    "*.txt"
):
    relative_path = (
        file_path.relative_to(
            source_dir
        )
    )

    target = (
        backup_dir
        / relative_path
    )

    target.parent.mkdir(
        parents=True,
        exist_ok=True,
    )

    shutil.copy2(
        file_path,
        target,
    )

练习五

找出目录中大于 10 MB 的文件。

limit = 10 * 1024 * 1024

large_files = [
    path
    for path in Path(
        "data"
    ).rglob("*")
    if (
        path.is_file()
        and path.stat().st_size
        > limit
    )
]

练习六

将文件扩展名统一转为小写。

for file_path in Path(
    "data"
).iterdir():
    if not file_path.is_file():
        continue

    suffix = (
        file_path.suffix
    )

    if not suffix:
        continue

    lower_suffix = suffix.lower()

    if suffix == lower_suffix:
        continue

    new_path = (
        file_path.with_suffix(
            lower_suffix
        )
    )

    print(
        file_path,
        "→",
        new_path,
    )

确认无冲突后执行:

file_path.rename(new_path)

四十四、核心总结

pathlib.Path 的本质是:

用对象表示路径
+
用对象方法操作文件系统

最常用的写法:

from pathlib import Path


BASE_DIR = Path(
    __file__
).resolve().parent

data_file = (
    BASE_DIR
    / "data"
    / "stocks.json"
)

小文件读写:

text = data_file.read_text(
    encoding="utf-8"
)

data_file.write_text(
    text,
    encoding="utf-8"
)

目录遍历:

for file_path in Path(
    "data"
).rglob("*.json"):
    print(file_path)

目录创建:

output_dir.mkdir(
    parents=True,
    exist_ok=True,
)

实际项目中应重点记住:

路径拼接使用 /
文本读写明确编码
项目路径不要依赖 cwd
大文件使用 open 逐行处理
复制移动配合 shutil
破坏性操作先预览
Python 3.12 可以使用 Path.walk

掌握 Path 后,文件操作代码会比传统字符串路径和 os.path 写法更加清晰、稳定和易维护。