这一章在干嘛?
第 3 章你已经会用
load/save读写「规整矩阵」数据文件了。但真实世界的数据文件经常一行里混着数字和文字、各行列数不一致,load直接罢工。这一章讲的就是 MATLAB 的底层文件 I/O(lower-level file I/O):先fopen拿到文件标识符(file identifier, fid),再逐行或整块读写,最后fclose关闭。学完你就能处理任意格式的文本数据 —— 而且整套流程和 C 语言的fopen/fscanf/fprintf/fclose几乎一模一样,C 程序员可以无缝衔接。
9.1 用 MAT 文件保存变量
先补一块前面欠的拼图:MAT 文件(MAT-file,扩展名 .mat)。它和数据文件完全是两码事 —— 数据文件只存「裸数据」,而 MAT 文件连变量名带变量值一起存。这相当于 C 里把整个内存结构序列化下来,只在 MATLAB 内部使用,不用于和别的程序交换数据。
>> mymat = rand(3,5);
>> x = 1:6;
>> y = x.^2;
>> who % 看当前工作区
Your variables are:
mymat x y
>> save sess1 % 全部变量存入 sess1.mat(扩展名自动加)
>> who -file sess1 % 不加载,只查看文件里有哪些变量
Your variables are:
mymat x y
>> save sess2 mymat % 只把 mymat 存入 sess2.mat
>> save -append sess2 x % 追加:把 x 加进 sess2.mat
>> load sess2 % 读回:变量 mymat、x 直接进工作区要点:
save 文件名存全部;save 文件名 变量名存指定的;-append追加(同名变量会被工作区里的新值覆盖)。load 文件名 变量列表可以只加载部分变量。- 文件存到「当前文件夹」,所以先确认路径正确。
什么时候用 MAT 文件?
中间结果太大、每次重算太慢时(比如训练好的权重矩阵、处理过的传感器数据),
save进 MAT 文件,下次load秒恢复。相关变量分组存进不同的 MAT 文件是好习惯,避免load一口气把几十个无关变量全灌进工作区。
9.2 读写电子表格文件
MATLAB 可以直接和 Excel 交互:xlswrite 写、xlsread 读(Windows 下需要装了 Excel)。最简形式:
>> ranmat = randi(100,5,3); % 5x3 随机整数矩阵
>> xlswrite('ranexcel', ranmat) % 写入 ranexcel.xls
>> ssnums = xlsread('ranexcel') % 读回真正的价值在处理数字与文字混排的表格。假设 texttest.xls 内容如下:
| a | 123 | Cindy |
|---|---|---|
| b | 333 | Suzanne |
| c | 432 | David |
| d | 987 | Burt |
>> [nums, txt] = xlsread('texttest.xls')
nums =
123
333
432
987
txt =
'a' '' 'Cindy'
'b' '' 'Suzanne'
'c' '' 'David'
'd' '' 'Burt'注意:数字先进 nums,文本进 txt(元胞数组),且 txt 的形状和原表一样 —— 中间列因为是数字被抽走了,留下空字符串占位。之后可以用循环配合 txt{i,j} 把数据拼回去打印。
常见坑:新版 MATLAB 用 readmatrix/readcell
新版 MATLAB 已推荐用
readmatrix(读数值)和readcell(读元胞)替代xlsread/xlswrite,且不再依赖 Excel 安装。但教材和老代码里全是xlsread,两边都要认识。
9.3 底层文件 I/O:打开与关闭
load 只能读「每行同样格式、全是同类型数据」的规整文件。一旦不规整(比如一行是 5.3 a,数字带字符编码),就必须上底层函数。整个流程和 C 语言完全同构:
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ fopen 打开 │ → │ 读 / 写 / 追加 │ → │ fclose 关闭 │
│ 拿到 fid │ │ (fgetl/fscanf/…) │ │ 检查返回值 │
└─────────────┘ └──────────────────┘ └─────────────┘
9.3.1 fopen 与 fid
fid = fopen('filename', '权限字符串');- 成功:
fid是一个正整数,之后所有读写函数都用它来指代这个文件 —— 概念上就是 C 的FILE*句柄。 - 失败:返回
-1,同时第二个输出参数给出错误信息。 - 权限字符串(permission string):
'r'读(默认,可省)、'w'写、'a'追加。C 语言里的"r+"读写模式这里也支持,加个加号即可。
每次 fopen 之后必须立刻检查返回值 —— 文件名拼错、文件不在当前文件夹,是最常见的失败原因:
>> [fid, msg] = fopen('sample.dat')
fid =
-1
msg =
No such file or directory
>> fid = fopen('samp.dat') % 打开成功
fid =
3为什么第一个打开的文件 fid 是 3?因为 0、1、2 已被 MATLAB 预留:0 = 标准输入(stdin)、1 = 标准输出(stdout,即屏幕)、2 = 标准错误(stderr) —— 和 C 语言的习惯完全一致。你平时写的 fprintf('...\n') 没给 fid,默认就是往 fid=1 的屏幕打。
还有个看不见但重要的概念:文件位置指示器(file position indicator)。fopen 成功后,指示器停在文件开头;每读/写一段数据,它就往后移动。fgetl 读一行它就移到下一行行首,feof 判断的就是它是否已到达文件末尾 —— 和 C 里文件流内部的读写指针是一个东西。这也解释了为什么循环里连续调 fgetl 能依次读出每一行:位置指示器一直在往前走,不会重复读。
如果把 fid 看成 C 的 FILE*,这一整套映射关系如下:
| MATLAB | C 语言 | 说明 |
|---|---|---|
fid = fopen('f','r') | fp = fopen("f","r") | 打开文件拿句柄 |
fid == -1 检查 | fp == NULL 检查 | 打开失败判断 |
aline = fgetl(fid) | fgets(buf,n,fp) | 逐行读 |
fprintf(fid,...) | fprintf(fp,...) | 格式化写(语法几乎照搬) |
fscanf(fid,...) | fscanf(fp,...) | 格式化读 |
fclose(fid) | fclose(fp) | 关闭 |
feof(fid) | feof(fp) | 是否到文件尾 |
9.3.2 fclose
closeresult = fclose(fid); % 关指定文件,成功返回 0,失败返回 -1
closeresult = fclose('all'); % 关掉所有打开的文件完整的「标准骨架」长这样,建议形成肌肉记忆:
fid = fopen('filename', 'r');
if fid == -1
disp('File open not successful')
else
% ... 读写文件 ...
closeresult = fclose(fid);
if closeresult == 0
disp('File close successful')
else
disp('File close not successful')
end
end常见坑:打开后不关闭
忘了
fclose的后果和 C 里一样严重:文件句柄泄漏,写入的数据可能还留在缓冲区没落盘(甚至损坏文件)。MATLAB 退出时通常会帮你收尾,但**「谁打开谁关闭」是底线**。另外用'w'打开一个已存在的文件会清空它的全部内容,想要保留原内容往末尾加数据,必须用'a'—— 这是本章第一大坑。
交互式容错:让用户输入文件名
实际程序里,文件名常常是运行时由用户敲进来的,拼错是家常便饭。标准写法是把 fopen 放进循环里反复重试,直到成功为止:
% 循环提示,直到拿到一个能打开的文件
filename = input('请输入文件名: ', 's');
fid = fopen(filename);
while fid == -1
fprintf('打不开 %s,请检查拼写和路径\n', filename);
filename = input('请重新输入文件名: ', 's');
fid = fopen(filename);
end
% ... 到这里才安全,可以放心读文件这个模式回答了一个常见疑问:「为什么打开一定要检查、读的时候却不用?」因为 fopen 失败只是「拿不到句柄」,返回 -1 你还有机会挽救(让用户重输);而拿着 -1 去读,程序只会以更难理解的方式崩掉。在错误发生的第一现场处理它,是文件 I/O 编程的基本素养。
9.4 逐行读取:fgetl
fgetl(fid) 每次从文件读一行进一个字符串(自动去掉行尾换行符);它的兄弟 fgets 会保留换行符。因为一次只读一行,必然放进循环里,循环条件用 feof(fid) —— 到达文件末尾(end of file)时返回逻辑真:
while ~feof(fid) % "还没到文件尾" 就继续读
aline = fgetl(fid);
% 用字符串函数拆解 aline、处理数据
end实战:数据文件 subjexp.dat 每行是「一个数 + 空格 + 一个字符」,load 对它无能为力:
5.3 a
2.2 b
3.3 a
4.4 a
1.1 b
用 fgetl 逐行读、strtok 拆分、str2double 转数值:
% fileex.m —— fgetl 逐行读取示例
fid = fopen('subjexp.dat');
if fid == -1
disp('File open not successful')
else
while feof(fid) == 0
aline = fgetl(fid); % 读一行字符串
[num, charcode] = strtok(aline); % 按空格拆成两段
fprintf('%.2f %s\n', str2double(num), charcode)
end
closeresult = fclose(fid);
if closeresult == 0
disp('File close successful')
end
end运行结果:
>> fileex
5.30 a
2.20 b
3.30 a
4.40 a
1.10 b
File close successfulstrtok 返回的两段都还是字符串,要参与计算必须先 str2double 转成 double —— 这一步初学者最容易漏。
顺带一提 fgetl 与 fgets 的唯一区别:fgets 会把行尾的换行符一起读进字符串,fgetl 则把它剥掉。处理数据一般用 fgetl 省心;但有些场合(比如原样复制文件、统计行长度)反而需要保留换行符。
还有一个高频实用模式:文件第一行是标题,后面才是数据。比如 vendorcust.dat:
Vendor Customer
Acme XYZ
Tulip2you Flowers4me
处理办法很简单 —— 先用 fgetl 把标题行「吃掉」,再交给 textscan 读正文:
fid = fopen('vendorcust.dat');
if fid == -1
error('File open not successful');
end
fgetl(fid); % 跳过标题行
data = textscan(fid, '%s %s');
fclose(fid);fgetl 读到的那行标题没接变量,直接丢弃 —— 这个「读一行扔一行」的技巧在处理带表头的 CSV、日志文件时天天用。
为什么「逐行」比「整块」更灵活?
逐行读取给了你对每一行的完全控制权:格式不一致的行可以单独处理,注释行可以跳过,坏数据可以容错。代价是代码长、要自己拆字符串。如果文件格式规整,后面讲的
fscanf/textscan一条语句就能读完整个文件。取舍原则:格式越乱,越值得逐行。另外读文件务必用while ~feof这种条件循环,别用for—— 因为你通常不知道文件有多少行。
9.5 写文件与追加:fprintf
你已经用了一整本书的 fprintf 其实就是文件输出函数!只是没给 fid 时默认写到屏幕(fid=1)。给它一个以 'w' 或 'a' 打开的文件 fid,输出就落到文件里:
>> fid = fopen('tryit.txt', 'w');
>> for i = 1:3
fprintf(fid, 'The loop variable is %d\n', i);
end
>> fclose(fid);写矩阵时有一个反直觉的细节:fprintf 是按列消费矩阵的。下面的 %d %d\n 格式串会把 2×4 矩阵写成 4 行、每行 2 个数 —— 相当于把矩阵转置了:
>> mat = [20 14 19 12; 8 12 17 5]
>> fid = fopen('randmat.dat', 'w');
>> fprintf(fid, '%d %d\n', mat);
>> fclose(fid);
>> type randmat.dat
20 8
14 12
19 17
12 5读回来时 load 会得到 4×2 矩阵,转置一下就还原了。想直接写出原形状,写之前先 mat' 转置即可。
追加模式把数据写到文件末尾,原内容保留:
fid = fopen('filename', 'a'); % 'a' = append,绝不丢原有数据常见坑:该用 'a' 时用了 'w'
想给日志文件追加第 100 条记录,结果
'w'一开、前 99 条全没了,且无法恢复。写代码前先问自己:这个文件是要「重写」还是「续写」。
9.6 整块读取:fscanf 与 textscan
这两个函数介于 load 和 fgetl 之间:文件仍需 fopen/fclose,但不需要循环,一次调用读完整个文件。
9.6.1 fscanf:读进矩阵
mat = fscanf(fid, '格式串', [维度]);关键规则:fscanf 把文件每一行读进矩阵的一列(按列填充),格式串描述的是文件里每一行的样子。还是 subjexp.dat:
>> fid = fopen('subjexp.dat');
>> mat = fscanf(fid, '%f %c', [2, inf]) % 2 行、列数自动
mat =
5.3000 2.2000 3.3000 4.4000 1.1000
97.0000 98.0000 97.0000 97.0000 98.0000
>> fclose(fid);
>> nums = mat(1,:);
>> charcodes = char(mat(2,:))
charcodes =
abaab两个细节要嚼透:
- 矩阵不能装混合类型,所以字符被存成 ASCII 码(
'a'=97),要用char转回字符。 - 格式串里的空格很重要。
'%f %c'匹配「数、空格、字符」;写成'%f%c'的话,%c会把空格本身当字符读走(ASCII 32),后续解析全乱。此外%f这类数值格式会自动跳过空白(含换行符),但%c是连换行符也照读的 —— 所以在逐行格式严格的文件里,格式串末尾常常要显式写\n。
9.6.2 textscan:读进元胞数组
cellarray = textscan(fid, '格式串');textscan 把文件的每一列读成元胞数组里的一个列向量 —— 类型天然分开,不用再从 ASCII 码转回来:
>> fid = fopen('subjexp.dat');
>> subjdata = textscan(fid, '%f %c');
>> fclose(fid);
>> subjdata
subjdata =
[5x1 double] [5x1 char]
>> subjdata{1}(3) % 花括号取元胞内容,圆括号再取向量元素
ans =
3.3000访问要两层索引:subjdata{1}(3) —— 花括号拆元胞,圆括号取向量元素。返回的向量是列向量,这也是 textscan 的固定行为。
fscanf vs textscan 怎么选?
数据能装进同一个数值矩阵(容忍 ASCII 码形式)→
fscanf;各列类型不同、想各拿各的向量 →textscan(更常用,也是 MATLAB 官方更推荐的方向)。两者共同前提:文件每行格式一致。格式不一致,回退到 9.4 的 fgetl 逐行方案。
9.7 四种读文件方法总结
用同一个例子串起全部方法。文件 xypoints.dat 存点的坐标,格式很别扭:每行是 x2.3y4.56 这样的「字符夹数字」:
x2.3y4.56
x7.7y11.11
x12.5y5.5
目标是把 x、y 坐标各抽成一个向量用于绘图。四种写法:
% 方法一:fgetl 逐行 + 字符串拆解(最通用,代码最多)
x = []; y = [];
while ~feof(fid)
aline = fgetl(fid);
aline = aline(2:end); % 去掉开头的 'x'
[xstr, rest] = strtok(aline, 'y'); % '2.3' 和 'y4.56'
x = [x str2double(xstr)];
y = [y str2double(rest(2:end))];
end
% 方法二:fscanf,字符也读进矩阵(注意末尾 \n 必须写!)
mat = fscanf(fid, '%c%f%c%f\n', [4, inf]);
x = mat(2,:); y = mat(4,:);
% 方法三:fscanf,把已知的 'x' 'y' 直接写进格式串(不读入矩阵)
mat = fscanf(fid, 'x%fy%f\n', [2, inf]);
x = mat(1,:); y = mat(2,:);
% 方法四:textscan,格式串描述「列」的样子,无需 \n
xydat = textscan(fid, 'x%fy%f');
x = xydat{1}; y = xydat{2};
plot(x, y, 'k*') % 四种方法之后都是同样的绘图收尾对比着看就很清楚了:fgetl 是「逐行苦力但万能」,fscanf 是「一张矩阵打天下但字符变 ASCII」,textscan 是「按列分家、格式串写起来最舒服」。此外还有两个本文没展开的工具值得知道:dlmread/dlmwrite 读写任意分隔符的 ASCII 文件;MATLAB 的 Import Tool(导入工具)则在图形界面下应付各种格式。
| 方法 | 需 fopen/fclose | 需循环 | 读进什么 | 适用场景 |
|---|---|---|---|---|
load | 否 | 否 | 数值矩阵 | 每行格式统一、纯数字 |
fgetl | 是 | 是 | 字符串(每行一个) | 格式乱、需要精细控制 |
fscanf | 是 | 否 | 矩阵(字符→ASCII) | 每行格式统一,列数未知可用 inf |
textscan | 是 | 否 | 元胞数组(按列分向量) | 每行格式统一、混合类型 |
综合实战:把一个「脏」文件读成结构体
把本章工具串一遍。假设 patwts.dat 存病人体重,每行是「名 姓 体重」:
Darby George 166.2
Helen Dee 143.5
Giovanni Lupa 192.4
Cat Donovan 215.1
每行有三个字段且含文字,load 出局。行格式统一,textscan 一网打尽;再配合循环打印:
% readpatwts.m —— textscan 读混合文件并统计
fid = fopen('patwts.dat');
if fid == -1
error('File open not successful');
end
% 格式串描述「每行的列」:%s 名 %s 姓 %f 体重
data = textscan(fid, '%s %s %f');
fclose(fid); % 读完了就立刻关,别拖到函数末尾
first = data{1}; last = data{2}; wts = data{3};
n = length(wts);
for i = 1:n
fprintf('%s, %s %6.1f\n', last{i}, first{i}, wts(i));
end
fprintf('The average weight is %.2f\n', sum(wts)/n);这段代码的骨架值得背下来:fopen 检查 → textscan 一次读完 → 立刻 fclose → 对内存里的向量做统计。文件 I/O 和数据处理被干净地分成两段,出错时容易定位。
先 fclose 还是最后 fclose?
数据一次性读完的场景,读完就关(如上例),句柄最短时间打开。而
fgetl逐行处理的场景,循环读的过程中文件必须保持打开,只能在循环结束后关。原则:文件保持打开的时间越短越好。
9.8 进阶:二进制读写与文件定位
前面所有函数处理的都是文本文件:数据以人眼可读的字符形式存放,5.3 占 3 个字节。另一条路线是二进制文件:数据按内存中的原始格式直接落盘 —— 一个 double 固定 8 字节,不管数值是 0.5 还是 1e300。两者取舍:
| 文本文件 | 二进制文件 | |
|---|---|---|
| 可读性 | 记事本直接能看 | 乱码,需程序解读 |
| 文件体积 | 大(每个字符占 1 字节) | 小(固定字节数) |
| 读写速度 | 慢(要做格式转换) | 快(原样搬字节) |
| 随机访问 | 麻烦(行长不等) | 容易(每元素定长,偏移可直接算) |
二进制路线的四件套:
fid = fopen('bin.dat', 'w');
fwrite(fid, [1.5 2.5 3.5], 'double'); % 把矩阵按 double 原样写入
fclose(fid);
fid = fopen('bin.dat', 'r');
x = fread(fid, 3, 'double')'; % 按 double 读回 3 个值
frewind(fid); % 位置指示器拨回文件头
fseek(fid, 8, 'bof'); % 或精确跳到第 8 字节(第 2 个 double)
fclose(fid);fwrite(fid, 数据, 精度)/fread(fid, 数量, 精度):按指定精度('double'、'uint8'、'int32'等)成块搬运数据,fread返回列向量。fseek(fid, 偏移, 起点):把位置指示器跳到任意字节(起点'bof'文件头 /'cof'当前 /'eof'文件尾),frewind是跳回头部的快捷方式。- 想同时读和写同一个文件,权限字符串要加加号:
'r+'、'w+'。
给有 C 经验的读者一个忠告:MATLAB 的 fwrite/fread 用「精度字符串」代替了 C 的指针强制转换,读写两端的精度必须配对,一边写 'double' 一边按 'single' 读,出来的就是乱数 —— 这相当于 C 里类型双关(type punning)的事故,MATLAB 同样会发生。
选型口诀
给人看的报告、配置文件、和其他程序交换的数据 → 文本文件(本章 9.3~9.7);大规模数值中间结果、需要随机访问、体积敏感 → 二进制(fread/fwrite)或直接 MAT 文件(save/load,本质也是二进制且额外存了变量名)。
本章通关标准:
- 能默写「fopen → 检查 fid==-1 → 读写 → fclose → 检查返回值」标准骨架,并说清 fid 与 C 语言
FILE*、标准输入输出 0/1/2 的对应关系。- 面对一个「每行数字+字符」的文件,能用 fgetl+strtok 和 textscan 两种方法分别写出读取代码,且不查书。
- 能准确回答:
'w'和'a'的区别?fscanf 为什么按列填充?格式串里%c前的空格为什么不能丢?
1. fopen 返回 -1 一般是什么原因?代码里该怎么处理?
最常见的是文件名拼写错误或文件不在当前文件夹(搜索路径里)。标准做法是判断
fid == -1后打印错误信息或走错误分支,绝不能拿着 -1 继续调用 fgetl/fscanf —— 会得到莫名其妙的错误。可以再用第二个输出参数msg = fopen(...)拿到系统错误描述。
2. load 和 fscanf 的本质区别是什么?
load不需要打开关闭,直接把纯数字规整文件读成矩阵;fscanf需要 fopen/fclose,按格式串解析,能处理数字与字符混合的文件(字符以 ASCII 码进矩阵)。判据就一条:文件规整用 load,不规整才动用底层函数 —— 能用 load/save 就别杀鸡用牛刀。
3.
fscanf(fid,'%f %c',[2,inf])里,[2,inf] 能换成 [inf,2] 吗?为什么?不能。fscanf 把文件的每一行读进矩阵的一列,行数由「每行的数据个数」决定(这里是 2 个:一个数一个字符),而列数(行数)未知,所以只能
[2, inf]。[inf,2]会试图让「文件的行数」当矩阵行数,方向完全反了。
4. textscan 返回的结果怎么访问第 1 列的第 3 个值?
两层索引:
subjdata{1}(3)。subjdata是 1×2 元胞数组,花括号{1}拆出第一个元胞里的数值列向量,圆括号(3)再取向量的第 3 个元素。只写subjdata(1)拿到的还是一个 cell,不能直接当数用。
5. fprintf 写矩阵为什么会「转置」?怎么避免?
fprintf 按列的顺序消费矩阵元素:格式串里有几个转换符,就依次取几个元素,取完一行格式就走
\n,于是 2×4 矩阵用%d %d\n写出来是 4 行 2 列。想保持原形状,写之前先转置fprintf(fid,'%d %d\n', mat')。这也是「先想清楚文件里长什么样,再定格式串」的原因。
6. 为什么读文件推荐 while ~feof 而不是 for 循环?
因为你通常事先不知道文件有多少行。for 循环需要固定的次数,要么读不完要么越界;
while ~feof(fid)以「到达文件尾」为终止条件,多少行都能正确处理。这和 C 里while(fgets(...) != NULL)的惯用法是一个道理。