🐍 Day 7:文件 I/O 实战

📌 一句话提纲

Python 文件操作从 open()pathlib,覆盖读写模式、编码处理、上下文管理、缓冲区控制、临时文件等实战场景。


1. 核心函数 open()

基本签名

python 复制代码
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+ 不是追加

python 复制代码
# 错误理解:以为 w+ 可以读又能追加
with open('test.txt', 'w+') as f:
    f.write('hello')        # 写完后指针在末尾
    f.read()                # ❌ 读到空字符串,因为指针在末尾
    f.seek(0)               # ✅ 需要先 seek 回开头
    f.read()                # 才能读到 'hello'

2. 上下文管理器 with

✅ 正确做法(自动关闭)

python 复制代码
with open('data.txt', 'r', encoding='utf-8') as f:
    content = f.read()
# 离开 with 块自动 f.close()

❌ 错误做法(忘记关闭)

python 复制代码
f = open('data.txt', 'r')
content = f.read()
# ... 如果中间异常,f 永远不会关闭
# 文件句柄泄漏,达到上限后报 "Too many open files"

同时打开多个文件

python 复制代码
with open('a.txt') as f1, open('b.txt') as f2:
    for line1, line2 in zip(f1, f2):
        print(line1.strip(), line2.strip())

3. 读写方法详解

读取

python 复制代码
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)

写入

python 复制代码
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. 编码与乱码

编码解码

python 复制代码
# 字符串 ↔ 字节
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'

文件编码检测

python 复制代码
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()

⚠️ 常见乱码场景

python 复制代码
# 场景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

python 复制代码
# 默认有缓冲区(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')  # 立即发送

手动刷新

python 复制代码
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

python 复制代码
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())  # 当前指针位置

⚠️ 文本模式限制

python 复制代码
# 文本模式下 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

python 复制代码
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 文件操作标准

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)

🧪 完整实战:日志分割器

python 复制代码
"""将大日志按日期分割成小文件"""
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 探测编码 处理未知编码文件时先检测
相关推荐
天天喝旺仔15 分钟前
Docker 镜像瘦身实战:多阶段构建把体积缩小 90%
运维·后端·ci/cd·docker·云原生·容器·性能优化
Lost of 程序猿20 分钟前
ASP.NET Core Saga 分布式事务深度实战:备件采购跨服务长流程,如何保证“要么全成,要么全回“
分布式·后端·asp.net
卷无止境23 分钟前
Coding Agent 里的上下文 Compact,到底在压缩什么
后端·python
爱勇宝29 分钟前
公司没给活干,却因为员工看手机把人开了:法院判赔11万元
前端·后端·程序员
2601_9620710034 分钟前
【Java EE】SpringBoot的创建与简单使用
spring boot·后端·java-ee
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 9 章 使用 WebMvc 开发进阶 阅读笔记 24
spring boot·笔记·后端
yunwei371 小时前
AgentCgroup:当 AI Agent 遇到操作系统资源
linux·人工智能·后端
元界metalite1 小时前
SpringBoot开发企业后台-操作日志记录的最佳实践
后端
用户298698530141 小时前
PDF 转纯文本(TXT)免费攻略:轻松提取文字内容
人工智能·后端·c#