简介:面向MATLAB初学者以及需要批量处理表格数据的分析人员,这份资料围绕读取Excel格式数据给出可直接运行的示例,帮助理解数据导入、文件路径设置、变量查看与结果保存等关键环节,适合课程实验、小型数据处理入门和自学练手,也便于教师作为课堂演示素材。压缩包共4个文件,包含m脚本、xlsx示例数据、txt使用说明和docx文档,整体约118KB,脚本与样例数据相互配套,说明文字覆盖运行思路与常见问题,便于按步骤复现并在此基础上替换成自己的表格。已有1982人学习,说明该主题在MATLAB数据处理入门中具有较高实用关注度。读者可获得一套结构简洁的MATLAB读表范例、可替换数据的演示流程以及配套文字说明,既能直接运行观察结果,也可据此修改列范围、表头处理与数据格式,为后续数据清洗和分析打基础。
1. 从一次读表读错列说起
接手别人的 MATLAB 读取 Excel 数据脚本,最容易踩坑的地方往往是不报错:数字列悄悄变成字符、日期列变成 45 开头的整数、前导零被吃掉,脚本一路跑到底,结果全错。这份资料值得先看选型再看代码,它把 readtable、readmatrix、readcell 三条读取路径的边界、参数和常见问题拆开讲,适合手上有一堆 xlsx 台账要清洗的分析人员,也适合刚把数据处理从 Excel 公式搬到 MATLAB 的新手。下面按选函数、固定类型、批量读、排错、校验五步走,每步都给能直接抄的代码和我踩过的坑。
2. 函数选型:readtable、readmatrix、readcell 怎么挑
xlsx 读到 MATLAB 里,第一步不是写循环,是决定读成什么形状。readtable 给 table、readmatrix 给数值矩阵、readcell 给元胞,三者对表头、混列、缺失值的处理策略完全不同,选错了后面全是补丁。老代码里还能见到 xlsread,这个函数在只处理老式 .xls 的年代够用,放到今天既慢又容易在新格式上出问题,除非维护遗留脚本,我基本不碰它。我一般使用的判断顺序是:有表头、列类型不一致,用 readtable;纯数值、行列对齐的矩阵型数据,用 readmatrix;表结构不固定、要先看原样再决定怎么清洗,用 readcell。
2.1 三行代码看清返回值
先别急着写清洗逻辑,用同一个文件跑三种读法,把 class 和尺寸打出来,心里就有数了。这个动作花不到十秒,能省掉后面半小时的调错。
% 同一个工作簿,三种读法的返回值对比 T = readtable('台账.xlsx'); % table:带变量名,每列类型独立 M = readmatrix('台账.xlsx'); % double 矩阵:非数值一律 NaN C = readcell('台账.xlsx'); % cell:原样搬运,不做列类型推断 fprintf('readtable -> %s, %d x %d\n', class(T), height(T), width(T)); fprintf('readmatrix -> %s, %d x %d\n', class(M), size(M,1), size(M,2)); fprintf('readcell -> %s, %d x %d\n', class(C), size(C,1), size(C,2));逻辑说明:三行读的是同一个文件,但 readtable 默认把第一行当变量名,剩下的按列推断类型,一列里只要有非数值就整列退化成 char 或 string;readmatrix 只认数值,表头行会被当成数据读进来,于是矩阵第一行全是 NaN;readcell 不做类型推断,数值是 double、文本是 char、日期可能是 datetime,具体靠检测,读进来之后再 cast。
参数说明:readmatrix 的尺寸一般比 readtable 多一行,多出来的就是表头;如果发现 M 的首行全 NaN,用'Range'参数据跳过表头,别用M(1,:) = []硬删,那样行号会错位。
| 函数 | 返回类型 | 表头处理 | 混列表现 | 典型场景 |
|---|---|---|---|---|
| readtable | table | 默认当变量名 | 整列退化,可强制指定 | 台账、报表、多类型字段 |
| readmatrix | double 矩阵 | 当数据处理 | 非数值变 NaN | 光谱、仿真、纯数值块 |
| readcell | cell | 当数据处理 | 原样保留 | 结构未知、先看后洗 |
| xlsread | 三返回值 | 分开返回 | 靠 raw 兜底 | 只维护老脚本 |
2.2 工作表与区域:Sheet、Range、DataRange 的区别
实际业务里的 xlsx 很少是「A1 起点、单表、干净表头」。常见情况是前三行是标题和说明、数据从第 4 行开始、目标 sheet 藏在第十几个标签页里。这时候靠'Sheet'和'Range'两个参数定位,比读进来再切更省内存,也更不容易错位。
% 先检测,再改检测结果,最后读——这是最稳的三段式 opts = detectImportOptions('月报.xlsx', 'Sheet', '2024Q1'); opts.DataRange = 'A4:H5000'; % 数据区:A4 到 H5000 opts.VariableNamesRange = 'A3:H3'; % 表头单独指定 opts.VariableNamingRule = 'preserve'; T = readtable('月报.xlsx', opts);逻辑说明:detectImportOptions 会先去扫一遍文件,给出它认为合理的列类型和范围;你在这个基础上改,比直接传一堆 name-value 参数可控得多。DataRange 管数据区,VariableNamesRange 管表头,两者可以不相邻,中间隔着说明行也没关系。
参数说明:'Sheet'接受工作表名字符串或正整数序号,序号从 1 开始,按工作簿标签顺序数,隐藏表也占位,所以按序号定位容易错,能用名字就用名字。Range 的写法有几种:'A1:D10'是矩形块,'B:B'是整列,'3:5'是整行,'A1'表示从该点一直读到数据末尾。矩形的右下角不要写太死,写小了会截断,我一般写大一点:'A4:H100000',多出来的空行 readtable 会自动去掉。
2.3 一个能直接复用的读取模板
每写一个新脚本就复制一遍参数太麻烦,把常用设置封装成函数,参数留默认值,调用时只传关心的那几个。这个模板我在多个项目里用了几年,改动很小。
function T = readSheetSafe(file, sheet, dataRange, varNamesRange) % 带默认值的 Excel 读取封装 % file : 文件路径,建议用 fullfile 拼 % sheet : 工作表名或序号,留空默认第 1 个 % dataRange : 数据区,如 'A4:H5000',留空则由检测决定 % varNamesRange : 表头区,如 'A3:H3',留空则由检测决定 if nargin < 2 || isempty(sheet), sheet = 1; end if nargin < 3, dataRange = ''; end if nargin < 4, varNamesRange = ''; end opts = detectImportOptions(file, 'Sheet', sheet); if ~isempty(dataRange), opts.DataRange = dataRange; end if ~isempty(varNamesRange), opts.VariableNamesRange = varNamesRange; end opts.VariableNamingRule = 'preserve'; % 保留原始表头,别自动改写 opts.TextType = 'string'; % 文本统一成 string,避免 char 混用 T = readtable(file, opts); end逻辑说明:函数只做三件事——补默认值、改检测选项、调用 readtable。真正的清洗逻辑放在外面,读取函数保持无副作用,出问题时容易二分定位。TextType设成 string 是为了让后面所有文本列类型一致,ismissing和strcmp都能直接用在 string 上,不用在 char 和 cellstr 之间来回转换。
参数说明:VariableNamingRule = 'preserve'会保留中文表头和带括号的列名,好处是列名可读,代价是访问时要写T.("单价(元)"),不能直接用点号。如果后续要做大量列运算,建议在这个函数外面再套一层改名逻辑,按固定规则映射成英文短名,比如T.Properties.VariableNames = {'id','name','price'},索引稳定,代码也短。
3. 固定列类型:表头改写、混列和日期序列号
readtable 的类型推断在干净数据上很准,在业务台账上经常判断错误。一列「金额」里混了两个「待确认」,整列变 string;一列「日期」里有个手工填的「-」,整列退化成 char。这些都不是 bug,是推断策略的必然结果。可控的做法是显式告诉它每列是什么,而不是读进来再 cast——读进来再转,中间那次转换可能已经丢信息了。
3.1 表头被改成 x 开头:VariableNamingRule 的默认行为
默认的'modify'会把不能做变量名的字符替换掉:空格、括号、斜杠、单位符号都在处理范围内,以数字开头的列名会被加前缀。于是「2024年销量」变成x2024年销量,「单价(元)」里的括号被替换。列名一变,后面按名字取列的代码全部失效,这是交接脚本里最常见的报错来源。
% 看默认改写成了什么,再决定要不要 preserve optsDefault = detectImportOptions('价格表.xlsx'); disp(optsDefault.VariableNames) % 改写后的列名 optsKeep = detectImportOptions('价格表.xlsx', 'VariableNamingRule', 'preserve'); T = readtable('价格表.xlsx', optsKeep); disp(T.Properties.VariableNames) % 原始列名 % preserve 之后按原名列访问,必须用动态字段名 price = T.("单价(元)");逻辑说明:detectImportOptions的第二个参数可以直接传VariableNamingRule,不用先建 opts 再赋值。preserve 之后列名保持原样,点号访问会报「无效字段名」,改用括号加字符串的动态字段名语法就能取到。
参数说明:preserve 和 modify 二选一,没有中间态。如果表头里有重复列名,两种模式都会加后缀区分,比如金额和金额_1,做列对齐时要留意这个后缀,它会让ismember判断失效。我的习惯是读进来立刻打印一次列名,肉眼扫一遍有没有奇怪的Var开头或者_1后缀,比事后调试便宜。
3.2 强制列类型:setvartype 与混列的处置
金额列里混文本、编号列里混空格,这类问题的正解是先声明类型再读,而不是读完再清洗。声明之后,不符合的内容按缺失值处理,位置不乱、行数不变,后续统计才有意义。
opts = detectImportOptions('订单.xlsx'); opts = setvartype(opts, {'订单号','客户名称'}, 'string'); % 强制文本 opts = setvartype(opts, {'金额','数量'}, 'double'); % 强制数值 % 把几种常见的占位符统一当缺失值 opts = setvaropts(opts, '金额', 'TreatAsMissing', {'NA','--','待确认',''}); opts = setvaropts(opts, '客户名称', 'TreatAsMissing', {'-'}); T = readtable('订单.xlsx', opts); fprintf('缺失单元格:%d 个\n', sum(ismissing(T), 'all'));逻辑说明:setvartype改的是整列的目标类型,setvaropts改的是单列的细节选项。TreatAsMissing 的作用是把指定字符串识别成 missing,数值列得到 NaN、文本列得到<missing>,而不是让它们变成字符串混在数值里。
参数说明:TreatAsMissing 的取值要对着真实数据列,别凭想象写。我一般先readcell读一次,unique一下目标列的取值,把非数字的挑出来,再回头写这个列表。文本列声明成 string 之后,ismissing认<missing>但不认空字符串"",要用T.客户名称 == ""单独判断空串,这是两个不同的东西。
3.3 日期与缺失值:45xxx 序列号、NaT 和空单元格
Excel 内部把日期存成从 1900-01-01 起算的序列号,45000 左右对应 2023 年前后。readtable 的大部分情况能自动识别并转成 datetime,但列里只要混进一个「见附件」或者合并单元格留下的空,检测就可能放弃转换,整列变成 double 或 char,日期就成了 45xxx。
opts = detectImportOptions('考勤.xlsx'); opts = setvaropts(opts, '打卡时间', 'Type', 'datetime', ... 'InputFormat', 'yyyy-MM-dd HH:mm:ss', ... 'DatetimeFormat', 'yyyy-MM-dd HH:mm:ss'); T = readtable('考勤.xlsx', opts); % 已经是序列号的情况,按 1900 日期系统换算 if isnumeric(T.打卡时间) T.打卡时间 = datetime(T.打卡时间, 'ConvertFrom', 'excel'); end disp(sum(ismissing(T.打卡时间))) % 空单元格会变成 NaT逻辑说明:InputFormat是告诉解析器源数据长什么样,格式对不上就记 NaT,不会瞎猜;DatetimeFormat只是显示格式,不影响存储值。第二段用'ConvertFrom','excel'处理已经读成数值的情况,这个转换按 1900 日期系统换算,如果源文件是按 1904 日期系统保存的,整体会差 4 年零 1 天,这种时候不要硬加偏移量,回 Excel 里确认工作簿设置更靠谱。
参数说明:空单元格在 datetime 列里是 NaT,数值列里是 NaN,文本列里是<missing>,三者都用ismissing判断,但只对同类型列有效。统计缺失率时按列做,别对整个表sum(isnan(...)),table 里有文本列会直接报错。
4. 批量与性能:多 Sheet、大文件和回写
单表读通之后,真正花时间的是批量。一个月十几个工作簿、每个工作簿十几个 sheet,手工改路径不现实;表大到几十万行时,一次性 readtable 又可能把内存拉满。这一章讲三件事:怎么循环读整本工作簿、大表怎么切片、结果怎么回写到 xlsx。
4.1 一次读完整本工作簿:sheetnames 加循环
sheetnames返回字符串数组,顺序和工作簿里的标签顺序一致,直接拿来遍历最省事。注意它读的是文件名,不是打开的文件,路径必须完整。
file = '月报汇总.xlsx'; names = sheetnames(file); % 字符串数组,不是 cell allData = cell(numel(names), 1); % 先用 cell 收集,最后再合并 for k = 1:numel(names) opts = detectImportOptions(file, 'Sheet', names(k)); opts.VariableNamingRule = 'preserve'; opts.TextType = 'string'; allData{k} = readtable(file, opts); fprintf('%-14s %6d 行 %3d 列\n', names(k), ... height(allData{k}), width(allData{k})); end逻辑说明:sheetnames(file)只接受文件路径,返回的是 string 数组,names(k)拿到的仍是 string,可以直接传给'Sheet'。循环里每次都重新 detectImportOptions,是因为不同 sheet 的表头和列数可能不一样,共用一套 opts 会读错列。
参数说明:合并之前先检查各表的列名是否一致,allData{k}.Properties.VariableNames打出来对比,列名或顺序不同就直接vertcat会得到错位数据。列名一致时用vertcat(allData{:})一次合并,比循环里T = [T; newT]快一个量级。表头行数不一致的,用'Range'按 sheet 单独指定,别指望一套参数通吃。
4.2 大文件的读法:分块、Range 与 datastore 的边界
几十万行的 xlsx 一次性读进来,内存占用大概是文件体积的十几倍,中间还有解析开销。分块读的关键是每块都用 Range 限定,收集到 cell 里,最后一次合并,避免在循环里反复复制大表。
file = '日志.xlsx'; blockSize = 50000; startRow = 2; % 第 1 行是表头 blocks = {}; while true rng = sprintf('A%d:Z%d', startRow, startRow + blockSize - 1); blk = readtable(file, 'Range', rng, 'ReadVariableNames', false); if isempty(blk), break; end % 读到末尾,退出 blocks{end+1} = blk; %#ok<SAGROW> startRow = startRow + blockSize; end T = vertcat(blocks{:}); % 一次性合并 T.Properties.VariableNames = {'时间','编号','类型','数值'}; % 补表头逻辑说明:'ReadVariableNames', false让每块都把第一行当数据读,避免第二块开始表头被吞掉;收集到 cell 里再加一个块,末尾一次vertcat,内存峰值只出现在合并那一步,比循环内拼接省很多。退出条件是读到空块,Range 超出数据区时 readtable 返回空表,正好用来判断结束。
参数说明:blockSize按内存调,一般 5 万到 20 万行之间。列数很多(超过 50 列)时把 Z 换成更靠后的列字母,Range 写小了会静默截断,读出来的表列数对不上才发现。另外,tabularTextDatastore只吃 csv 和 txt,xlsx 用不了,数据量大到几十万行以上时,我一般的做法是先用 Excel 另存成 csv,再走 datastore,速度差好几倍。
4.3 回写:writetable 的 Sheet、Range 与 WriteMode
读完之后把清洗结果写回去,最容易出的问题是覆盖了别人的原始数据。writetable 的WriteMode决定这个行为,默认值不是追加。
% 写入指定 sheet 的指定位置,覆盖该 sheet 原有内容 writetable(T, '结果.xlsx', 'Sheet', '清洗后', 'Range', 'B2', ... 'WriteVariableNames', true, 'WriteMode', 'overwritesheet'); % 追加到已有数据下方(要求列结构一致) writetable(T, '结果.xlsx', 'Sheet', '清洗后', 'Range', 'A1', ... 'WriteMode', 'append', 'WriteVariableNames', false);逻辑说明:'overwritesheet'会清空目标 sheet 再写,'append'从已有数据的最后一行往下接。Range在 overwritesheet 模式下表示写入起点,不是覆盖范围,这一点容易误解——写'B2'不代表只动 B2 那一块。
参数说明:'WriteVariableNames'在 append 模式下必须设 false,否则表头会被当成一行数据追加进去,第二次运行就多一行表头。'Sheet'指定的工作表不存在时,writetable 会新建一个,不会报错。还有个细节:写入会打开文件句柄,如果同一个文件正被 Excel 打开着,写入会失败并报权限错误,批量写之前先确认没有进程占用。
5. 避坑与排查:五类读表引发的问题
下面五条是我在实际项目里真实遇到过的,按「现象 → 原因 → 解决」记录,每条都给了可执行的检查手段。遇到读表结果不对,对着这五条过一遍,八成能定位。
5.1 现象:编号列变成数字,前导零全部丢失
现象:Excel 里显示0012345,读到 MATLAB 里是12345,和另一张表按编号关联时全部匹配不上。
原因:这一列在 Excel 里本身是数值格式,前导零只是显示格式,值里根本没有零。readtable 读到数值就按 double 处理,零在源头就没了,不是 MATLAB 丢的。
解决:确认 Excel 里该列是不是文本格式。如果源头是数值格式,只能回 Excel 用分列或 TEXT 函数转成文本再读,或者在 MATLAB 里用sprintf('%07d', x)按固定宽度补齐,但补齐的前提是知道原始宽度。读取端做强制声明,让含非数字的行不至于整列退化:
opts = detectImportOptions('订单.xlsx'); opts = setvartype(opts, '订单号', 'string'); % 前提:Excel 里是文本格式 T = readtable('订单.xlsx', opts);顺带一提,MATLAB 里对 double 做num2str后再补零,位数要靠业务规则确定,别用%d猜。
5.2 现象:日期列读成 45000 多
现象:T.日期是一列 45000 左右的数值,datetime相关函数全部报错,画图横轴变成一串数字。
原因:该列在 Excel 里是日期格式,但列里混了空行或文本,类型检测放弃转换,保留原始序列号。也可能是这一列的单元格格式被设成了「常规」,Excel 自己就按数字显示。
解决:先用readcell读一次,看这一列里非日期的值是什么,把它们处理掉;再用setvaropts强制Type为 datetime 并指定 InputFormat。已经是数值的,用datetime(x, 'ConvertFrom', 'excel')换算,换算前先确认工作簿用的是 1900 还是 1904 日期系统,后者会整体差 4 年零 1 天。
5.3 现象:合并单元格只读到左上角,其余是空
现象:表格里「部门」一列用了合并单元格,读进来只有第一行有值,后面全是<missing>,按部门分组统计时数据全丢。
原因:合并单元格在文件里只有左上角那个格子存了值,其余格子物理上是空的。读取函数看到的是一张标准的稀疏表,没有任何信息告诉它这些空格应该被填充。
解决:读取之后按业务规则向下填充,用fillmissing对分类列做前向填充:
T.部门 = fillmissing(T.部门, 'previous');注意fillmissing对 string 列支持'previous',对 cellstr 要先转 string。填充前务必确认首行不为空,否则第一段数据仍然是缺失的。更彻底的做法是在 Excel 端取消合并,或者要求上游按规范出表。
5.4 现象:文件明明在,readtable 报无法打开
现象:路径肉眼看着没问题,exist也返回 2,readtable 还是报错;或者换了台机器就报同样的错。
原因:三个常见来源——路径里有中文或空格但没做规范化、文件正被 Excel 独占打开、网络盘或同步盘的临时锁文件干扰。还有一种隐蔽情况:文件名看起来一样,实际末尾有不可见空格。
解决:按顺序排查,每一步都能排除一类问题。
f = fullfile(pwd, '数据', '月报.xlsx'); disp(exist(f, 'file')) % 0 不存在,2 是文件 disp(char(f)) % 看有没有隐藏字符 finfo = dir(f); disp(finfo.bytes) % 文件大小为 0 说明没同步完'Sheet'名字写错时的报错信息有时候指向文件不存在,误导性很强,先把sheetnames(f)打出来对一遍再判断。
5.5 现象:老脚本里的 xlsread 报错或者慢到无法接受
现象:维护几年前的项目,[num, txt, raw] = xlsread(...)在新版本上报错,或者读一个几万行的文件要等几分钟,CPU 占用不高但就是不返回。
原因:xlsread 在新版本里对 xlsx 的支持依赖额外的转换链路,遇到公式、图表、条件格式丰富的文件容易卡住;它的三返回值设计也逼着调用方做二次拼接。
解决:整体迁移到 readtable,用'Sheet'和'Range'替代原来 xlsread 的位置参数,返回值从三段拼成一张表。迁移时注意旧代码里num和txt往往按列号硬索引,改成按变量名索引更稳。迁移脚本不用一次改完,可以先在旧函数外面包一层,内部改用 readtable,保持返回值结构不变,逐步替换调用点。
6. 校验与进阶:读完之后怎么确认数据没读偏
读取代码跑通不代表数据对,我见过太多「脚本无报错、结果差一半」的案例。所以每次读完,我会固定做四项体检:行数对得上不、列数和列名符合预期不、每列类型是不是我想要的那个、缺失比例是不是在合理范围。这四项用几条命令就能跑完,比事后拿结果反推便宜得多。
function checkTable(T, expectRows, expectCols) % 读表后的四项基础校验 assert(istable(T), '返回值不是 table'); assert(height(T) == expectRows, sprintf('行数不符:%d != %d', height(T), expectRows)); assert(width(T) == expectCols, sprintf('列数不符:%d != %d', width(T), expectCols)); types = varfun(@class, T, 'OutputFormat', 'cell'); disp(table(T.Properties.VariableNames', types', ... 'VariableNames', {'列名','类型'})); missRate = sum(ismissing(T), 1) / height(T); bad = find(missRate > 0.3); if ~isempty(bad) warning('以下列缺失超过 30%%:%s', strjoin(T.Properties.VariableNames(bad), ', ')); end end逻辑说明:istable挡住「读成了矩阵」这类低级错误,行数列数的断言在批量处理时特别有用——某个 sheet 少读了一行,通常是 Range 写死了上限。类型表打出来是为了肉眼确认日期列没变成 double、编号列没变成数值。缺失率超过 30% 的列单独告警,这类列往往是读取范围错位或者表头错行。
参数说明:expectRows和expectCols在批量场景下不好硬编码,可以改成和上一期数据对比,行数波动超过 10% 就告警。varfun(@class, T, 'OutputFormat','cell')返回的是每列的类型字符串,比T.Properties.VariableTypes更直观,后者给的是 MATLAB 内部类型名,和实际存储类型有时候对不上。
进阶一点的做法是做一次抽样比对:从 Excel 里手工抄 5 行关键数据,写成常量数组,读完之后用ismember或者按主键 join 一遍,确认这 5 行完全一致。这个方法看起来笨,但能抓住类型转换、编码、四舍五入这几类最难查的问题。下面这张表是我常用的核对清单,按顺序过一遍基本不会有漏网之鱼。
| 检查项 | 命令 | 期望结果 |
|---|---|---|
| 行数 | height(T) | 与源表数据行数一致 |
| 列名 | T.Properties.VariableNames | 无Var/x前缀,无_1后缀 |
| 列类型 | varfun(@class, T, 'OutputFormat','cell') | 日期 datetime、编号 string |
| 缺失率 | sum(ismissing(T),1)/height(T) | 各列在业务合理范围内 |
| 极值 | groupsummary(T, '类别', 'max', '金额') | 无异常大的 45xxx 混入金额列 |
| 抽样 | 手工 5 行 vs 读入 5 行 | 逐字段完全一致 |
有一个我吃过亏的细节:ismissing对 string 列不把""当缺失,只认<missing>,而readtable读空单元格给的恰恰是<missing>,所以你手工在 Excel 里敲一个空格,读进来是" ",不计入缺失但也不等于空串,筛选的时候会漏。这种数据我一般在读取后统一做一次strip,把纯空白转成 missing,避免统计口径打架。
从那以后我每次读完 Excel 都强制走一遍checkTable,行数、列名、类型、缺失率四项不打印出来就不往下写清洗逻辑。这个习惯救过我好几次——有一次上游偷偷加了一列备注,所有按列号索引的代码全部右移一位,结果全错但没有任何报错,就是靠列名核对当场发现的。希望帮到你。
本文还有配套的精品资源,点击获取