📌 一句话提纲
Python 文件操作从 open() 到 pathlib,覆盖读写模式、编码处理、上下文管理、缓冲区控制、临时文件等实战场景。
1. 核心函数 open()
基本签名
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
常用模式一览
| 模式 |
含义 |
指针位置 |
文件不存在 |
r |
只读文本 |
开头 |
报错 |
w |
写入文本(清空) |
开头 |
创建 |
a |
追加文本 |
末尾 |
创建 |
x |
排他创建 |
开头 |
创建,存在则报错 |
r+ |
读写 |
开头 |
报错 |
w+ |
读写(清空) |
开头 |
创建 |
a+ |
读写(追加) |
末尾 |
创建 |
rb |
二进制读 |
开头 |
报错 |
wb |
二进制写 |
开头 |
创建 |
⚠️ 常见坑:w+ 不是追加
# 错误理解:以为 w+ 可以读又能追加
with open('test.txt', 'w+') as f:
f.write('hello') # 写完后指针在末尾
f.read() # ❌ 读到空字符串,因为指针在末尾
f.seek(0) # ✅ 需要先 seek 回开头
f.read() # 才能读到 'hello'
2. 上下文管理器 with
✅ 正确做法(自动关闭)
with open('data.txt', 'r', encoding='utf-8') as f:
content = f.read()
# 离开 with 块自动 f.close()
❌ 错误做法(忘记关闭)
f = open('data.txt', 'r')
content = f.read()
# ... 如果中间异常,f 永远不会关闭
# 文件句柄泄漏,达到上限后报 "Too many open files"
同时打开多个文件
with open('a.txt') as f1, open('b.txt') as f2:
for line1, line2 in zip(f1, f2):
print(line1.strip(), line2.strip())
3. 读写方法详解
读取
with open('data.txt', 'r', encoding='utf-8') as f:
f.read() # 全部读到字符串
f.read(1024) # 读前 1024 字节
f.readline() # 读一行
f.readlines() # 读所有行 → 列表(大文件慎用!)
# ✅ 大文件最佳实践:逐行迭代
with open('large.log', 'r', encoding='utf-8') as f:
for line in f: # 内部缓冲,不会撑爆内存
process(line)
写入
with open('out.txt', 'w', encoding='utf-8') as f:
f.write('hello\n') # 写入单字符串
f.writelines(['a\n', 'b\n']) # 写入多行(不会自动加 \n!)
# ✅ 推荐用 print 函数
with open('out.txt', 'w', encoding='utf-8') as f:
print('hello', file=f) # 自动加换行
print('a', 'b', 'c', sep=',', file=f)
4. 编码与乱码
编码解码
# 字符串 ↔ 字节
s = '你好世界'
b = s.encode('utf-8') # str → bytes
s2 = b.decode('utf-8') # bytes → str
# 编码错误处理
s = '你好'
b = s.encode('ascii', errors='replace') # b'??'
b = s.encode('ascii', errors='ignore') # b''
b = s.encode('ascii', errors='backslashreplace') # b'\u4f60\u597d'
文件编码检测
import chardet
with open('unknown.txt', 'rb') as f:
raw = f.read(10000) # 读前 10KB 检测即可
result = chardet.detect(raw)
encoding = result['encoding'] # 如 'utf-8', 'gb2312'
confidence = result['confidence']
# 用检测到的编码打开
with open('unknown.txt', 'r', encoding=encoding) as f:
content = f.read()
⚠️ 常见乱码场景
# 场景1:GBK 文件用 UTF-8 读 → UnicodeDecodeError
with open('gbk.txt', 'r', encoding='utf-8') as f: # ❌ 报错
pass
# ✅ 解决方法
with open('gbk.txt', 'r', encoding='gbk') as f:
pass
# 场景2:不确定编码时先检测
import chardet
with open('gbk.txt', 'rb') as f:
enc = chardet.detect(f.read(10000))['encoding']
with open('gbk.txt', 'r', encoding=enc) as f:
content = f.read()
5. 缓冲区控制
为什么需要 buffering?
# 默认有缓冲区(8KB),减少磁盘 IO
# 但在某些场景需要立即写入:
# 行缓冲(文本模式 + buffering=1)
with open('log.txt', 'w', buffering=1) as f:
f.write('即时写入\n') # 遇到换行就刷入磁盘
# 无缓冲(适合管道/串口通信)
with open('/dev/cu.usbserial', 'wb', buffering=0) as f:
f.write(b'\x01\x02') # 立即发送
手动刷新
with open('progress.txt', 'w') as f:
for i in range(10):
f.write(f'step {i}\n')
f.flush() # 立即写入磁盘(不关闭文件)
# os.fsync(f.fileno()) # 更激进:强制磁盘物理写入
6. 文件指针移动 seek
with open('data.txt', 'rb') as f:
f.seek(0, 0) # 开头(默认)
f.seek(0, 2) # 末尾
f.seek(10, 0) # 第 10 字节
f.seek(-5, 1) # 当前位置往前 5 字节
f.seek(-10, 2) # 末尾往前 10 字节
print(f.tell()) # 当前指针位置
⚠️ 文本模式限制
# 文本模式下 seek 只能从开头(0)或末尾(2),且末尾只能 seek(0, 2)
with open('data.txt', 'r', encoding='utf-8') as f:
f.seek(10, 0) # ✅ 文本模式只支持这个
f.seek(5, 1) # ❌ 报错!文本模式不支持相对定位
7. 临时文件 tempfile
import tempfile
# 临时文件(自动删除)
with tempfile.TemporaryFile(mode='w+t') as f:
f.write('临时数据')
f.seek(0)
print(f.read()) # 读取写入的内容
# 离开 with 自动删除
# 命名临时文件
with tempfile.NamedTemporaryFile(suffix='.csv', delete=False) as f:
f.write(b'name,age\nAlice,30\n')
temp_path = f.name
print(f'临时文件路径: {temp_path}')
# 临时目录
with tempfile.TemporaryDirectory() as tmpdir:
file_path = Path(tmpdir) / 'test.txt'
file_path.write_text('hello')
8. pathlib --- 当代 Python 文件操作标准
from pathlib import Path
p = Path('data.txt')
# 读写(一行搞定)
p.write_text('hello world', encoding='utf-8')
p.read_text(encoding='utf-8') # 'hello world'
p.write_bytes(b'\x00\x01\x02')
p.read_bytes() # b'\x00\x01\x02'
# 路径操作
p.parent # 父目录
p.stem # 文件名(无后缀):'data'
p.suffix # 扩展名:'.txt'
p.name # 完整文件名:'data.txt'
p.exists() # 是否存在
p.is_file() # 是否文件
p.is_dir() # 是否目录
p.stat() # 文件元信息
# 目录遍历
dir_p = Path('/tmp')
for f in dir_p.iterdir(): # 仅当前层
print(f.name)
for f in dir_p.glob('*.txt'): # 通配符搜索
print(f)
for f in dir_p.rglob('**/*.py'): # 递归搜索
print(f)
# 创建目录
Path('a/b/c').mkdir(parents=True, exist_ok=True)
🧪 完整实战:日志分割器
"""将大日志按日期分割成小文件"""
from pathlib import Path
def split_log_by_date(log_path: str, output_dir: str):
log_file = Path(log_path)
out_dir = Path(output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
current_date = None
current_file = None
with open(log_file, 'r', encoding='utf-8') as f:
for line in f:
# 假设日志格式: [2026-07-15 10:30:00] 消息
if line.startswith('['):
date_part = line[1:11] # 取 '2026-07-15'
if date_part != current_date:
if current_file:
current_file.close()
current_date = date_part
current_file = open(
out_dir / f'{date_part}.log',
'a', encoding='utf-8'
)
if current_file:
current_file.write(line)
if current_file:
current_file.close()
if __name__ == '__main__':
split_log_by_date('app.log', 'split_logs')
📝 要点总结
| 要点 |
说明 |
✅ 始终用 with |
自动关闭,防泄漏 |
✅ 明确 encoding |
文本模式不指定会用平台默认编码(不同系统可能不同) |
| ✅ 大文件逐行迭代 |
for line in f: 而不是 .readlines() |
✅ 用 pathlib |
比 os.path 更现代、更简洁 |
⚠️ w 会清空文件 |
不清空想追加用 a |
⚠️ w+ 指针在开头 |
读之前可能需要 seek(0) |
| ⚠️ 文本模式限制 seek |
只能用 seek(0) 和 seek(0, 2) |
💡 buffering=0 无缓冲 |
适合实时日志、串口通信 |
💡 chardet 探测编码 |
处理未知编码文件时先检测 |