跳到主要内容

操作文件和目录

文件读写关注“文件里面有什么”,文件和目录操作则关注“文件在哪里、叫什么、如何组织”。现代 Python 推荐使用 pathlib.Path 表示路径,再用 shutil 完成复制、移动和目录树操作。

from pathlib import Path

report = Path("data") / "reports" / "summary.txt"
print(report.parent) # data/reports
print(report.name) # summary.txt

本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:

  • 使用 Path 构造和拆分跨平台路径;
  • 查询文件或目录是否存在、类型和元数据;
  • 创建、遍历、搜索、重命名和删除文件或目录;
  • 使用 shutil 复制、移动和删除目录树;
  • 处理常见异常,并降低误删、覆盖和路径越界的风险。

使用 Path 表示路径​

Path 对象表示一个文件系统路径。用 / 运算符拼接路径,能够自动采用当前操作系统的路径规则:

from pathlib import Path

base_dir = Path("project")
config_path = base_dir / "config" / "settings.json"

print(config_path)

不要通过 "project/" + name 手动拼接路径。除了容易遗漏分隔符,也不利于跨平台运行。

相对路径与绝对路径​

相对路径从当前工作目录开始解析:

from pathlib import Path

print(Path.cwd())
print(Path("data/report.txt").is_absolute()) # False

absolute() 可以得到绝对形式,resolve() 还会规范化 .、..,并通常解析符号链接:

from pathlib import Path

path = Path("data") / ".." / "output"

print(path.absolute())
print(path.resolve())

resolve() 默认使用 strict=False,路径尚未创建时也可以得到规范化结果;使用 resolve(strict=True) 则会在路径不存在时抛出 FileNotFoundError。是否应该解析符号链接取决于业务需求,不要只为了显示路径就随意改变它的含义。

用户主目录与环境变量​

expanduser() 可以展开 ~:

from pathlib import Path

config_dir = Path("~/.myapp").expanduser()

Path 不会自动展开 $HOME、%APPDATA% 等环境变量。确实需要读取环境变量时,使用 os.environ 明确取得值:

import os
from pathlib import Path

cache_dir = Path(os.environ["MYAPP_CACHE_DIR"])

环境变量可能不存在,生产代码应根据配置要求提供默认值或清晰报错。

拆分和修改路径​

路径可以在不访问文件系统的情况下拆分:

from pathlib import Path

path = Path("archive/reports/summary.tar.gz")

print(path.name) # summary.tar.gz
print(path.stem) # summary.tar
print(path.suffix) # .gz
print(path.suffixes) # ['.tar', '.gz']
print(path.parent) # archive/reports
print(path.parts) # ('archive', 'reports', 'summary.tar.gz')

常见修改方法会返回新对象,不会重命名磁盘上的文件:

from pathlib import Path

path = Path("reports/summary.txt")

print(path.with_name("detail.txt")) # reports/detail.txt
print(path.with_suffix(".csv")) # reports/summary.csv
print(path.parent / f"{path.stem}.bak")

Path 对象通常是不可变的。只有调用 rename()、unlink() 等文件系统方法时,磁盘内容才会改变。

查询路径状态​

from pathlib import Path

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

print(path.exists())
print(path.is_file())
print(path.is_dir())
print(path.is_symlink())
  • exists():路径指向的目标是否存在;
  • is_file():是否为普通文件;
  • is_dir():是否为目录;
  • is_symlink():路径本身是否为符号链接。

状态可能在检查后立即变化。例如,exists() 返回 True 后,另一个进程可能马上删除文件。因此,预检查可以改善提示,但真正操作时仍要处理异常。

读取元数据​

stat() 返回文件大小、时间和权限等元数据:

from datetime import datetime
from pathlib import Path

path = Path("data/report.txt")
info = path.stat()

print(info.st_size) # 字节数
print(datetime.fromtimestamp(info.st_mtime)) # 最后修改时间

常见属性包括:

属性含义
st_size文件大小,单位为字节
st_mtime最后修改时间的时间戳
st_atime最后访问时间的时间戳
st_mode文件类型与权限位

时间语义和精度由操作系统及文件系统决定。不要假设所有平台都能可靠记录完全相同的创建时间或访问时间。

创建文件和目录​

创建目录​

from pathlib import Path

output_dir = Path("output/reports")
output_dir.mkdir(parents=True, exist_ok=True)
  • parents=True:父目录不存在时一并创建;
  • exist_ok=True:目标目录已经存在时不报错。

如果同名路径存在但它是文件,即使设置 exist_ok=True 也会抛出 FileExistsError。

创建空文件​

from pathlib import Path

path = Path("output/ready.flag")
path.parent.mkdir(parents=True, exist_ok=True)
path.touch(exist_ok=True)

touch() 在文件不存在时创建空文件;文件已经存在时,默认会更新其修改时间。若不允许覆盖已有路径,使用 touch(exist_ok=False)。

写入文本或字节也能自动创建文件,但不会自动创建父目录:

from pathlib import Path

path = Path("output/message.txt")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("任务完成\n", encoding="utf-8")

查看和遍历目录​

遍历直接子项​

iterdir() 返回目录中的直接子项,顺序由文件系统决定:

from pathlib import Path

directory = Path("data")

for child in sorted(directory.iterdir(), key=lambda item: item.name):
kind = "目录" if child.is_dir() else "文件"
print(kind, child.name)

需要稳定顺序时应显式排序。文件名的大小写规则和排序效果可能因操作系统而异。

使用通配模式搜索​

glob() 按模式搜索,rglob() 递归搜索子目录:

from pathlib import Path

docs_dir = Path("docs")

for markdown_file in docs_dir.glob("*.md"):
print(markdown_file)

for python_file in docs_dir.rglob("*.py"):
print(python_file)

常见通配符:

模式含义
*匹配任意数量的字符
?匹配一个字符
**跨目录递归匹配

也可以组合筛选条件:

from pathlib import Path

large_logs = [
path
for path in Path("logs").rglob("*.log")
if path.is_file() and path.stat().st_size > 1024 * 1024
]

大型目录树中反复调用 stat() 可能较慢,应根据实际需求限制搜索范围。

重命名和移动​

rename() 与 replace()​

rename() 可以在同一文件系统内重命名或移动路径:

from pathlib import Path

source = Path("draft.txt")
target = Path("archive/final.txt")
target.parent.mkdir(parents=True, exist_ok=True)

source.rename(target)

目标已经存在时,rename() 的行为可能因操作系统而异。需要明确替换目标时使用 replace():

source.replace(target)

即使使用 replace(),替换目录、跨文件系统移动和被其他程序占用的文件也可能失败,应处理 OSError。如果不允许覆盖,应在业务层采用独占创建或其他并发控制;仅先调用 exists() 仍存在竞态条件。

使用 shutil.move()​

shutil.move() 可以移动文件或整个目录,并能在跨文件系统时退化为复制后删除:

from pathlib import Path
import shutil

source = Path("downloads/report.csv")
target_dir = Path("archive/2026")
target_dir.mkdir(parents=True, exist_ok=True)

new_path = shutil.move(source, target_dir / source.name)
print(new_path)

跨文件系统的移动不再是单一步骤,中途失败时需要检查源和目标状态。

复制文件和目录​

shutil 提供高层复制操作:

from pathlib import Path
import shutil

source = Path("data/report.txt")
backup = Path("backup/report.txt")
backup.parent.mkdir(parents=True, exist_ok=True)

shutil.copy2(source, backup)
函数用途
shutil.copyfile()只复制文件内容,源和目标都必须是文件路径
shutil.copy()复制内容和权限模式
shutil.copy2()尽可能再保留修改时间等元数据
shutil.copytree()递归复制整个目录树

没有一种跨平台复制能够保证保留所有所有者、ACL、扩展属性等元数据。对备份、部署等严格场景,应确认目标平台的实际要求。

复制目录树:

from pathlib import Path
import shutil

source_dir = Path("project-template")
target_dir = Path("workspace/my-project")

shutil.copytree(source_dir, target_dir)

目标目录默认必须不存在。Python 3.8 及以上版本可以设置 dirs_exist_ok=True,把内容复制到已经存在的目录中:

shutil.copytree(source_dir, target_dir, dirs_exist_ok=True)

这会合并目录并覆盖同名文件,但不会删除目标中多余的旧文件,因此它不等同于“完全同步”。

删除文件和目录​

删除文件或符号链接​

from pathlib import Path

path = Path("output/old.txt")
path.unlink(missing_ok=True)

missing_ok=True 在 Python 3.8 及以上版本可用,路径不存在时不会报错。unlink() 删除符号链接本身,不会递归删除它指向的目录。

删除空目录​

from pathlib import Path

Path("output/empty").rmdir()

目录非空时,rmdir() 会抛出 OSError。

删除整个目录树​

from pathlib import Path
import shutil

target = Path("build-cache")

if target.is_dir():
shutil.rmtree(target)

rmtree() 会递归删除目录中的全部内容,通常难以恢复。删除前至少应做到:

  • 目标来自可信配置,而不是未经检查的用户输入;
  • 把路径规范化后确认它位于允许删除的根目录中;
  • 拒绝空路径、文件系统根目录和过于宽泛的目录;
  • 重要数据先备份,必要时使用回收站方案而不是永久删除。

路径边界与安全​

Web 服务经常根据用户提供的相对路径访问固定目录。只做字符串前缀检查并不可靠,例如 /srv/data-old 也以 /srv/data 开头。应解析路径后检查父子关系:

from pathlib import Path


def resolve_inside(base_dir: Path, user_path: str) -> Path:
base = base_dir.resolve()
candidate = (base / user_path).resolve()

if not candidate.is_relative_to(base):
raise ValueError("路径超出允许目录")

return candidate

Path.is_relative_to() 从 Python 3.9 开始可用。这个检查可以阻止普通的 .. 路径穿越,但安全敏感程序还要考虑符号链接在检查后被替换的竞态条件,并使用操作系统提供的更严格文件描述符接口或隔离环境。

文件权限​

chmod() 可以修改权限模式:

from pathlib import Path

script = Path("run.sh")
script.chmod(0o755)

权限模型高度依赖操作系统。Unix 风格的 0o755 在 Windows 上不具有完全相同的意义,文件所有者、ACL 和继承权限也不由这一行代码完整表达。跨平台应用应尽量只设置确实需要的权限,并处理 PermissionError。

使用临时文件和目录​

不要自己猜测临时目录或用固定名称。tempfile 会选择合适位置并避免常见命名冲突:

from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as temporary_dir:
workspace = Path(temporary_dir)
result_path = workspace / "result.txt"
result_path.write_text("临时结果", encoding="utf-8")
print(result_path.read_text(encoding="utf-8"))

# 离开 with 后,临时目录及其内容被删除

如果外部程序需要通过路径访问临时文件,可以使用 NamedTemporaryFile。Windows 对仍打开文件的再次打开规则与 Unix 不同,跨平台代码应根据 delete、delete_on_close 和关闭时机进行设计,并以所支持的 Python 版本文档为准。

常见异常​

异常常见原因
FileNotFoundError源文件、目录或某一级父路径不存在
FileExistsError要创建的路径已经存在
PermissionError当前进程没有读取、写入或删除权限
NotADirectoryError路径中本应是目录的部分实际是文件
IsADirectoryError把目录当作普通文件操作
OSError文件占用、跨设备限制等系统错误

只捕获能够处理的异常,并保留原始异常信息:

from pathlib import Path
import shutil


def backup_file(source: Path, backup_dir: Path) -> Path:
backup_dir.mkdir(parents=True, exist_ok=True)
target = backup_dir / source.name
return Path(shutil.copy2(source, target))


try:
backup = backup_file(Path("data/report.csv"), Path("backup"))
except FileNotFoundError as error:
print(f"找不到文件:{error.filename}")
except PermissionError as error:
print(f"没有操作权限:{error.filename}")
else:
print(f"备份完成:{backup}")

不要使用 except OSError: pass 静默忽略所有问题,否则复制失败、磁盘空间不足等重要错误也会被隐藏。

综合示例:归档日志​

下面的函数查找指定天数以前的 .log 文件,并将它们移动到归档目录。dry_run=True 时只显示计划,不实际移动:

from datetime import datetime, timedelta
from pathlib import Path
import shutil


def archive_old_logs(
log_dir: Path,
archive_dir: Path,
days: int,
*,
dry_run: bool = True,
) -> list[Path]:
if days < 0:
raise ValueError("days 不能为负数")

cutoff = datetime.now().timestamp() - timedelta(days=days).total_seconds()
archive_dir.mkdir(parents=True, exist_ok=True)
archived: list[Path] = []

for source in sorted(log_dir.glob("*.log")):
if not source.is_file() or source.stat().st_mtime >= cutoff:
continue

target = archive_dir / source.name
if target.exists():
raise FileExistsError(f"归档文件已存在:{target}")

print(f"{source} -> {target}")
if not dry_run:
shutil.move(source, target)
archived.append(target)

return archived


archive_old_logs(
Path("logs"),
Path("archive/logs"),
days=30,
dry_run=True,
)

实际归档系统还应考虑并发写入、重名策略、时区、磁盘空间和失败恢复。对可能批量修改文件的工具,先提供预览模式通常更安全。

常见错误与最佳实践​

  • 路径使用 Path 构造,不手动拼接分隔符;
  • 相对路径取决于当前工作目录,应明确基准目录;
  • iterdir() 和 glob() 的结果顺序不保证稳定,需要时显式排序;
  • 状态检查不能代替异常处理,因为文件系统随时可能变化;
  • 复制、移动前明确同名目标的覆盖策略;
  • 递归删除前校验规范化后的精确路径,并为批量操作提供预览;
  • 只捕获能够恢复或转换为清晰业务提示的异常。

小结​

  • 使用 pathlib.Path 表示、拼接、拆分和查询路径;
  • mkdir()、touch()、rename()、replace()、unlink() 和 rmdir() 处理基础操作;
  • iterdir() 查看直接子项,glob() 和 rglob() 按模式搜索;
  • shutil 负责复制、跨文件系统移动和目录树操作;
  • 文件系统存在并发变化、平台差异和权限限制,预检查之后仍要处理异常;
  • 覆盖和递归删除属于高风险操作,必须明确目标、边界和恢复策略。