简介:这份MATLAB读取Excel格式数据的方法配套材料,面向刚接触MATLAB、需要把表格数据导入工作区继续分析的学生与工程技术人员,重点解决xlsx数据读取与后续处理之间的衔接问题。压缩包共4个文件,包含MATLAB脚本、示例Excel数据表以及说明文档,整体约118KB,体积轻便,适合课堂演示、课程作业和自学练习时快速取用。其中脚本承担核心读取逻辑,表格文件提供可直接对照的样例数据,文档则补充程序用途与使用提示,三者配合能让读者较快看清数据从Excel进入MATLAB的完整路径。已有1982人学习,说明其在入门场景中具有一定参考价值。读者可据此理解读取函数的基本调用方式、数据组织形态和结果查看方法,并在此基础上替换自己的表格,开展统计、绘图或建模前的数据准备,减少从零摸索的时间。
1. 从一份"能打开但读不对"的表说起:MATLAB 读 Excel 到底难在哪
接手别人的业务表时,用 MATLAB 读取 Excel 格式数据,最容易翻车的场景是:命令跑完不报错,whos一看也确实有变量,可金额列全是NaN,日期列变成45000这样的整数,中文表头被改成了Var3。文件在 Excel 里明明没问题,MATLAB 却像换了个世界看同一份数据。原因不复杂:Excel 的单元格是"带格式的值",MATLAB 拿到的是"值 + 类型推断结果",中间这一步推断由导入器自动完成,而自动推断恰恰是最不透明的地方,一旦猜错,你只会看到一个安静的错误结果,就像黑匣子一样。
这份资源围绕的就是这个问题域:把readtable、readmatrix、readcell、xlsread这几个入口的边界讲清楚,再用可复现的参数把 Sheet 定位、区域裁剪、列名保留、日期还原、缺失值归一这几件事一件件固定下来。适合两类人:刚把数据从 Excel 搬进 MATLAB、被NaN坑过一次的新手;以及要写批处理脚本、需要保证每次读出来的表结构完全一致的熟手。读完你应该能做到:拿到任意一份表,先判断该用哪个函数,再用detectImportOptions把读取行为钉死,只处理需要处理的列,不需要的列一个字节都不读。
2. 四个函数怎么选:readtable、readmatrix、readcell 与 xlsread 的边界
2.1 能力矩阵:先看你要的输出类型,再定函数
很多人选函数是凭习惯,看到 Excel 就条件反射写xlsread,结果在一个混合类型的表上被三个返回值绕晕。正确的顺序反过来:先问自己"读完之后我要拿它做什么",答案决定了输出类型,输出类型决定了函数。
| 函数 | 返回类型 | 列类型处理 | 表头处理 | 适合场景 |
|---|---|---|---|---|
readtable | table | 每列独立推断,可混合 | 默认识别首行为变量名 | 带表头的业务表、后续要按列名取数 |
readmatrix | 数值矩阵double | 只保留数值,其余转NaN | 默认跳过表头 | 纯数值区域、要直接做矩阵运算 |
readcell | 元胞数组cell | 完全不推断,原样搬运 | 不特殊处理,表头也是一行 | 结构不规范、需要自己逐格判断 |
readvars | 多个独立变量 | 同readtable | 变量名即工作区变量名 | 快速把几列拿到工作区,不想写T.xxx |
xlsread | [num, txt, raw] | 数值/文本分开返回 | 需要自己按行偏移 | 维护老脚本、需要raw原始区域 |
选型判断可以压缩成三句话。第一,只要表里有文字列,就优先readtable,它的价值不在于"能读",而在于"按列保住类型",后面T.金额直接就是double,不用再转。第二,只有当整块区域确认是纯数字、并且你要的是矩阵而不是表时,才用readmatrix,它对非数值的处理是静默转NaN,这个特性在清洗阶段是优点,在排查阶段是灾难。第三,readcell是兜底方案,当你连表头在第几行都不确定、列数每份文件还不一样时,用它先把原始格子拿进来,再写代码判断,比让导入器去猜要可控得多。
xlsread单独说,是因为它至今仍大量存在于存量脚本里。它的三返回值设计在当年是合理的:num给你数值矩阵,txt给你文本,raw给你包含表头的原始元胞。问题在于这三份数据的行列索引是对不齐的,num里被跳过的文本单元格会直接消失,导致行号偏移,这是老代码里最常见的血泪经验。新写的脚本不建议再用它。
2.2 同一份表跑四遍:差异是看得见的
与其记结论,不如跑一遍。下面这段代码对同一个文件连读四次,把类型和维度打出来,差异一眼就能看到。
f = 'D:\data\demo.xlsx'; % 换成自己的路径,注意用完整路径 T = readtable(f, 'Sheet', 1); % 表格:列名 + 每列独立类型 M = readmatrix(f, 'Sheet', 1); % 矩阵:非数值一律 NaN C = readcell(f, 'Sheet', 1); % 元胞:原样搬运,零推断 [num, txt, raw] = xlsread(f); % 旧接口:三份数据分开给 fprintf('T : %s, %d 行 x %d 列\n', class(T), height(T), width(T)); fprintf('M : %s, %d 行 x %d 列\n', class(M), size(M,1), size(M,2)); fprintf('C : %s, %d 行 x %d 列\n', class(C), size(C,1), size(C,2)); fprintf('raw : %s, %d 行 x %d 列\n', class(raw), size(raw,1), size(raw,2)); disp(T.Properties.VariableNames) % 看列名被改成什么样 disp(T.Properties.VariableTypes) % 看每列被判成什么类型这段代码的关键在最后两行。VariableNames告诉你导入器有没有擅自改名,VariableTypes告诉你每列被判成了double、char、datetime还是cell。我一般在拿到一份陌生的表时,第一件事就是跑这两行,比直接head(T)有用得多,因为类型错了,看前几行数据往往看不出来。M和raw的行数通常会比T少,差值就是表头占掉的行数,这个差值在排查偏移问题时是重要线索。
参数上需要注意的是'Sheet', 1这种序号写法,序号是工作簿里标签的实际顺序,不是名字排序。如果你手上的文件工作表顺序会变,一律改成按名字取,下一章展开。
2.3 xlsread 还在被用,但只在三种情况下值得留
存量代码里xlsread的调用点,我一般不会一次性全改,而是分三批处理。第一批是那种"读进来就num(2:end,:)直接算"的脚本,这类改动收益最大,换成readtable之后列名语义清晰,出错概率明显下降。第二批是有复杂后处理、短期不敢动的,先加一层包装,保持接口不变。第三批才是真正需要保留xlsread的,只有三种情况。
一是需要raw那个包含完整区域的元胞,且后续逻辑依赖它的行列对齐。二是文件是.xlsb这类readtable默认不处理的二进制格式,需要走 Excel 的接口。三是一些带特殊保护或外部链接的工作簿,用流式读取拿不到值,只能借助 Excel 本身打开。除此之外没有必须用它的理由。
有一个参数值得单独提:readtable和readmatrix都支持'UseExcel',默认是false,走的是内置的表格解析器;设成true时会调用本机的 Excel 程序去打开文件。这个开关的代价是启动开销,一个文件慢几秒,但换来的是对某些异常格式的兼容性。判断标准很简单:单个文件、格式古怪、只读一次,可以开;批量几百个文件,坚决关掉,否则光是 Excel 进程的启停就能把整晚的批处理时间吃掉。
3. 定位与参数:Sheet、Range、列名三处最容易错位
3.1 Sheet 定位:名称、序号和一次读全部
工作表定位看着简单,实际上是一堆脚本里最常见的硬编码来源。'Sheet', 2这种写法在文件被同事调整过一次标签顺序之后就静默读错了表——不报错,数据也像那么回事,只是口径完全不同。
f = 'D:\data\demo.xlsx'; names = sheetnames(f); % 返回 string 数组,按标签实际顺序 disp(names) % 先看清楚有哪些表 T1 = readtable(f, 'Sheet', names(1)); % 按顺序取,适合表名会变的场景 T2 = readtable(f, 'Sheet', '明细'); % 按名字取,最稳,优先用这种 % 一次把多个表读进来,装进结构体,字段名就是表名 S = struct(); for k = 1:numel(names) S.(matlab.lang.makeValidName(names(k))) = ... readtable(f, 'Sheet', names(k), 'VariableNamingRule', 'preserve'); endsheetnames是我每次开工的第一条命令,它的返回值是string数组而不是元胞,所以可以直接索引和strcmp比较,不用再char()转换。第二段用matlab.lang.makeValidName把中文或带空格的工作表名转成合法的字段名,这一步不做的话,S.明细表这种写法在老版本上会直接报错。
需要提醒一点:sheetnames会把隐藏工作表也列出来,而readtable按序号读取时,某些版本的序号计数是否包含隐藏表存在差异。所以只要文件里有隐藏表,就一定用名字读,别用序号。这个坑不常遇到,遇到一次能查半天。
3.2 DataRange 的写法与行列偏移
真实业务表很少是"第一行表头、第二行开始干净数据"。上面压着两三行合并的标题、左边挂着一列序号、右边还有备注栏,这种情况必须用DataRange把有效区域切出来,否则导入器会把合并单元格的空格当成表头的一部分。
% 情况一:标题占了两行,表头在第 3 行,数据到第 100 行 T1 = readtable(f, 'Sheet', '明细', 'DataRange', 'B3:G100'); % 情况二:只知道左上角,行数会随月份增长,让它自己往下读 T2 = readtable(f, 'Sheet', '明细', 'DataRange', 'B3'); % 情况三:表头在第 3 行,但数据行数不定,显式指定表头行 T3 = readtable(f, 'Sheet', '明细', ... 'DataRange', 'A3', ... 'ReadVariableNames', true); % 情况四:工作簿里已经定义好了名称区域 T4 = readtable(f, 'Sheet', '明细', 'DataRange', 'SaleRange');四种写法的差别在"右边界和下边界谁来定"。'B3:G100'是硬边界,列和行都写死,好处是读出来的表结构绝对稳定,坏处是数据行变多之后会被截断,而且不报错。'B3'只给左上角,导入器读到连续空行为止,适合行数增长的月表,但如果数据中间有空行,会在那里断掉。第三种写法最有迷惑性:'DataRange','A3'配合'ReadVariableNames', true,含义是"A3 这一行是表头,下面是数据",这是最容易记混的一组参数——它和'DataRange','A3:G100'的行为不完全一样,后者会把区域的第一行当表头。实际写的时候,我习惯先跑一次'DataRange','A3',用height(T)和 Excel 里肉眼数的行数对一遍,对上了再固化下来。
第四种写法用得人少但特别值:如果这份表是别人定期给你的,可以在 Excel 里按Ctrl+F3定义一个名称区域,之后脚本就只认这个名字。对方加行减行、插列删列,只要名称区域的引用跟着更新,脚本一行都不用改。
3.3 列名:VariableNamingRule 与中文表头的处理
从某个版本开始,readtable的默认行为从"保留原列名"改成了"把列名改成合法标识符"。中文表头会被转成拼音或直接变成Var1、Var2,带空格的变成下划线,重名的加后缀。这个改动对交互式使用是友好的,对脚本是致命的——你的T.金额今天就报"未定义变量"。
% 保留原始表头,中文、空格、括号都原样留着 T = readtable(f, 'Sheet', '明细', 'VariableNamingRule', 'preserve'); disp(T.Properties.VariableNames) % 确认列名没被改 % 老版本没有 VariableNamingRule,用这个等价写法 T2 = readtable(f, 'Sheet', '明细', 'PreserveVariableNames', true);'preserve'之后,T.金额这种写法在脚本里是可以用的,因为 MATLAB 支持用点号访问非标识符形式的字段名。真正需要注意的地方在别处:一旦列名里有空格或括号,动态拼字段名的时候要用T.(nameStr)的形式,nameStr是字符串变量,不能直接写死。这一点在批处理里很常见,因为每份文件的表头可能差一个字。
还有一个隐蔽差异:'preserve'保留的是原样,也就意味着两列重名时不会自动加后缀,readtable会报重复变量名错误。遇到这种表,只能退一步用'modify'让导入器改名,然后自己维护一张新旧列名对照表。我一般会在脚本开头把这张对照表写成containers.Map,后面所有取数都走映射,比硬编码列名抗变化。
3.4 先探后读:detectImportOptions 的正确用法
到这里,参数已经不少了。把它们全都塞进一次readtable调用不是不行,但可读性差,而且每次读同一份文件都要重新推断一遍类型。detectImportOptions解决的就是这个问题:先探测一次,拿到一个配置对象,改完之后复用。
opts = detectImportOptions(f, 'Sheet', '明细'); disp(opts.VariableNames) % 探测出来的列名 disp(opts.VariableTypes) % 探测出来的每列类型 disp(opts.SelectedVariableNames) % 默认全选 % 只读需要的四列,其余列不进入内存 opts.SelectedVariableNames = {'订单号', '下单日期', '金额', '状态'}; opts.VariableNamingRule = 'preserve'; % 逐列钉死类型,不让它再猜 opts = setvaropts(opts, '下单日期', 'Type', 'datetime', ... 'InputFormat', 'yyyy/MM/dd'); opts = setvaropts(opts, '金额', 'Type', 'double'); opts = setvaropts(opts, '订单号', 'Type', 'char'); % 订单号必须留文本 T = readtable(f, opts); preview(f, opts) % 读之前先预览前几行,确认没歪这段代码里最值钱的是SelectedVariableNames和setvaropts两处。前者做的是列裁剪,效果不只是"少几列",而是内存占用和耗时同步下降,一份 200 列的宽表只取 4 列,读取时间能差出一个数量级。后者做的是类型固化,订单号这类看起来全是数字的列必须强制成char,否则前导零会丢——0012345读进来变成12345,而且不报错。
preview(f, opts)是我很推荐的一个习惯。它在不把整个文件读进内存的前提下,把前几行按当前配置渲染出来,类型对不对、表头对不对、有没有混进汇总行,一眼就能看出来。比读完之后head(T)再回头改参数要省事得多,尤其是文件大的时候。
提示:
detectImportOptions的探测结果依赖文件内容,同一份模板文件在数据变化后可能探测出不同类型(比如某个月金额列全是整数,下个月出现了小数,或者某列前 100 行是数字、第 101 行混了个 "待定")。所以生产脚本里,探测出来的VariableTypes不要直接用,一定要用setvaropts显式覆盖。
4. 类型与脏数据:日期、缺失值、文本数字的处理链
4.1 日期列:Excel 序列号与 datetime 的换算
日期是 Excel 到 MATLAB 之间最容易出错的一类数据。Excel 内部把日期存成一个从 1900 年 1 月 1 日起算的序列号,2024-03-15在底层就是45366。如果这一列的单元格格式是"常规"而不是"日期",导入器看到一个整数,就老老实实按double给你,于是你的日期列变成了一个毫无意义的数字。
% 路径一:单元格本身就是日期格式,让导入器认,但要把格式钉死 opts = detectImportOptions(f, 'Sheet', '明细'); opts.VariableNamingRule = 'preserve'; opts = setvaropts(opts, '下单日期', 'Type', 'datetime', ... 'InputFormat', 'yyyy-MM-dd'); T = readtable(f, opts); % 路径二:已经读成了 double,事后换算 if ~isdatetime(T.下单日期) T.下单日期 = datetime(T.下单日期, 'ConvertFrom', 'excel'); end T.下单日期.Format = 'yyyy-MM-dd'; % 路径三:文本格式的日期,显式给格式串,不要让它猜 T.下单日期 = datetime(string(T.下单日期), ... 'InputFormat', 'yyyy年MM月dd日');三条路径对应三种不同的源数据状态,判断方法很简单:读进来先class(T.下单日期),是datetime就走通了,是double走路径二,是char或cell走路径三。'ConvertFrom','excel'是官方给的换算入口,默认按 1900 日期系统算;如果碰上 1904 日期系统的老文件(比较少见),换算结果会整体差 1462 天,这种误差不会报错,只会让你的月度统计全部偏移,属于典型的安静错误。
路径三的InputFormat一定要显式给。不给的话,datetime会按本机区域设置去猜03/04/2024到底是 3 月 4 日还是 4 月 3 日,这个行为在不同机器上跑出来的结果可能不一致,脚本换台电脑就换了答案。写死格式串是最省心的做法。
4.2 缺失值:空单元格、空字符串与 NaN 的三种表现
缺失值在 Excel 里只有一种形态——空格子,但在 MATLAB 里会分裂成三种:数值列里是NaN,字符列里可能是''或''(空字符),string列里是<missing>。这三种东西不通用,isnan对字符报错,ismissing对double虽然能跑但语义容易混。
T = readtable(f, 'Sheet', '明细', 'VariableNamingRule', 'preserve'); % 数值列:空格子直接就是 NaN nMiss = sum(isnan(T.金额)); % 文本列:先统一成 string,再统一判缺失 T.状态 = string(T.状态); T.状态(ismissing(T.状态) | T.状态 == "") = "未知"; % 从源头治理:把业务上的占位符也声明成缺失 opts = detectImportOptions(f, 'Sheet', '明细'); opts.VariableNamingRule = 'preserve'; opts.TreatAsMissing = {'N/A', 'NA', '-', '无', '--', '待定'}; T2 = readtable(f, opts);TreatAsMissing这个参数值得单独记住。业务表里的缺失很少是干净的空白,更多是N/A、-、无、待定这类人工填的占位符。如果不声明,这些列会被推断成文本类型,然后你在做数值统计的时候才发现整列都是NaN,回头查半天。把它写在detectImportOptions之后,导入阶段就统一成缺失值,后面所有处理逻辑都干净。
还有一类更隐蔽:单元格里放了一个空格(' ')或者不换行空格(char(160))。这类"看起来空、实际非空"的单元格不会进TreatAsMissing,因为它是非空的。批量处理时我一般会在清洗函数里加一步strtrim后再判空,具体放在 4.4 节。
4.3 文本型数字:千分位、货币符号与全角空格
财务给的表格里,金额写成1,234.56、¥8,900、123(全角数字)都不稀奇。这些列如果让导入器自动推断,结论通常有两个:要么判成文本,整列变成char;要么强行转数值,遇到第一个逗号就放弃,整列变NaN。两种结果都不好用。
稳妥的做法是反过来——明确告诉导入器"这列先按文本读",把清洗放在自己手里,因为只有你知道业务上哪些符号该去掉。
opts = detectImportOptions(f, 'Sheet', '明细'); opts.VariableNamingRule = 'preserve'; opts = setvaropts(opts, '金额', 'Type', 'char'); % 关掉自动数值推断 T = readtable(f, opts); s = string(T.金额); s = erase(s, [",", "¥", "¥"]); % 去千分位和货币符号 s = erase(s, char(12288)); % 去全角空格 s = strtrim(s); % 去普通前后空白 s(s == "" | s == "-" | s == "—") = "NaN"; v = str2double(s); % 转不过来的是 NaN,可以单独挑出来查 bad = find(isnan(v) & s ~= "NaN"); fprintf('还有 %d 个单元格无法解析,行号:%s\n', numel(bad), mat2str(bad'));最后三行是我一定会加的。str2double在转换失败时返回NaN而不报错,如果不单独统计,脏数据就被静默吞掉了。把bad打出来,往往能发现一些规律——比如某几行是"小计"、某几行带了备注文字,这些是表格结构问题,不是清洗能解决的,得回头看DataRange是不是切错了。
char(12288)这个字符要单独说。全角空格在视觉上和普通空格几乎没区别,strtrim处理不了它,str2double也会因为它返回NaN。这是我在处理从网页或某些系统导出的表格时踩过的坑,症状是"看起来是数字,转出来全是NaN",排查时用double(s(1))打印字符编码才看出来。
4.4 把清洗链固定成一个函数
零散的处理步骤写多了会散,我的习惯是固化成一个小函数,所有脚本都调它,参数化程度不高但胜在行为一致。
function T = readBizSheet(f, sheetName) % 读取业务表并做基础归一:保留原列名、声明占位缺失、金额转数值、日期转 datetime opts = detectImportOptions(f, 'Sheet', sheetName); opts.VariableNamingRule = 'preserve'; opts.TreatAsMissing = {'N/A', 'NA', '-', '--', '无', '待定'}; vn = opts.VariableNames; if any(strcmp(vn, '金额')) opts = setvaropts(opts, '金额', 'Type', 'char'); end T = readtable(f, opts); vn = T.Properties.VariableNames; if any(strcmp(vn, '金额')) s = erase(string(T.金额), [",", "¥", "¥", char(12288)]); T.金额 = str2double(strtrim(s)); end if any(strcmp(vn, '下单日期')) && ~isdatetime(T.下单日期) T.下单日期 = datetime(T.下单日期, 'ConvertFrom', 'excel'); T.下单日期.Format = 'yyyy-MM-dd'; end end这个函数的边界很明确:只处理金额和下单日期两个已知字段,其余列原样返回。刻意的设计,是因为通用清洗函数一旦开始"猜哪些列是数值",就会退化成另一个不透明的导入器。函数里用any(strcmp(...))先判列是否存在,而不是直接T.金额,这样同一套代码能跑在缺列的月度文件上,不会因为某个字段没导出就整体报错。
5. 避坑与排查:五条从现场捡回来的记录
5.1 列名全变成 Var1、Var2,脚本报"未定义变量"
现象:昨天还能跑的脚本,今天报无法识别的表变量 '金额',disp(T.Properties.VariableNames)看到的是{'Var1','Var2','Var3',...}。
原因:readtable的默认列名处理规则是'modify',中文表头、含空格或括号的表头都会被改写成合法标识符,且改写规则和你想象的拼音不一样。这个默认值在版本升级时变过一次,所以同一份脚本换台机器就跑不通。
解决:所有readtable调用统一加'VariableNamingRule', 'preserve'。老版本没有这个参数,用'PreserveVariableNames', true。如果两份表列名重复导致'preserve'报错,改成'modify'并在脚本里维护一张列名映射表。
5.2 金额列读进来全是 NaN,Excel 里看着明明是数字
现象:sum(isnan(T.金额))等于总行数,Excel 里打开看这一列全是正常数字。
原因:三种可能,按出现频率排——单元格是文本格式(左上角有绿色小三角)配千分位逗号;列里混了-、N/A这类占位符导致整列被判成char;或者表头行判断错了,读进来的"金额"其实是标题行下面的说明文字。
解决:先用class(T.金额)确认类型,是char就走 4.3 节的文本清洗路径;再用disp(T.金额(1:5))看头几行原始值,确认没有混入说明文字。如果class是double但还是全NaN,那基本是DataRange切错了,回去检查表头位置。
5.3 日期整体偏移了几年,或者全部变成 45000 多
现象:T.下单日期是double类型,值在 40000 到 50000 之间;或者用了datetime换算但结果显示 1904 年。
原因:Excel 用序列号存日期,1900 日期系统和 1904 日期系统差 1462 天。某些从旧版表格软件或特定区域设置导出的文件用的是 1904 系统,'ConvertFrom','excel'默认按 1900 算,于是整体偏了四年。
解决:先确认是哪种。读进来是double就用datetime(x,'ConvertFrom','excel');换算完发现整体偏了 1462 天,就在换算后统一加days(1462)。根治办法是在 Excel 里把单元格格式改成日期、另存一份再读,从源头消除序列号。
5.4 几百个文件的批处理跑到一半就卡住,或者报文件被占用
现象:循环跑到第 37 个文件时抛错,提示无法打开文件;或者整批跑了一夜还没结束。
原因:卡住通常是那个文件正在 Excel 里打开着,被独占锁定了;跑得慢多半是开了'UseExcel', true,或者每个文件都重新detectImportOptions一次,探测本身就扫了一遍全表。
解决:dir之后先过滤掉~$开头的临时锁文件,剩下的用try/catch包住,单个失败不中断整批,把失败文件名记到一个日志里最后统一处理。性能方面,同一模板的文件只detectImportOptions一次,之后复用opts改opts.Sheet,不要每个文件重新探测。
5.5 只取四列,内存却还是爆了
现象:文件 200 多列、几十万行,明确只需要四个字段,但readtable之后内存占用仍然接近文件大小。
原因:readtable是先按整个区域读进来、再做列裁剪的。SelectedVariableNames在detectImportOptions生成的对象上设置才有效,如果是在一次性的readtable调用里临时传参,裁剪发生在读取之后,峰值内存压不下去。
解决:走opts = detectImportOptions(...)→opts.SelectedVariableNames = {...}→readtable(f, opts)这条路径。真正的瓶颈如果真的在文件本身,就退到DataRange层面裁剪,连不需要的列区域都不进入解析器。
6. 批量与回读验证:把 200 个表读成一张表并确认没读歪
批处理的骨架本身不复杂,难点在于"读完之后怎么确认没读错"。我的做法是让每一条记录带上出处,再在末尾做一次写回校验。
files = dir(fullfile('D:\data\monthly', '*.xlsx')); files = files(~startsWith({files.name}, '~$')); % 排除 Excel 锁文件 parts = cell(numel(files), 1); for k = 1:numel(files) f = fullfile(files(k).folder, files(k).name); try T = readBizSheet(f, '明细'); % 复用 4.4 节的函数 T.源文件 = string(files(k).name); % 留一列出处,查错用 parts{k} = T; catch ME fprintf('跳过 %s:%s\n', files(k).name, ME.message); end end parts = parts(~cellfun(@isempty, parts)); out = vertcat(parts{:}); % 列必须完全一致两点必须注意。vertcat要求所有表的变量名和类型完全一致,所以读取函数里不能有"某列存在才处理"之外的分支差异,否则会在拼接时报变量不匹配。T.源文件这一列看着多余,实际是排查利器——后面发现某个数值异常,能立刻定位到是哪份文件,不用一行行反查。
回读验证更简单:把out写出去,再读回来,逐列比类型。
writetable(out, 'D:\data\merged_check.xlsx', 'Sheet', '汇总'); back = readtable('D:\data\merged_check.xlsx', 'Sheet', '汇总', ... 'VariableNamingRule', 'preserve'); fprintf('行数 %d -> %d\n', height(out), height(back)); assert(isequal(out.Properties.VariableTypes, back.Properties.VariableTypes)); assert(abs(sum(out.金额, 'omitnan') - sum(back.金额, 'omitnan')) < 1e-6);这个往返测试拦下过好几次真问题:一次是订单号在写出时被 Excel 自动转成了数值,前导零丢了;一次是某列里混了一个Inf,写出去变成空单元格,读回来成了NaN。这类问题在内存里看数据是发现不了的,只有走一遍文件才能暴露出来。
从那以后我每次做批量合并,不管多急,都会强制走一遍这个"写出去再读回来"的往返校验,尤其要盯住VariableTypes是否一致。这个习惯花不了三十秒,省下的是通宵排查。希望帮到你。
本文还有配套的精品资源,点击获取