🐍 Day 7:文件 I/O 实战

6 阅读5分钟

📌 一句话提纲

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 探测编码处理未知编码文件时先检测