RSS
菜单
全部文章快讯开发科技深度热点

文件与路径处理(Python 从精通到入门 · 14)

内容摘要

文件读写看着简单,坑都藏在编码、换行和路径分隔符里;这篇把它们一次讲透,并告诉你为什么 pathlib 应当取代 os.path。

文件读写看着简单,坑都藏在编码、换行和路径分隔符里;这篇把它们一次讲透,并告诉你为什么 pathlib 应当取代 os.path。

你将学到

  • open() 的 encoding 与 newline 到底在控制什么,以及为什么必须显式指定
  • 读大文件的正确姿势:逐行迭代 vs 分块读取
  • os.path 和 pathlib.Path 的对比,以及为什么新代码应当用后者
  • 路径拼接、遍历、glob / rglob 通配,文件和目录的增删改查
  • tempfile 临时文件,文本与二进制的区别,以及 JSON 文件的读写
  • with 上下文管理器在文件 I/O 里的再次应用

前置知识

需要理解 with 语句和上下文管理器,见 上一篇:代码质量与工具链 之前的 04-上下文管理器与with。

打开文件的三个参数

open(file, mode, encoding, newline) 里,最容易被忽略的是 encoding 和 newline。

# ❌ 不指定 encoding:用了操作系统的默认编码
# Windows 默认 GBK,Linux/macOS 默认 UTF-8,同一份代码换个机器就乱码
with open("data.txt", "r") as f:
    text = f.read()

# ✅ 永远显式指定 encoding="utf-8"
with open("data.txt", "r", encoding="utf-8") as f:
    text = f.read()

encoding="utf-8" 还有个小技巧:用 "utf-8-sig" 可以自动吃掉 Windows 记事本写文件时加的 BOM(\ufeff),处理别人给的 CSV 时很实用。

newline 控制换行符的翻译。默认 newline=None 时,读的时候会把 \r\n、\r、\n 统一翻译成 \n;写的时候把 \n 翻译成当前平台的换行符。这大概率是你想要的。但有两个例外:

# 处理 csv 时希望保留原始换行,交给 csv 模块自己处理
with open("data.csv", "r", encoding="utf-8", newline="") as f:
    ...

# 写文本时固定用 \n(比如生成给 Linux 用的脚本),别被平台翻译
with open("run.sh", "w", encoding="utf-8", newline="\n") as f:
    f.write("#!/bin/bash\necho hi\n")

模式字符串速记:r 读、w 写(清空)、a 追加、x 独占创建(已存在就报错)、b 二进制、+ 读写。文本模式别和 b 混用,open(..., "rb") 时不能传 encoding。

读大文件:别 readlines

一个 5GB 的日志文件,f.readlines() 会把它整个塞进内存——直接 OOM。

# ❌ 一次性全读进内存
with open("huge.log", encoding="utf-8") as f:
    for line in f.readlines():   # readlines 先把所有行加载成 list
        process(line)

# ✅ 文件对象本身就可迭代,逐行流式读取,内存占用恒定
with open("huge.log", encoding="utf-8") as f:
    for line in f:
        process(line)

如果一行本身就超长(比如没有换行的单行 JSON),逐行也不好使,得按块读:

# ✅ 按固定大小分块读,适合二进制或超长行
def read_in_chunks(path, size=8192):
    with open(path, "rb") as f:
        while chunk := f.read(size):   # 海象运算符,读到空 bytes 就结束
            yield chunk

for chunk in read_in_chunks("big.bin"):
    # 这里 chunk 是 bytes,交给解析逻辑
    consume(chunk)

os.path vs pathlib.Path

os.path 是一堆操作字符串的函数,pathlib 是把路径当成对象。后者可读性、可组合性全面胜出,Python 3.6+ 就应该用它。

import os
from pathlib import Path

# os.path 风格:函数套函数,返回的都是 str
base = "/tmp/data"
p = os.path.join(base, "2026", "report.csv")
parent = os.path.dirname(p)
name = os.path.basename(p)
ext = os.path.splitext(name)[1]

# pathlib 风格:用 / 拼路径,属性直接取
p = Path("/tmp/data") / "2026" / "report.csv"   # 输出: /tmp/data/2026/report.csv
p.parent        # 输出: /tmp/data/2026
p.name          # 输出: report.csv
p.stem          # 输出: report
p.suffix        # 输出: .csv
p.exists()      # 输出: False(还没创建)

/ 运算符会让很多从别的语言转过来的人愣一下,但它真的比 os.path.join 舒服。Path 还能直接读写文件:

p = Path("notes.txt")
p.write_text("hello\n", encoding="utf-8")   # 等价于 open 写
text = p.read_text(encoding="utf-8")         # 等价于 open 读

跨平台是 pathlib 的另一大优势:Windows 上 Path("a") / "b" 会得到 a\b,在 Linux 上得到 a/b,你不用手写 os.sep。

遍历与通配

glob 用 *(单层)和 **(递归任意层)匹配路径,比手写 os.walk 舒服得多。

from pathlib import Path

root = Path("project")

# 当前目录下所有 .py 文件
for p in root.glob("*.py"):
    print(p)

# 递归所有层级的 .py(rglob 等价于 glob("**/*.py"))
for p in root.rglob("*.py"):
    print(p)

# 多条件:遍历所有图片
for p in root.rglob("*"):
    if p.suffix.lower() in {".jpg", ".png", ".gif"}:
        print(p)

需要完整目录树遍历时用 os.walk,或者 Path.rglob("*"):

import os

for dirpath, dirnames, filenames in os.walk("project"):
    # dirnames 可以原地修改,用来剪枝(比如跳过 .git)
    dirnames[:] = [d for d in dirnames if d != ".git"]
    for name in filenames:
        print(os.path.join(dirpath, name))

增删改查

from pathlib import Path
import shutil

p = Path("out/report")
p.mkdir(parents=True, exist_ok=True)   # 递归建目录,已存在也不报错
p.is_dir()                              # 输出: True
p.is_file()                             # 输出: False

f = p / "a.txt"
f.touch(exist_ok=True)                  # 创建空文件

# 改名 / 移动
f.rename(p / "b.txt")

# 复制(含元数据)
shutil.copy2(p / "b.txt", p / "c.txt")

# 复制整棵目录树
shutil.copytree("out/report", "out/backup", dirs_exist_ok=True)

# 删除文件 / 空目录 / 整棵树
(p / "c.txt").unlink(missing_ok=True)
shutil.rmtree("out/backup", ignore_errors=True)

Path 上还有 iterdir() 列目录、resolve() 转绝对路径、stat() 拿文件信息。resolve() 在处理 .. 和符号链接时特别有用。

临时文件 tempfile

写测试、做中间缓存时,别用 "tmp.txt" 这种会污染工作目录的名字,用 tempfile。

import tempfile
from pathlib import Path

# 一次性临时文件:关闭即自动删除
with tempfile.NamedTemporaryFile("w", suffix=".txt", encoding="utf-8",
                                 delete=True) as tf:
    tf.write("临时内容")
    tf.flush()                      # 让其他进程能读到
    print(Path(tf.name).read_text(encoding="utf-8"))   # 输出: 临时内容
# with 块结束后文件已被删除

# 临时目录
with tempfile.TemporaryDirectory() as d:
    p = Path(d) / "x.txt"
    p.write_text("hi", encoding="utf-8")
    # 出了 with 块,整个目录连同内容一起删除

文本 vs 二进制

文本模式(默认)会做编码解码和换行翻译,返回 str;二进制模式("rb" / "wb")返回 bytes,不做任何翻译。

# 文本:读回来是 str
with open("t.txt", "w", encoding="utf-8") as f:
    f.write("你好")

# 二进制:读回来是 bytes,适合图片、压缩包、序列化数据
with open("t.png", "rb") as f:
    header = f.read(8)
    print(header[:4])   # 输出: b'\x89PNG'

判断一个文件是不是文本靠猜,但可以用 errors 参数兜底:open(p, encoding="utf-8", errors="replace") 会把非法字节替换成 �,而不是抛 UnicodeDecodeError。

JSON 文件读写

json 模块只管 str 和对象之间的转换,落盘仍然要配合 open。

import json
from pathlib import Path

data = {"name": "Alice", "score": 95, "tags": ["a", "b"]}

# 写:ensure_ascii=False 才能正常显示中文
Path("u.json").write_text(
    json.dumps(data, ensure_ascii=False, indent=2),
    encoding="utf-8",
)

# 更省事的写法:dump 直接写文件对象
with open("u.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

# 读:load 直接从文件对象解析
with open("u.json", encoding="utf-8") as f:
    loaded = json.load(f)
print(loaded["tags"])   # 输出: ['a', 'b']

ensure_ascii=True(默认)会把中文转成 \uXXXX,看着丑,但纯 ASCII 兼容性最好。给人类看就 False。

with 的再次应用

open 返回的文件对象本身就是上下文管理器,with 保证异常时也能关闭。多个文件可以一行搞定:

# ✅ 同时打开输入输出,任何一边出错都会正确关闭
with open("in.txt", encoding="utf-8") as fin, \
     open("out.txt", "w", encoding="utf-8") as fout:
    for line in fin:
        fout.write(line.rstrip() + "\n")

Python 3.10+ 还能用括号分组,可读性更好:

# Python 3.10+
with (
    open("in.txt", encoding="utf-8") as fin,
    open("out.txt", "w", encoding="utf-8") as fout,
):
    fout.writelines(fin)

常见坑

# ❌ 不指定 encoding,靠平台默认值
open("f.txt")                        # 换个系统就乱码

# ✅ 显式 utf-8
open("f.txt", encoding="utf-8")      # 到哪都一样

# ❌ 手动拼字符串拼路径,Windows 上分隔符错
path = "data" + "\\" + "a.csv"       # 只在 Windows 对

# ✅ 用 pathlib 的 /
from pathlib import Path
path = Path("data") / "a.csv"        # 哪都对
# ❌ 读大文件用 readlines / read(),内存爆炸
lines = open("huge.log").readlines()

# ✅ 逐行迭代,或用 read_in_chunks 分块
for line in open("huge.log", encoding="utf-8"):
    ...

# ❌ 忘了关闭文件句柄,靠 GC 不稳定
f = open("f.txt", "w")
f.write("x")

# ✅ 用 with,作用域结束自动关闭
with open("f.txt", "w", encoding="utf-8") as f:
    f.write("x")

小结

  • 永远显式写 encoding="utf-8";需要保留原始换行或处理 CSV 时加 newline=""。
  • 大文件不要 readlines();文件对象本身可迭代,超长行用 read(size) 分块。
  • 新代码一律用 pathlib.Path;/ 拼路径、.parent/.suffix 取片段、glob/rglob 做匹配。
  • 建目录用 mkdir(parents=True, exist_ok=True),删树用 shutil.rmtree(..., ignore_errors=True)。
  • 临时文件用 tempfile,别污染工作目录;文本模式返回 str,二进制模式返回 bytes,两者别混。
  • JSON 落盘:ensure_ascii=False 保留中文,indent=2 方便人看。
  • with 是文件 I/O 的标准姿势,多文件可用括号分组一次打开。

延伸阅读

  • 官方文档:pathlib、os.path、tempfile、shutil
  • 下一篇会讲 正则表达式,处理文本时和文件读写是绝配

上一篇:代码质量与工具链 · 下一篇:正则表达式

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《文件与路径处理(Python 从精通到入门 · 14)》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。