前言
一说到「文件操作」,多数人第一反应是open()读和写。但真实项目里,读写只占一半,另一半是路径拼接、判断存在、复制、移动、重命名、删除、取文件信息。这些操作分散在os、os.path、shutil、pathlib几个模块里,新手常常凭印象拼一个函数名出来,跑起来才发现根本没有这个函数。
另一个常见误解是把pathlib和os.path当成「新写法 / 旧写法」二选一。实际上两者定位不同:os.path处理的是字符串路径,pathlib处理的是路径对象,它们调用同一套底层系统接口,官方文档专门列了对照表。选哪个更多是风格问题,混用也不会出错。
本文按「路径怎么拼 → 文件怎么判断、复制、移动、删除 → 信息怎么取」的顺序,把常用操作和它们的准确签名梳理一遍。示例面向 Python 3.8+,涉及 3.9+ 才有的方法时会单独注明。
一、路径拼接与规范化
拼接路径不要手写字符串加号,因为 Windows 用反斜杠、类 Unix 用正斜杠:
# 适用于 Python 3.8+
import os
path = os.path.join("reports", "2026", "q1.csv")
print(path) # Windows 上是 reports\2026\q1.csvos.path常用的几个函数:
| 函数 | 作用 |
|---|
os.path.join(*parts) | 用当前系统的分隔符拼接路径 |
os.path.basename(path) | 取最后一段(文件名) |
os.path.dirname(path) | 取最后一段之前的部分 |
os.path.splitext(path) | 拆成(主干, 扩展名)二元组 |
os.path.abspath(path) | 转成绝对路径(不解析符号链接) |
os.path.realpath(path) | 解析符号链接后的真实路径 |
os.path.normpath(path) | 折叠..与多余分隔符 |
pathlib把同样的功能做成了方法。官方的对照关系是:os.path.join()对应PurePath.joinpath(),os.path.abspath()对应Path.absolute(),os.path.realpath()对应Path.resolve()。用/运算符拼接最直观:
# 适用于 Python 3.8+
from pathlib import Path
p = Path("reports") / "2026" / "q1.csv"
print(p.name) # q1.csv
print(p.suffix) # .csv
print(p.stem) # q1
print(p.parent) # reports/2026有一点值得注意:Path.absolute()会保留路径里的..,而os.path.abspath()会把..消掉,这两者在语义上有细微差别,迁移代码时不要想当然地替换。
二、复制、移动、改名、删除
shutil(shell utilities)负责「文件/目录级」的搬运。它的签名必须写准:
# 适用于 Python 3.8+
import shutil
shutil.copyfile(src, dst) # 只复制内容,dst 必须是完整文件名
shutil.copy(src, dst) # 复制内容 + 权限位;dst 可以是目录
shutil.copy2(src, dst) # 在 copy 基础上再尽量保留元数据
shutil.copytree(src, dst) # 递归复制整个目录
shutil.move(src, dst) # 移动(同盘相当于改名,跨盘是复制后删除)
shutil.rmtree(path) # 递归删除目录树,危险copyfile与copy的区别常被忽略:copyfile的第二个参数必须是完整的文件名,而copy允许给一个目录,此时会把源文件名接到该目录下。copy2额外用copystat()复制元数据,但官方明确说它也不能复制全部元数据——不要以为copy2就是无损克隆。
单纯改名用os.rename();如果目标存在需要覆盖,用os.replace():
# 适用于 Python 3.8+
import os
os.rename("old.txt", "new.txt") # 目标已存在时,Windows 上会报错
os.replace("draft.txt", "final.txt") # 目标存在则原子覆盖删除分三种,务必分清:
# 适用于 Python 3.8+
import os
os.remove("a.txt") # 删文件;等价别名 os.unlink()
os.rmdir("emptydir") # 只能删空目录
os.removedirs("a/b/c") # 自底向上删,目录不空就停os.rmdir()遇到非空目录会抛OSError,这是它和rmtree最大的区别——也是保护机制,别急着改成rmtree。
三、判断存在性与取文件信息
os.path系列的判断函数返回布尔值:
| 函数 | 判断内容 |
|---|
os.path.exists(p) | 路径是否存在(文件或目录都算) |
os.path.isfile(p) | 是否是普通文件 |
os.path.isdir(p) | 是否是目录 |
os.path.islink(p) | 是否是符号链接 |
os.path.getsize(p) | 文件字节数(目录上的结果依系统而定) |
pathlib里的对应写法是Path.exists()、Path.is_file()、Path.is_dir()、Path.is_symlink()、Path.stat()。用stat()拿到的是os.stat_result,可以读.st_size、.st_mtime等字段:
# 适用于 Python 3.8+
from pathlib import Path
p = Path("data.csv")
if p.is_file():
info = p.stat()
print(f"占用 {info.st_size} 字节")实战:把一个目录里的文件按扩展名归档
这段脚本把inbox里的文件按扩展名移动到对应子目录,演示了路径拼接、存在判断、目录创建和移动的组合用法:
# 适用于 Python 3.8+
from pathlib import Path
import shutil
def archive(inbox="inbox"):
root = Path(inbox)
for entry in root.iterdir():
if not entry.is_file():
continue
ext = entry.suffix.lstrip(".").lower() or "noext"
target_dir = root / ext
target_dir.mkdir(exist_ok=True) # 已存在不报错
shutil.move(str(entry), str(target_dir / entry.name))
if __name__ == "__main__":
archive()两个细节:Path.mkdir(exist_ok=True)才能对已存在的目录静默通过,默认exist_ok=False会抛FileExistsError;shutil.move接受的是路径,传Path对象大多数时候没问题,但为了行为稳定,显式str()一下更保险。
常见坑点
1. 用字符串加号拼路径
❌"src/" + name,在 Windows 上可能拼出混用分隔符的路径。 ✅os.path.join("src", name)或Path("src") / name。
2. 把shutil.copyfile的第二个参数写成目录
❌shutil.copyfile("a.txt", "backup/"),会抛IsADirectoryError。 ✅ 要写目录就用shutil.copy("a.txt", "backup/")。
3. 用os.rmdir删非空目录
❌ 对非空目录调用os.rmdir(),报OSError: Directory not empty。 ✅ 确认要整树删除时用shutil.rmtree();只是想删空目录才用rmdir。
4. 判断存在后立刻操作,中间目录被删
❌if os.path.exists(p): open(p),两步之间文件可能已经消失。 ✅ 直接操作并捕获FileNotFoundError,也就是常说的「宁求原谅不求许可」。
5. 以为copy2保留全部元数据
❌ 认为copy2能把所有者、ACL 都带过去。 ✅ 官方文档明确说明copy2也不能复制全部元数据,需要时用系统级工具或copystat单独补。
6.os.rename跨盘失败
❌ 用os.rename()在 C 盘和 D 盘之间移动文件,报错。 ✅ 跨卷移动用shutil.move(),它内部会退化成「复制 + 删除」。
7.mkdir少写parents/exist_ok
❌Path("a/b/c").mkdir()在a不存在时抛FileNotFoundError。 ✅Path("a/b/c").mkdir(parents=True, exist_ok=True)。
8. 在os.walk里边遍历边删目录
❌ 遍历中直接shutil.rmtree(dirnames),导致后续遍历踩到不存在的路径。 ✅ 先收集完整路径列表,遍历结束后再统一删除。
总结
| 需求 | 首选写法 | 备注 |
|---|
| 拼接路径 | os.path.join/Path / name | 不要用字符串加号 |
| 复制文件 | shutil.copy/shutil.copy2 | copyfile要求完整目标名 |
| 递归复制 | shutil.copytree | dirs_exist_ok控制目标存在行为 |
| 改名 / 覆盖 | os.rename/os.replace | replace原子覆盖 |
| 删文件 | os.remove | 别名os.unlink |
| 删空目录 / 整树 | os.rmdir/shutil.rmtree | rmtree危险,二次确认 |
| 存在性 | os.path.exists/Path.exists | 判断与操作之间别留缝 |
文件操作本身是简单 API 的堆叠,真正容易出事的地方是边界:目标已存在、目录非空、跨卷移动、路径分隔符。把这几类情况的预期行为想清楚,代码就稳了。