3、文件I/O基础:打开/关闭文件、读取文本与二进制、写入操作、with语句上下文管理

做嵌入式开发的朋友,文件I/O这关必须得过。

我刚开始搞单片机时,总觉得文件操作是上位机的事。后来做数据采集系统,要把传感器数据存到SD卡里,才发现不会文件I/O根本玩不转。说白了,不管是读写配置文件、保存日志,还是处理固件升级包,都离不开文件操作。

3.1 打开与关闭文件:基本功

Python里操作文件,第一步就是打开它。用open()函数,语法很简单:

file_object = open(file_name, mode)

file_name是文件路径,mode是打开模式。我习惯把模式记成三个维度:

  • 读/写/追加'r'读,'w'写(会清空原文件),'a'追加
  • 文本/二进制't'文本(默认),'b'二进制
  • 读写组合'r+'读写,'w+'写读,'a+'追加读

举个例子:

# 打开一个文本文件用于读取
f = open('config.txt', 'r')
# ... 做点什么 ...
f.close()  # 用完一定要关

# 打开二进制文件用于写入
f = open('firmware.bin', 'wb')
# ... 写数据 ...
f.close()
⚠️ 我曾经踩过的坑:忘记调用close()。有一次跑数据采集脚本,开了几百个文件没关,结果文件描述符耗尽了,程序直接崩溃。从那以后,我养成了「开即关」的习惯。

3.2 读取文件:文本 vs 二进制

读取方式取决于你的需求。文本文件用read()readline()readlines(),二进制文件用read()读字节。

文本读取

# 一次性读完整个文件
with open('log.txt', 'r') as f:
    content = f.read()
    print(content)

# 逐行读取,适合大文件
with open('log.txt', 'r') as f:
    for line in f:
        print(line.strip())  # strip()去掉换行符

# 读取所有行到列表
with open('log.txt', 'r') as f:
    lines = f.readlines()
    print(lines[0])  # 只打印第一行

我个人习惯用for line in f这种方式。为什么?因为它是按需读取,不会把整个文件塞进内存。你想想看,如果日志文件有1GB,用read()直接内存爆炸。

二进制读取

做嵌入式免不了要处理二进制文件,比如固件、图片、音频。二进制读取返回的是bytes对象:

# 读取固件文件
with open('firmware.bin', 'rb') as f:
    data = f.read(1024)  # 一次读1024字节
    while data:
        # 处理数据块
        process_firmware_chunk(data)
        data = f.read(1024)

# 读取文件头信息
with open('image.jpg', 'rb') as f:
    header = f.read(4)  # 读前4个字节
    if header == b'\xff\xd8\xff\xe0':  # JPEG文件头
        print('这是一个JPEG文件')
💡 我的经验:处理二进制文件时,建议固定缓冲区大小(比如4096字节),这样既不会太慢,也不会占用太多内存。我在做OTA升级时,就是按4KB一块读取固件,然后校验、写入Flash。

3.3 写入操作:文本与二进制

写入文件用write()方法。文本写入传字符串,二进制写入传字节。

# 文本写入
with open('output.txt', 'w') as f:
    f.write('Hello, 嵌入式世界!\n')
    f.write('这是第二行\n')

# 二进制写入
with open('data.bin', 'wb') as f:
    f.write(b'\x01\x02\x03\x04')  # 直接写字节
    f.write(bytes([0x05, 0x06, 0x07]))  # 用列表转字节

# 追加写入
with open('log.txt', 'a') as f:
    f.write('新日志条目\n')

嗯,这里要注意:'w'模式会覆盖原文件。如果你不想丢失原有内容,用'a'追加模式。我在项目中就犯过这种低级错误——调试时用'w'写配置文件,结果把之前的配置全清了。

3.4 with语句:上下文管理器

这是Python里我最喜欢的特性之一。用with语句,你不需要手动调用close(),它会自动帮你处理。

# 传统写法
f = open('test.txt', 'r')
try:
    data = f.read()
finally:
    f.close()

# with写法(推荐)
with open('test.txt', 'r') as f:
    data = f.read()
# 出了with块,文件自动关闭

说白了,with就是帮你做了「善后工作」。即使中间抛出异常,文件也会被正确关闭。我建议所有文件操作都用with,省心又安全。

同时操作多个文件

# 复制文件
with open('source.bin', 'rb') as src, open('dest.bin', 'wb') as dst:
    while True:
        chunk = src.read(4096)
        if not chunk:
            break
        dst.write(chunk)

你看,一个with管理两个文件,代码简洁又清晰。

3.5 文件指针与定位

文件操作时,内部有个指针记录当前位置。用tell()查看位置,用seek()跳转位置。

with open('data.bin', 'rb') as f:
    print(f.tell())  # 0,文件开头
    
    f.read(10)       # 读10字节
    print(f.tell())  # 10
    
    f.seek(0)        # 回到开头
    print(f.tell())  # 0
    
    f.seek(-5, 2)    # 从末尾往前5字节
    print(f.tell())  # 文件大小-5

seek()的第二个参数:0表示文件开头,1表示当前位置,2表示文件末尾。我在解析文件头时经常用seek()跳来跳去,比如先读文件头,再跳到数据区。

3.6 实战:寄存器配置文件读写

最后来个实战例子。假设我们要读写一个寄存器配置文件,格式是每行一个寄存器:

# reg_config.txt
GPIOA_MODER, 0x00000100
GPIOB_ODR, 0x000000FF
TIM2_CR1, 0x00000001
def load_reg_config(filename):
    """加载寄存器配置文件"""
    config = {}
    with open(filename, 'r') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue  # 跳过空行和注释
            name, value = line.split(',')
            config[name.strip()] = int(value.strip(), 16)
    return config

def save_reg_config(filename, config):
    """保存寄存器配置到文件"""
    with open(filename, 'w') as f:
        for name, value in config.items():
            f.write(f'{name}, 0x{value:08X}\n')

# 使用示例
config = load_reg_config('reg_config.txt')
print(config)  # {'GPIOA_MODER': 256, 'GPIOB_ODR': 255, 'TIM2_CR1': 1}

# 修改一个寄存器
config['GPIOA_MODER'] = 0x00000101
save_reg_config('reg_config.txt', config)

核心要点回顾:

  • open()打开文件,记得关闭或用with
  • 文本模式处理字符串,二进制模式处理字节
  • with语句自动管理资源,强烈推荐
  • 大文件用迭代读取,小文件可以一次读完
  • seek()tell()控制文件指针

文件I/O是嵌入式开发的基础技能。你想想看,从读取配置文件到保存日志,从加载固件到导出数据,哪样离得开文件操作?把这些基础打牢了,后面做自动化脚本、数据处理就顺手多了。