查看: 1279|回复: 3

pathlib替代os.path:路径拼接与文件读写实战

[复制链接]
发表于 16 小时前 | 显示全部楼层 |阅读模式
在 Python 里处理文件路径,过去基本围绕 os.path 和字符串拼接展开:拼接要 join、取文件名要 basename、判断存在要 exists,写起来冗长,而且路径始终只是一个普通字符串,没有任何语义。从 Python 3.4 开始,标准库提供了 pathlib,把路径抽象成一个有属性和方法的 Path 对象,拼接、分解、读写、遍历都能挂在对象上调用。本文按“创建—分解—变换—读写—遍历—迁移”的顺序,把 pathlib 的核心用法和与 os.path 的对应关系整理成可直接套用的写法。

一、从字符串路径切换到 Path 对象

先看两种风格的差别,同样的“拼接路径—取文件名—判断存在—读内容”流程:
  1. import os
  2. from pathlib import Path
  3. # 旧方式:全是字符串操作
  4. path_str = os.path.join("folder", "subfolder", "file.txt")
  5. basename = os.path.basename(path_str)
  6. if os.path.exists(path_str):
  7.     with open(path_str) as f:
  8.         content = f.read()
  9. # pathlib 方式:面向对象
  10. path = Path("folder") / "subfolder" / "file.txt"
  11. basename = path.name
  12. if path.exists():
  13.     content = path.read_text()
复制代码

这里体现了两点变化:路径拼接从函数调用变成运算符重载(/),读取文件从 open 上下文管理器变成 read_text() 方法。注意默认编码会跟随系统,中文文本建议显式传 encoding。

二、创建 Path 对象与 / 运算符

Path 构造函数接收字符串,相对路径和绝对路径都可以,Windows 的反斜杠路径用原始字符串即可:
  1. from pathlib import Path
  2. p1 = Path("folder/file.txt")          # 相对路径
  3. p2 = Path("/home/user/documents")     # 绝对路径(Linux)
  4. p3 = Path(r"C:\Users\name\docs")     # 绝对路径(Windows)
  5. # 用 / 拼接,这是 pathlib 最常用的特性
  6. base = Path("project")
  7. src = base / "src" / "main.py"        # project/src/main.py
  8. print(src)
  9. data_dir = Path("data")
  10. file_path = data_dir / "2024" / "report.json"
  11. print(file_path)
  12. # 常用入口
  13. cwd = Path.cwd()      # 当前工作目录
  14. home = Path.home()    # 用户主目录
  15. print(f"当前目录: {cwd}")
  16. print(f"主目录: {home}")
复制代码

需要强调的是 Path 对象是不可变的:/ 运算、with_name()、with_suffix() 都不会修改原对象,而是返回新的 Path。这一点对链式调用和函数传参很友好,不会因为某一步操作意外改掉上游持有的路径。
  1. p = Path("base")
  2. p2 = p / "sub"                # 创建新 Path,p 不变
  3. p3 = p.with_name("new_base")  # 创建新 Path,p 不变
  4. print(p)    # base
  5. print(p2)   # base/sub
  6. print(p3)   # new_base
复制代码

三、路径属性与逐段分解

拿到一个 Path 后,文件名、后缀、父目录等信息都是属性,不需要再调函数解析:
  1. p = Path("/home/user/documents/report.txt")
  2. print(p)                 # /home/user/documents/report.txt
  3. print(p.name)            # report.txt
  4. print(p.suffix)          # .txt
  5. print(p.suffixes)        # ['.txt']
  6. print(p.stem)            # report
  7. print(p.parent)          # /home/user/documents
  8. print(p.parent.parent)   # /home/user
  9. print(p.parts)           # ('/', 'home', 'user', 'documents', 'report.txt')
  10. print(p.anchor)          # /(Linux)或 C:\(Windows)
  11. print(p.is_absolute())              # True
  12. print(Path("a/b").is_absolute())    # False
  13. print(p.is_relative_to("/home"))    # True(Python 3.9+)
复制代码

suffixes 返回的是列表,用于处理 report.tar.gz 这类多重后缀;parent 可以连续取值,便于由文件定位到上级目录;anchor 表示路径的锚点,跨平台脚本用它判断是根路径还是盘符。is_relative_to() 是 Python 3.9 才加入的方法,低版本环境需注意兼容。

四、路径变换:改名、改后缀、相对化与解析
  1. p = Path("/home/user/documents/report.txt")
  2. print(p.with_name("summary.txt"))
  3. # /home/user/documents/summary.txt
  4. print(p.with_suffix(".csv"))
  5. # /home/user/documents/report.csv
  6. v2 = p.with_stem("report_v2")   # Python 3.9+
  7. print(v2)                       # /home/user/documents/report_v2.txt
  8. base = Path("/home/user")
  9. print(p.relative_to(base))      # documents/report.txt
  10. abs_path = Path(".").resolve()
  11. print(f"绝对路径: {abs_path}")
复制代码

with_name 改文件名、with_suffix 改扩展名、with_stem 改主干名,三者都只返回新对象,适合批量生成输出文件路径。relative_to 用于把绝对路径转成相对基准目录的写法,但要求路径确实位于该基准之下。resolve 会把相对路径转成绝对路径并解析符号链接,这也是路径比较的前提:
  1. a = Path("/home/user/docs")
  2. b = Path("/home/user/docs/../docs").resolve()
  3. print(a == b)   # 字符串层面比较为 False;两边都 resolve 后为 True
复制代码

Path 的比较按路径语义进行,但含 .. 或符号链接时仍需先 resolve 才能得到一致结果。

五、文件读写:一行搞定

text 与 bytes 的读写都直接封装在 Path 上,JSON 也可以借道 text 接口:
  1. from pathlib import Path
  2. import json
  3. p = Path("test_pathlib.txt")
  4. p.write_text("Hello, pathlib!\n这是第二行", encoding="utf-8")
  5. content = p.read_text(encoding="utf-8")
  6. print(content)
  7. # 二进制
  8. # p.write_bytes(b"binary data")
  9. # data = p.read_bytes()
  10. data = {"name": "张三", "age": 25}
  11. json_path = Path("data.json")
  12. json_path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  13. loaded = json.loads(json_path.read_text(encoding="utf-8"))
  14. print(loaded)
复制代码

write_text / read_text 适合中小文件,大文件仍然要用 open 流式处理,避免一次性读入内存。写 JSON 时 ensure_ascii=False 保证中文不被转义,encoding="utf-8" 保证跨平台一致。

六、存在性判断、目录创建与删除
  1. print(p.exists())
  2. print(p.is_file())
  3. print(p.is_dir())
  4. print(p.stat().st_size)   # 字节数
  5. new_dir = Path("new_pathlib_dir/sub")
  6. new_dir.mkdir(parents=True, exist_ok=True)
  7. new_dir.joinpath("test.txt").touch()
  8. p.unlink()                 # 删除文件
  9. json_path.unlink()
  10. new_dir.joinpath("test.txt").unlink()
  11. new_dir.rmdir()            # 删除空目录
  12. new_dir.parent.rmdir()
复制代码

mkdir 的 parents=True 会自动补齐多级父目录,exist_ok=True 让“目录已存在”不再抛异常,这两个参数几乎是脚本初始化的标配。touch() 创建空文件或更新时间戳;unlink() 删文件、rmdir() 删空目录,非空目录会报错,删非空目录需自行递归处理。目录遍历用 iterdir,模式匹配用 glob / rglob:
  1. cwd = Path(".")
  2. py_files = list(cwd.glob("*.py"))
  3. print([f.name for f in py_files])
  4. all_py = list(cwd.rglob("*.py"))   # 等价于 cwd.glob("**/*.py")
  5. print(f"递归找到 {len(all_py)} 个 Python 文件")
复制代码

glob 只扫当前层,rglob 递归所有子目录,直接返回 Path 对象,后续取 name、读内容都不必再拼字符串。

七、os.path 到 pathlib 对照表

迁移老代码时按下面的对应关系替换即可:
  1. # os.path.join(a, b)          -> Path(a) / b
  2. # os.path.basename(p)           -> Path(p).name
  3. # os.path.dirname(p)            -> Path(p).parent
  4. # os.path.splitext(p)[1]        -> Path(p).suffix
  5. # os.path.exists(p)             -> Path(p).exists()
  6. # os.path.isdir(p)              -> Path(p).is_dir()
  7. # os.path.isfile(p)             -> Path(p).is_file()
  8. # os.path.abspath(p)            -> Path(p).resolve()
  9. # os.path.getsize(p)            -> Path(p).stat().st_size
  10. # os.listdir(p)                 -> Path(p).iterdir()
  11. # os.makedirs(p)                -> Path(p).mkdir(parents=True)
  12. # open(p, "r")                  -> Path(p).open("r")
  13. # glob.glob("*.py")             -> Path(".").glob("*.py")
复制代码

迁移时有两个细节值得留意:一是 os.listdir 返回字符串列表,iterdir 返回 Path 迭代器,遍历后取名的写法要相应调整;二是 os.path.abspath 只是补全为绝对路径,而 resolve 还会解析符号链接和 ..,两者行为并不完全等价,涉及软链的目录要确认结果是否符合预期。

八、使用建议

新项目可以直接以 pathlib 为主:用 / 拼接、用属性取文件名与后缀、用 read_text / write_text 读写、用 glob / rglob 检索、用 mkdir(parents=True, exist_ok=True) 建目录,代码量比 os.path 明显更短也更好读。维护老代码时不必全量重写,新增逻辑用 Path、与 os.path 交界处用 str(path) 转换即可平滑过渡。需要注意 with_stem、is_relative_to 属于 Python 3.9 起提供的接口,若项目需支持更低版本,应改用 with_name 组合 stem/suffix 或自行做前缀判断。
回复

使用道具 举报

发表于 16 小时前 | 显示全部楼层

Re: pathlib替代os.path:路径拼接与文件读写实战

整理得很清楚,从 os.path 的字符串操作切换到 Path 对象这条主线讲得很顺,“创建—分解—变换—读写—遍历—迁移”的顺序也确实符合实际写代码时的思路。前面那段新旧对比很直观,尤其是把 open 上下文管理器替换成 read_text 这一点,最能体现改成 Path 之后代码确实变短了。 几个我自己用下来认同的点:一是 Path 不可变这点值得反复强调,刚切换过来时容易下意识以为 p.with_suffix 会就地改掉 p,实际上都是返回新对象,所以链式写起来很安心,函数传参也不怕被上游改动。二是 / 运算符虽然好用,但要注意拼接的起点必须是个 Path,如果写成 “folder” / “sub” 就会直接报错,跟 os.path.join 那种随手传字符串的习惯不太一样,从旧代码迁移时这类地方最容易漏改。三是 suffixes 处理 report.tar.gz 这种多重后缀确实比手动 split 干净,而 parent 连续取值对“从文件往上找目录”的场景也很顺手。 另外你提到默认编码跟随系统、中文文本要显式传 encoding,这一点很关键,跨平台脚本里如果不定死 utf-8,在 Windows 上读中文文件经常会踩坑,read_text 和 write_text 都建议养成带上 encoding 的习惯。还有 is_relative_to 是 3.9 才有的,低版本就
回复 支持 反对

使用道具 举报

发表于 16 小时前 | 显示全部楼层

Re: pathlib替代os.path:路径拼接与文件读写实战

这篇整理得很清楚,按“创建—分解—变换—读写—遍历—迁移”的顺序展开,查起来很方便。把 os.path 和 pathlib 放在同一段流程里对比,`/` 拼接、`.name`、`.exists()`、`read_text()` 这些差异一眼就能看出来,尤其适合刚开始从字符串路径切到 Path 对象的人。 几个提醒也很实用:默认编码跟随系统、中文文本显式传 encoding;Path 不可变,`/`、`with_name()`、`with_suffix()` 都返回新对象;还有 `is_relative_to()` 是 Python 3.9+ 才有的,低版本要留意兼容性。这些都是实际写脚本时容易踩的点。 不过正文末尾 `v2 = p.with_stem("` 这里好像被截断了,后面“读写、遍历、迁移”的部分没显示完整。如果方便的话可以补全一下,尤其是 `read_text` / `write_text` 的编码写法和 `os.path` 到 `pathlib` 的对应表,应该会很有参考价值。
回复 支持 反对

使用道具 举报

发表于 16 小时前 | 显示全部楼层

Re: pathlib替代os.path:路径拼接与文件读写实战

楼主这篇整理得很清楚,尤其是开头那段新旧写法对照,一眼就能看出 pathlib 在可读性上的优势。我原来习惯混着用,拼接用 os.path.join 判断存在用 exists,代码里到处是字符串,看完这个“创建—分解—变换—读写—遍历—迁移”的顺序,思路顺畅多了。 几个自己踩过的点补充一下: 一是 `/` 运算符刚开始觉得别扭,用多了确实顺手,但要注意两边不能都是字符串,比如 `"a" / "b"` 会报错,得至少一边是 Path 对象,所以一般开头 `Path("base")` 定一个基点,后面链式拼。 二是 `read_text()` / `write_text()` 默认编码跟随系统,这个坑在 Windows 上特别明显,中文文本我基本都会显式写 `encoding="utf-8"`,不然换台机器就乱码。 三是 `resolve()` 和 `absolute()` 的区别,前者会解析 `..` 和符号链接,后者只是补上当前目录,需要真实绝对路径时我应该更常用前者。 四是 3.9 才有的 `is_relative_to()` 和 `with_stem()`,如果项目要兼容 3.8 及以下就得绕一下,可以用字符串或者 `parents` 判断,`with_stem` 也能用 `with_name(p.stem + "_v2" + p.suffix)` 替代。 另外看到正文最后 `
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-9 23:50 , Processed in 0.021647 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部