Skip to content

06-文件操作

Python 3.11+


为什么需要文件操作?

问题场景

程序运行时数据在内存中,关闭后就丢失了。文件是最基本的持久化手段:

python
# ❌ 不写文件:数据随程序退出消失
orders = [{"id": 1, "amount": 99.0}]

# ✅ 写入文件:数据持久化,下次启动可读回
from pathlib import Path
import json

Path("orders.json").write_text(
    json.dumps(orders, ensure_ascii=False), encoding="utf-8"
)

文件操作是日志记录、配置管理、数据导入导出等几乎所有应用场景的基础。


概念铺垫

Python 的文件 I/O 建立在 io 模块之上,这是一个分层的 I/O 系统。理解各层级的职责能帮你写出高性能的文件操作代码。

┌──────────────────────────────────────────────────────────────┐
│          Python I/O 层次结构                                  │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────────────────────────────────────┐               │
│   │  Raw I/O (二进制底层)                    │               │
│   │  io.FileIO — 直接系统调用 read/write     │               │
│   │  无缓冲,直接操作文件描述符               │               │
│   └─────────────────────────────────────────┘               │
│                      ↑                                        │
│   ┌─────────────────────────────────────────┐               │
│   │  Buffered I/O (带缓冲的二进制)            │               │
│   │  io.BufferedReader / io.BufferedWriter   │               │
│   │  io.BufferedRandom                       │               │
│   │  默认缓冲区:DEFAULT_BUFFER_SIZE = 8192  │               │
│   │  优化:减少系统调用次数                   │               │
│   └─────────────────────────────────────────┘               │
│                      ↑                                        │
│   ┌─────────────────────────────────────────┐               │
│   │  Text I/O (文本层)                       │               │
│   │  io.TextIOWrapper                       │               │
│   │  编码:UTF-8/GBK 等                      │               │
│   │  换行符转换:\n ↔ os.linesep             │               │
│   │  行缓冲:每行后自动 flush                 │               │
│   └─────────────────────────────────────────┘               │
│                                                              │
│   open() 的 mode 参数决定创建哪些层的组合:                    │
│   · 'r'  → TextIOWrapper(BufferedReader(FileIO))            │
│   · 'rb' → BufferedReader(FileIO)                           │
│   · 'w'  → TextIOWrapper(BufferedWriter(FileIO))            │
│   · 'wb' → BufferedWriter(FileIO)                           │
│                                                              │
│   文本 vs 二进制模式的关键区别:                              │
│   文本模式:自动编码/解码 + 换行符转换 + 行迭代               │
│   二进制模式:原始字节流,无编解码开销                        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

L1 理解层:会用

第一部分:文件读写基础

1.1 open() 函数

实际场景

在数据处理、日志记录、配置管理等场景中,都需要与文件进行交互。理解文件打开模式是进行任何文件操作的第一步。

问题:Python 的 open() 函数有哪些打开模式?如何选择合适的模式?

python
from typing import TextIO, BinaryIO

file: TextIO = open('file.txt', 'r', encoding='utf-8')

file_r: TextIO = open('file.txt', 'r+')  # 读写,文件必须存在
file_w: TextIO = open('file.txt', 'w+')  # 写读,覆盖原文件
file_rb: BinaryIO = open('image.png', 'rb')  # 二进制读
file_wb: BinaryIO = open('data.bin', 'wb')  # 二进制写

打开模式:

模式说明
r读取(默认),文件必须存在
w写入,文件不存在则创建,存在则覆盖
a追加,文件不存在则创建
x创建写入,文件已存在则报错
b二进制模式
t文本模式(默认)
+读写模式

1.2 with 语句

实际场景

文件资源需要正确释放,忘记关闭文件会导致资源泄漏甚至数据丢失。在异常发生时,更需要确保文件被正确关闭。

问题:为什么推荐使用 with 语句打开文件?它有什么优势?

python
from typing import TextIO

with open('file.txt', 'r', encoding='utf-8') as f:
    content: str = f.read()

f: TextIO = open('file.txt', 'r', encoding='utf-8')
content: str = f.read()
f.close()

with 语句的优势:

┌─────────────────────────────────────────────────────────────┐
│              with 语句的优势                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. 自动关闭                                               │
│      代码块结束后自动调用 f.close()                          │
│                                                             │
│   2. 异常安全                                               │
│      即使发生异常,文件也会正确关闭                          │
│                                                             │
│   3. 代码简洁                                               │
│      不需要手动管理资源                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.3 读取文件

实际场景

处理日志文件、配置文件、数据文件时,需要选择合适的读取方式:一次性读取、逐行读取、按字节读取。

问题:read()、readline()、readlines() 有什么区别?处理大文件应该用什么方法?

python
with open('file.txt', 'r') as f:
    content: str = f.read()

with open('file.txt', 'r') as f:
    chunk: str = f.read(1024)

with open('file.txt', 'r') as f:
    line: str = f.readline()

with open('file.txt', 'r') as f:
    lines: list[str] = f.readlines()

with open('file.txt', 'r') as f:
    for line in f:
        print(line.strip())

读取配置文件示例:

python
def read_config(filename: str) -> dict[str, str]:
    config: dict[str, str] = {}
    with open(filename, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if line and not line.startswith('#'):
                key, value = line.split('=')
                config[key.strip()] = value.strip()
    return config

config: dict[str, str] = read_config('config.txt')
print(config)

1.4 写入文件

实际场景

记录日志、保存数据、导出报告等场景需要写入文件内容,包括覆盖写入、追加写入、批量写入。

问题:write() 和 writelines() 有什么区别?如何追加内容到文件末尾?

python
with open('file.txt', 'w', encoding='utf-8') as f:
    f.write('Hello, World!\n')
    f.write('Python 文件操作')

lines: list[str] = ['第一行\n', '第二行\n', '第三行\n']
with open('file.txt', 'w', encoding='utf-8') as f:
    f.writelines(lines)

with open('file.txt', 'a', encoding='utf-8') as f:
    f.write('\n追加的内容')

写入日志示例:

python
import datetime

def write_log(filename: str, message: str) -> None:
    timestamp: str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    with open(filename, 'a', encoding='utf-8') as f:
        f.write(f'[{timestamp}] {message}\n')

write_log('app.log', '用户登录成功')
write_log('app.log', '订单创建完成')

1.5 文件指针

实际场景

读取文件的特定位置、实现随机访问、分块读取大文件时,需要操作文件指针。

问题:如何获取和设置文件当前位置?seek() 的三种模式有什么区别?

python
with open('file.txt', 'r') as f:
    pos: int = f.tell()
    print(f"当前位置: {pos}")

    data: str = f.read(10)
    print(f"读取后位置: {f.tell()}")

    f.seek(0)

    f.seek(20)

    f.seek(10, 1)

    f.seek(0, 2)

seek() 参数:

参数值含义
0从文件开头计算
1从当前位置计算
2从文件末尾计算

第二部分:pathlib 路径处理

2.1 Path 对象

实际场景

传统的 os.path 操作路径需要拼接字符串,容易出错且难以跨平台。pathlib 提供了面向对象的路径操作方式。

问题:pathlib.Path 相比字符串路径有什么优势?如何使用 / 操作符拼接路径?

python
from pathlib import Path

p: Path = Path('data/file.txt')
p = Path('/home/user/documents')

cwd: Path = Path.cwd()

home: Path = Path.home()

p: Path = Path('data') / 'subdir' / 'file.txt'

路径属性:

python
p: Path = Path('/home/user/documents/report.pdf')

print(p.name)      # report.pdf(文件名)
print(p.stem)      # report(不含扩展名)
print(p.suffix)    # .pdf(扩展名)
print(p.parent)    # /home/user/documents(父目录)
print(p.anchor)    # /(根路径)

print(p.exists())  # 是否存在
print(p.is_file()) # 是否是文件
print(p.is_dir())  # 是否是目录
print(p.is_absolute())  # 是否是绝对路径

2.2 目录操作

实际场景

创建日志目录、遍历文件系统、批量处理文件等场景需要操作目录结构。

问题:如何创建多级嵌套目录?如何递归查找特定类型的文件?

python
from pathlib import Path

p: Path = Path('data/output')
p.mkdir()

p: Path = Path('data/subdir/output')
p.mkdir(parents=True, exist_ok=True)

p: Path = Path('data')
for item in p.iterdir():
    print(item.name, item.is_file(), item.is_dir())

p: Path = Path('data')
for txt_file in p.glob('*.txt'):
    print(txt_file)

p: Path = Path('data')
for py_file in p.rglob('*.py'):
    print(py_file)

删除目录:

python
from pathlib import Path
import shutil

p: Path = Path('empty_dir')
p.rmdir()

shutil.rmtree('directory')

2.3 文件操作

实际场景

使用 pathlib 可以更优雅地进行文件读写、复制、移动、删除等操作,无需手动打开关闭文件。

问题:Path 对象提供了哪些文件操作方法?如何直接读写文件内容?

python
from pathlib import Path
import shutil

p: Path = Path('data/file.txt')

content: str = p.read_text(encoding='utf-8')

p.write_text('Hello, World!', encoding='utf-8')

data: bytes = p.read_bytes()

p.write_bytes(b'\x00\x01\x02')

p: Path = Path('old_name.txt')
p.rename('new_name.txt')

p: Path = Path('file.txt')
p.rename(Path('new_dir/file.txt'))

shutil.copy('source.txt', 'dest.txt')
shutil.copy2('source.txt', 'dest.txt')

p: Path = Path('file.txt')
p.unlink()

第三部分:异常处理

3.1 文件异常

实际场景

文件操作可能出现各种错误:文件不存在、权限不足、编码错误等。需要优雅地处理这些异常,提供友好的错误信息。

问题:文件操作可能抛出哪些异常?如何编写健壮的文件读取函数?

python
from pathlib import Path

def safe_read(filename: str) -> str | None:
    try:
        p: Path = Path(filename)
        return p.read_text(encoding='utf-8')

    except FileNotFoundError:
        print(f"文件不存在: {filename}")
        return None

    except PermissionError:
        print(f"无权限访问: {filename}")
        return None

    except UnicodeDecodeError:
        print(f"编码错误: {filename}")
        try:
            return p.read_text(encoding='gbk')
        except Exception:
            return None

    except IOError as e:
        print(f"IO 错误: {e}")
        return None

异常类型:

异常说明
FileNotFoundError文件不存在
PermissionError权限不足
IsADirectoryError期望文件但路径是目录
NotADirectoryError期望目录但路径是文件
UnicodeDecodeError编码错误
FileExistsError文件已存在(x 模式)

L2 实践层:用好

推荐做法

做法原因示例
始终用 with 语句打开文件自动关闭,异常时也不泄漏with open("f.txt") as f:
明确指定 encoding="utf-8"默认编码因系统而异,Windows 可能是 GBKopen("f.txt", encoding="utf-8")
优先用 pathlibread_text()/write_text()open 更简洁,自动管理资源Path("f.txt").read_text(encoding="utf-8")
大文件逐行迭代,不用 readlines()readlines() 一次性加载全文到内存for line in f:
写追加日志用 "a" 模式"w" 每次会覆盖全文,日志丢失open("app.log", "a", encoding="utf-8")
创建目录加 exist_ok=True目录已存在不报错Path("out").mkdir(parents=True, exist_ok=True)

实际应用示例

python
from pathlib import Path
import datetime

# 安全读取文件(带默认值)
def read_file(path: str | Path, default: str = "") -> str:
    p = Path(path)
    if not p.exists():
        return default
    return p.read_text(encoding="utf-8")

# 追加日志
def log(message: str, log_file: str = "app.log") -> None:
    ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with open(log_file, "a", encoding="utf-8") as f:
        f.write(f"[{ts}] {message}\n")

# 批量处理某目录下所有 txt 文件
for txt in Path("data").rglob("*.txt"):
    content = txt.read_text(encoding="utf-8")
    print(f"{txt.name}: {len(content)} 字符")

反模式:不要这样做

python
# ❌ 不用 with,忘记 close() 导致文件句柄泄漏
f = open("data.txt")
content = f.read()
# 如果这里抛异常,f.close() 永远不会被调用

# ✅
with open("data.txt", encoding="utf-8") as f:
    content = f.read()

# ❌ 不指定编码(在 Windows 上可能读出乱码)
content = open("中文.txt").read()

# ✅
content = open("中文.txt", encoding="utf-8").read()

# ❌ 大文件一次性读入内存
lines = open("huge.log").readlines()   # 1GB 日志直接 OOM

# ✅ 逐行迭代
with open("huge.log", encoding="utf-8") as f:
    for line in f:
        process(line)

# ❌ 日志写入用 "w" 模式(每次覆盖)
with open("app.log", "w") as f:
    f.write("新事件\n")    # 历史日志全丢失

# ✅ 追加模式
with open("app.log", "a", encoding="utf-8") as f:
    f.write("新事件\n")

常见陷阱

陷阱现象解决方案
不指定 encodingWindows 读出乱码,\u 转义字符始终加 encoding="utf-8"
"w" 写日志每次运行历史日志被清空改用 "a" 追加模式
readlines() 读大文件内存溢出(OOM)for line in f: 逐行迭代
路径字符串含 \n\tWindows 路径 C:\new\table.txt 被转义用原始字符串 r"C:\new\table.txt"Path
不捕获 FileNotFoundError文件不存在时程序崩溃path.exists() 检查或 try/except
复制文件用 read + write二进制文件(图片)可能损坏shutil.copy2()

适用场景

场景推荐方式说明
读写文本文件pathlib.read_text()/write_text()简洁,自动关闭
追加日志open("f", "a") + with不覆盖历史
大文件逐行处理for line in f:内存占用恒定
复制/移动文件shutil.copy2() / shutil.move()保留元数据
递归查找文件Path.rglob("*.py")简洁,返回 Path 对象
临时文件tempfile.NamedTemporaryFile()自动清理,不污染磁盘

L3 专家层:深入

Python 如何实现

Python 的 open() 函数在 CPython 中创建了 io 模块的三层包装结构。每次打开文件时:

┌──────────────────────────────────────────────────────────────┐
│          open() 内部调用链路                                 │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   open("file.txt", "r")                                     │
│     │                                                        │
│     ├→ io.FileIO("file.txt", "r")          // Raw I/O       │
│     │   └→ os.open() → OS 系统调用 open()                     │
│     │                                                         │
│     ├→ io.BufferedReader(FileIO)           // 缓冲层         │
│     │   └→ 默认缓冲区: 8KB (8192 bytes)                      │
│     │   └→ 预读策略: 每次从 OS 读取缓冲区大小                  │
│     │                                                         │
│     └→ io.TextIOWrapper(BufferedReader)    // 文本层         │
│         └→ 编码: UTF-8 codec → PyUnicode 对象                │
│         └→ 换行符: \n ↔ os.linesep 转换                      │
│         └→ 行迭代: 按 \n 分割,延迟读取                       │
│                                                              │
│   seek() 在文本模式下有限制:                                 │
│   · tell() 返回"不透明"位置(非字节偏移)                     │
│   · seek() 只能用 tell() 返回值或 0                           │
│   · 原因:UTF-8 变长编码使字节偏移 ≠ 字符位置                │
│                                                              │
│   二进制模式跳过文本层:                                      │
│   open("file.bin", "rb")                                     │
│     ├→ FileIO                                    // Raw I/O │
│     └→ BufferedReader(FileIO)                    // 仅缓冲   │
│   seek(0)/tell() 返回精确字节偏移                             │
│                                                              │
└──────────────────────────────────────────────────────────────┘
python
# 验证:open() 返回的对象类型
f_text = open('/dev/null' if __import__('os').name == 'posix' else 'nul', 'r')
print(type(f_text))          # <class '_io.TextIOWrapper'>
print(type(f_text.buffer))   # <class '_io.BufferedReader'>
print(type(f_text.buffer.raw))  # <class '_io.FileIO'>

# 验证:默认缓冲区大小
import io
print(f"默认缓冲区: {io.DEFAULT_BUFFER_SIZE} bytes")  # 8192

# 验证:手动控制缓冲
# buffering=0  → 无缓冲(仅二进制模式)
# buffering=1  → 行缓冲(仅文本模式)
# buffering=N  → N 字节缓冲
f_unbuffered = open('/tmp/test.bin', 'wb', buffering=0)
print(type(f_unbuffered))  # <class '_io.FileIO'>(无缓冲层,直接 Raw I/O)
python
# 验证:文本模式 vs 二进制模式性能差异
import io, timeit, tempfile, os

# 生成测试数据
data = "Hello 世界\n" * 10000
data_bytes = data.encode('utf-8')

with tempfile.NamedTemporaryFile(delete=False) as f:
    f.write(data_bytes)
    tmp_path = f.name

# 文本模式读取
t_text = timeit.timeit(
    lambda: open(tmp_path, 'r', encoding='utf-8').read(),
    number=100
)

# 二进制模式读取
t_binary = timeit.timeit(
    lambda: open(tmp_path, 'rb').read(),
    number=100
)

print(f"文本模式 read():     {t_text:.4f}s / 100 calls")
print(f"二进制模式 read():   {t_binary:.4f}s / 100 calls")
# 二进制模式通常快 20-40%(省去 UTF-8 解码开销)

# 验证:缓冲对性能的影响
# 无缓冲 vs 有缓冲写
t_no_buf = timeit.timeit(
    lambda: open(tmp_path, 'wb', buffering=0).write(data_bytes),
    number=100
)
t_with_buf = timeit.timeit(
    lambda: open(tmp_path, 'wb').write(data_bytes),
    number=100
)
print(f"无缓冲 write:  {t_no_buf:.4f}s / 100 calls")
print(f"有缓冲 write:  {t_with_buf:.4f}s / 100 calls")
# 有缓冲通常快 10-50 倍(系统调用从 N 次降为 ceil(N/8192) 次)

os.unlink(tmp_path)

性能考量

操作复杂度说明
open()O(1)单次系统调用,分配缓冲区
f.read() (全读)O(n)n=文件大小,含 decode 开销
f.read(N) (分块)O(N)单次读取 N 字节,缓冲预读
for line in f:O(n)n=总行数+文件大小,逐行延迟解码
f.readline()O(k)k=单行长度,缓冲预读
f.write(s)O(len(s))缓冲后批量写入,flush 时才系统调用
f.seek(pos)O(1)lseek() 系统调用
f.close()O(1)flush 缓冲区 + close() 系统调用
Path.read_text()O(n)等同于 open+read+close,自动管理
Path.write_text(s)O(n)等同于 open+write+close,自动 flush
python
# 验证:逐行迭代 vs readlines 内存对比
import sys

# 假设有一个 100MB 的日志文件
# for line in f:    内存占用 ≈ 缓冲大小 (8KB) + 单行长度
# f.readlines():    内存占用 ≈ 文件总大小 (100MB)

知识关联

┌──────────────────────────────────────────────────────────────┐
│          文件操作知识关联图                                   │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌──────────┐     ┌──────────┐     ┌───────────┐          │
│   │  open()  │────→│    io    │────→│ 系统调用  │          │
│   │  内置    │     │  模块    │     │ open/read │          │
│   └──────────┘     └──────────┘     │  /write   │          │
│        │                │           └───────────┘          │
│        │                │                                    │
│        ↓                ↓                                    │
│   ┌──────────┐     ┌──────────┐     ┌───────────┐          │
│   │ pathlib  │     │ TextI/O  │     │  UTF-8    │          │
│   │  简化API │──→  │ Wrapper  │──→  │  编解码   │          │
│   └──────────┘     └──────────┘     └───────────┘          │
│                                                              │
│   ┌──────────┐     ┌──────────┐     ┌───────────┐          │
│   │  shutil  │     │  高级    │     │ 复制/移动 │          │
│   │          │──→  │  文件操作│──→  │  元数据   │          │
│   └──────────┘     └──────────┘     └───────────┘          │
│                                                              │
│   ┌──────────┐     ┌──────────┐     ┌───────────┐          │
│   │ tempfile │     │  临时    │     │ 自动清理  │          │
│   │          │──→  │  文件    │──→  │  mkstemp  │          │
│   └──────────┘     └──────────┘     └───────────┘          │
│                                                              │
│   选择决策:                                                  │
│   小文本文件 → Path.read_text()(最简洁)                    │
│   大文本文件 → for line in f(恒定内存)                     │
│   二进制文件 → open("f", "rb")(无编解码开销)               │
│   追加日志   → open("f", "a")                                │
│   临时文件   → tempfile                                      │
│                                                              │
│   缓冲区设计原理:                                            │
│   · 默认 8KB 缓冲:平衡系统调用频率和内存占用                 │
│   · 系统调用成本:open/read/write 约 1-10μs                  │
│   · 无缓冲时:每字节一次系统调用 → 极慢                       │
│   · 文本模式额外开销:UTF-8 解码 + 换行符转换                 │
│   · Python 3.10+:TextIOWrapper 内部使用 io._BufferedIO     │
│      实现更高效的字符扫描                                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

本章小结

┌──────────────────────────────────────────────────────────────┐
│                   文件操作 知识要点                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   open() 模式:r 读 / w 写覆盖 / a 追加 / x 创建 / b 二进制  │
│   始终用 with 语句:自动关闭,异常安全                        │
│   始终指定 encoding="utf-8":避免乱码                        │
│                                                              │
│   读取方法:                                                 │
│   read()         全部读入(小文件)                           │
│   readline()     读一行                                      │
│   for line in f  逐行迭代(大文件推荐)                       │
│                                                              │
│   pathlib(推荐):                                          │
│   read_text(encoding="utf-8")  读文本                        │
│   write_text(...)              写文本                        │
│   rglob("*.txt")               递归查找                      │
│   mkdir(parents=True,exist_ok=True)  创建目录                │
│                                                              │
│   异常:FileNotFoundError / PermissionError / UnicodeDecodeError │
│                                                              │
│   L3 要点: io 三层架构 → 缓冲层 8KB → 文本模式 UTF-8 解码    │
│                                                              │
└──────────────────────────────────────────────────────────────┘