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