MATLAB批量读取Excel并自动绘图:数据清洗+可视化闭环
2026/9/16 13:25:26 网站建设 项目流程

简介:本资源是一份面向MATLAB初学者与数据处理实践者的实用脚本工具,聚焦Excel批量读取、无效数据清洗及自动化绘图三大核心需求,适用于课程设计、科研数据预处理及工程报告可视化等场景。压缩包为ZIP格式,仅含1个关键文件——data_treating.m函数脚本(718B),该文件封装了可指定Sheet编号的xlsread批量读取逻辑、基于isnan/isnumeric的空值与非数值内容识别与NaN替换机制,以及列筛选与plot绘图一体化流程,支持灵活调用与快速二次开发。目前已有2122人学习下载,体现了其在轻量级自动化办公脚本领域的高频实用价值。读者可直接部署运行,获得从原始Excel到结构化图表的一站式处理能力,并基于源码理解MATLAB中文件遍历、条件清洗、矩阵索引与图形标注等关键编程范式。

1. 用 MATLAB 批量读取 Excel 表格并自动绘图:跳过空行、识别错位标题、过滤非数值列,一线工程师每天都在跑的「数据清洗+可视化」闭环

你刚收到市场部发来的 37 个 Excel 文件,每个文件含 5 张表(“日销售”“渠道明细”“退货记录”“库存快照”“客户反馈”),但命名不统一(有的叫“日销.xlsx”,有的叫“sales_daily_202405_v2.xlsx”),部分表格首行是合并单元格标题,第 3 行才是真实列名,还有几份数据里混着“暂无”“/”“N/A”甚至整列是文字说明。手动打开、复制、粘贴、补缺失值、再画折线图?别试了——MATLAB 的readtable+dir+plot组合拳,12 行核心代码就能完成从文件扫描到多子图输出的全流程。这不是脚本玩具,而是产线质量监控、金融日报生成、科研实验复现中真正落地的标准化数据流水线。适合需要处理 10~500 个 Excel 文件、且对数据鲁棒性(比如空单元格、类型错乱、列顺序变动)有硬性要求的工程师、分析师和研究生。它不依赖 Excel 应用程序进程,不触发 COM 接口超时,也不需要提前安装任何加载项。

2. 构建可扩展的批量读取框架:用dir筛选 +readtable自适应解析 + 结构体封装原始数据

2.1 按业务规则精准定位 Excel 文件:支持通配符、路径递归与时间过滤

批量处理的第一步不是读数据,而是精准找到该读的文件dir函数远比uigetdir或硬编码路径可靠。假设所有待处理文件存放在D:\data\monthly_reports\下,且只需处理 2024 年 5 月之后的.xlsx文件:

% 定义根目录与搜索模式 root_dir = 'D:\data\monthly_reports\'; search_pattern = '**\*20240[5-9]*.xlsx'; % 匹配 202405 到 202409 的文件名 all_files = dir(fullfile(root_dir, search_pattern)); % 过滤掉子目录(只保留文件) excel_files = {all_files([all_files.isdir] == 0).name}; full_paths = fullfile({all_files([all_files.isdir] == 0).folder}, excel_files); % 按修改时间升序排列(确保按时间顺序处理) file_times = [all_files([all_files.isdir] == 0).datenum]; [~, idx] = sort(file_times); excel_files = excel_files(idx); full_paths = full_paths(idx);

提示'**\'启用递归搜索,避免手动遍历子文件夹;datenum获取修改时间戳,比datestr更易排序;[all_files.isdir] == 0是 MATLAB 中筛选文件的标准写法,比isfile在旧版本中更兼容。

2.2 用readtable的高级选项应对 Excel 布局混乱:跳过空行、定位真实标题、自动类型推断

Excel 表格结构千差万别,readtable的默认行为常失败。关键参数必须显式设置:

% 预定义要读取的工作表名列表(按业务优先级) target_sheets = {'日销售', 'sales_daily', 'Daily_Sales', 'Sheet1'}; % 多种可能名称 all_data = struct(); % 存储每个文件的解析结果 for i = 1:length(full_paths) fprintf('正在处理: %s\n', excel_files{i}); % 尝试读取第一个匹配的工作表 sheet_name = ''; for j = 1:length(target_sheets) try % 关键参数详解: % - 'ReadVariableNames': true → 从第一行读列名(但需配合 'HeaderLines') % - 'HeaderLines': 2 → 跳过前2行(合并标题+空行),第3行作为列名 % - 'EmptyFieldRule': 'auto' → 自动将空单元格转为 <missing> % - 'DatetimeType': 'datetime' → 强制日期列解析为 datetime 类型 % - 'TextType': 'string' → 文本列统一为 string,避免 char 数组截断 tbl = readtable(full_paths{i}, ... 'Sheet', target_sheets{j}, ... 'ReadVariableNames', true, ... 'HeaderLines', 2, ... 'EmptyFieldRule', 'auto', ... 'DatetimeType', 'datetime', ... 'TextType', 'string'); sheet_name = target_sheets{j}; break; catch ME continue; % 尝试下一个工作表名 end end if isempty(sheet_name) warning('文件 %s 未找到有效工作表,跳过', excel_files{i}); continue; end % 封装为结构体字段,保留原始文件名与工作表名 all_data.(excel_files{i}) = struct('table', tbl, 'sheet', sheet_name, 'path', full_paths{i}); end

注意'HeaderLines'参数是解决“标题在第 N 行”的核心,它比手动readmatrix+readcell拼接更安全;'EmptyFieldRule'设为'auto'后,readtable会将空单元格、#N/ANULL统一转为<missing>,后续可用fillmissing统一处理;'TextType'必须设为'string',否则中文列名或长文本会被截断为 char 数组。

2.3 将异构表格统一为标准结构体数组:列名标准化 + 数据类型校验

不同 Excel 文件的列名可能为“销售额”“SALES_AMT”“amount_yuan”,需映射到统一字段。同时校验数值列是否真为数值:

% 定义业务字段映射表(key=标准名,value=可能的原始列名) field_mapping = containers.Map(... {'sales', 'date', 'product_id', 'channel'}, ... {{'销售额','SALES_AMT','amount_yuan'}, ... {'日期','DATE','sale_date'}, ... {'产品编码','PROD_ID','item_no'}, ... {'渠道','CHANNEL','sales_channel'}}); % 对每个文件执行标准化 for file_name = fieldnames(all_data)' tbl = all_data.(file_name{1}).table; % 步骤1:重命名列(模糊匹配) new_vars = {}; for std_field = keys(field_mapping)' candidates = field_mapping(std_field); found = false; for k = 1:length(candidates) % 不区分大小写、忽略空格和下划线的近似匹配 pattern = regexprep(lower(candidates{k}), '[ _]+', ''); for j = 1:width(tbl) col_test = regexprep(lower(tbl.Properties.VariableNames{j}), '[ _]+', ''); if contains(col_test, pattern) || contains(pattern, col_test) new_vars{j} = std_field; found = true; break; end end if found, break; end end if ~found, new_vars{end+1} = []; end % 未匹配列置空 end % 步骤2:提取必需列,强制类型转换 required_cols = {'sales', 'date', 'product_id', 'channel'}; valid_tbl = table(); for col = required_cols' if ismember(col, new_vars) idx = find(ismember(new_vars, col)); col_data = tbl{:,idx}; % 类型强转:sales→double,date→datetime,其余→string switch col case 'sales' valid_tbl.("sales") = double(col_data); case 'date' valid_tbl.("date") = datetime(col_data, 'InputFormat', 'auto'); otherwise valid_tbl.(col) = string(col_data); end else % 缺失列填充默认值 switch col case 'sales' valid_tbl.("sales") = NaN(height(tbl), 1); case 'date' valid_tbl.("date") = datetime('NaT', 'Format', 'yyyy-MM-dd'); otherwise valid_tbl.(col) = repelem("<unknown>", height(tbl), 1); end end end all_data.(file_name{1}).standardized = valid_tbl; end

提示containers.Map实现灵活映射,避免硬编码strcmpiregexprep清洗列名空格和下划线,提升匹配鲁棒性;datetime(..., 'InputFormat', 'auto')能自动识别2024/05/0101-May-202420240501等格式;对缺失列填充NaNNaT,保证后续plot不报错。

3. 无效内容的三层防御机制:缺失值插补、异常值剔除、逻辑矛盾校验

3.1 第一层:用fillmissing智能填充数值列,拒绝简单均值填充

直接fillmissing(tbl.sales, 'mean')会污染趋势分析。应按业务周期分组填充:

% 按日期分组,用同周内其他天的均值填充(更符合销售场景) if ~isempty(all_data) for file_name = fieldnames(all_data)' tbl = all_data.(file_name{1}).standardized; % 添加星期几列用于分组 tbl.weekday = weekday(tbl.date, 'long'); % 按 weekday 分组,用组内均值填充 sales 缺失 tbl.sales = fillmissing(tbl.sales, 'groupmean', 'GroupingVariables', tbl.weekday); % 移除辅助列 tbl.weekday = []; all_data.(file_name{1}).standardized = tbl; end end

注意'groupmean''linear'更符合业务逻辑(周一销量通常接近,而非线性过渡);'GroupingVariables'参数必须传入变量名或向量,不能传字符串'weekday'

3.2 第二层:用 IQR 法识别并标记异常销售值,保留原始数据可追溯

异常值不直接删除,而是标记为NaN并记录原因,便于审计:

function [cleaned_sales, outlier_log] = detect_sales_outliers(sales_vec, date_vec) % 计算季度滚动 IQR(比全局 IQR 更敏感) q_start = min(date_vec); q_end = max(date_vec); q_len = calmonths(q_end, q_start); cleaned_sales = sales_vec; outlier_log = table('Size', [0,3], 'VariableTypes', {'datetime','double','string'}, ... 'VariableNames', {'Date','Value','Reason'}); for i = 1:length(sales_vec) if isnan(sales_vec(i)), continue; end % 取前后 90 天窗口内的数据(排除自身) window_mask = (date_vec >= date_vec(i)-days(90)) & ... (date_vec <= date_vec(i)+days(90)) & ... (date_vec ~= date_vec(i)); window_data = sales_vec(window_mask); if length(window_data) < 5, continue; end % 窗口数据不足,跳过 Q1 = prctile(window_data, 25); Q3 = prctile(window_data, 75); IQR = Q3 - Q1; lower_bound = Q1 - 1.5*IQR; upper_bound = Q3 + 1.5*IQR; if sales_vec(i) < lower_bound || sales_vec(i) > upper_bound outlier_log = [outlier_log; table(date_vec(i), sales_vec(i), ... sprintf('IQR超出 %.1f-%.1f', lower_bound, upper_bound))]; cleaned_sales(i) = NaN; end end end % 在主循环中调用 for file_name = fieldnames(all_data)' tbl = all_data.(file_name{1}).standardized; [tbl.sales, log] = detect_sales_outliers(tbl.sales, tbl.date); all_data.(file_name{1}).outlier_log = log; all_data.(file_name{1}).standardized = tbl; end

提示:滚动窗口法(rolling window)比静态 IQR 更适应季节性波动;prctile计算分位数比quantile在旧版 MATLAB 中更稳定;记录outlier_log表,方便后续导出为outliers_report.xlsx

3.3 第三层:跨字段逻辑校验,拦截明显错误组合

例如:sales > 0dateNaT,或channel为空但product_id有效:

% 定义校验规则:返回逻辑向量,true 表示该行需标记为可疑 function flag = cross_field_validation(tbl) flag = false(height(tbl), 1); % 规则1:销售为正但日期缺失 flag = flag | (tbl.sales > 0 & isnat(tbl.date)); % 规则2:渠道为空但销售非零 flag = flag | (ismissing(tbl.channel) & ~isnan(tbl.sales) & tbl.sales ~= 0); % 规则3:产品ID含非法字符(仅数字和字母) if ~isempty(tbl.product_id) invalid_id = ~cellfun(@(x) isempty(regexp(x, '[^a-zA-Z0-9]')), tbl.product_id); flag = flag | invalid_id; end end % 执行校验并添加标记列 for file_name = fieldnames(all_data)' tbl = all_data.(file_name{1}).standardized; tbl.validation_flag = cross_field_validation(tbl); all_data.(file_name{1}).standardized = tbl; end

注意isnat专用于判断datetime是否为NaTcellfun+regexp处理 string 列的正则校验;validation_flag列可后续用于findgroups分组统计问题率。

4. 高效批量绘图:用tiledlayout统一布局 +plot自动适配多数据源 + 中文标签防乱码

4.1 用tiledlayout创建自适应网格,避免subplot的坐标轴重叠问题

% 计算所需子图数量(每个文件一个图,最多 6 行×4 列) n_files = length(fieldnames(all_data)); n_rows = min(6, ceil(n_files / 4)); n_cols = min(4, n_files); fig = tiledlayout(n_rows, n_cols, 'TileSpacing', 'compact', 'Padding', 'none'); title(fig, '各月销售趋势对比(已过滤异常值)', 'FontSize', 12, 'FontWeight', 'bold'); for i = 1:n_files file_name = fieldnames(all_data){i}; tbl = all_data.(file_name).standardized; % 过滤掉 validation_flag 为 true 的行 valid_mask = ~tbl.validation_flag; plot_data = tbl(valid_mask, :); % 按日期排序(确保折线连续) plot_data = sortrows(plot_data, 'date'); % 创建子图 nexttile; h = plot(plot_data.date, plot_data.sales, '-o', 'MarkerSize', 3, 'LineWidth', 1.2); % 设置标题与标签(支持中文) title(sprintf('%s (%d条)', file_name, height(plot_data)), 'FontSize', 9); xlabel('日期', 'FontSize', 8); ylabel('销售额', 'FontSize', 8); % 旋转 x 轴标签避免重叠 xtickangle(gca, -30); grid on; end

提示tiledlayout是 R2019b+ 推荐方案,'TileSpacing''Padding'参数可消除子图间白边;sortrows(..., 'date')确保plot不因日期乱序而画出交叉线;xtickangle直接旋转刻度标签,比datetick更可控。

4.2 解决 MATLAB 绘图中文乱码:三步永久生效配置

乱码根源是默认字体不支持中文。以下配置一次,永久生效:

% 步骤1:查询系统中可用的中文字体(Windows 常见) available_fonts = system('fc-list :lang=zh'); % Linux/macOS % Windows 下常用:'SimHei'(黑体)、'Microsoft YaHei'(微软雅黑)、'KaiTi'(楷体) % 步骤2:设置图形默认字体(影响所有后续 figure) set(groot, 'DefaultAxesFontName', 'SimHei'); set(groot, 'DefaultTextFontName', 'SimHei'); set(groot, 'DefaultLegendFontName', 'SimHei'); % 步骤3:设置字号(避免过小) set(groot, 'DefaultAxesFontSize', 10); set(groot, 'DefaultTextFontSize', 10); % 验证:新建 figure 测试 test_fig = figure('Name', '中文测试'); ax = axes(test_fig); plot(ax, 1:5, rand(1,5), '-o'); title(ax, '中文标题正常显示'); xlabel(ax, 'X轴中文标签'); ylabel(ax, 'Y轴中文标签');

注意groot是图形根对象,set(groot, ...)影响所有新创建的 figure;fc-list命令在 Linux/macOS 中有效,Windows 用户可改用system('powershell -Command "[System.Drawing.Text.FontFamily]::Families | ForEach-Object Name"');若SimHei不可用,尝试'Microsoft YaHei'

4.3 导出高清图与数据摘要:一键生成 PDF 报告 + CSV 清洗日志

% 导出当前 tiledlayout 为 PDF(矢量图,缩放不失真) exportgraphics(fig, 'sales_trends_report.pdf', 'ContentType', 'vector'); % 生成清洗摘要 CSV summary_data = table('Size', [0,5], 'VariableTypes', {'string','double','double','double','string'}, ... 'VariableNames', {'FileName','RawRows','CleanedRows','OutlierCount','ValidationFailRate'}); for i = 1:n_files file_name = fieldnames(all_data){i}; raw_rows = height(all_data.(file_name).table); clean_rows = height(all_data.(file_name).standardized); outlier_count = height(all_data.(file_name).outlier_log); fail_rate = mean(all_data.(file_name).standardized.validation_flag); summary_data = [summary_data; table(file_name, raw_rows, clean_rows, outlier_count, ... sprintf('%.1f%%', fail_rate*100))]; end writematrix(summary_data, 'data_cleaning_summary.csv', 'Delimiter', ','); % 导出所有清洗后数据为单个 Excel(每文件一个 sheet) cleaned_tables = {}; sheet_names = {}; for i = 1:n_files file_name = fieldnames(all_data){i}; tbl = all_data.(file_name).standardized; % 移除 validation_flag 列(仅用于过程标记) tbl.validation_flag = []; cleaned_tables{i} = tbl; sheet_names{i} = strrep(file_name, '.xlsx', ''); % 去掉扩展名作 sheet 名 end writecell({'文件名', '原始行数', '清洗后行数', '异常值数', '校验失败率'}, 'report_summary.xlsx'); xlswrite('report_summary.xlsx', summary_data{:,:}, 'Summary'); for i = 1:length(cleaned_tables) writematrix(cleaned_tables{i}, 'report_summary.xlsx', 'Sheet', sheet_names{i}); end

提示exportgraphics(..., 'ContentType', 'vector')输出 PDF 为矢量图,插入 Word/PPT 无限缩放;writematrix写入 CSV 比writetable更快且无引号包裹;xlswrite已被writematrix/writetable替代,但writematrix不支持多 sheet,故此处仍用xlswrite(R2019a+ 兼容)。

5. 生产环境加固技巧:用try-catch包裹关键步骤 + 日志文件记录全过程 + 内存优化策略

5.1 用结构化日志替代fprintf:记录时间戳、操作、状态、耗时

function log_event(log_file, event_type, message, duration_sec) timestamp = datetime('now', 'Format', 'yyyy-MM-dd HH:mm:ss.SSS'); log_entry = sprintf('%s | %s | %s | %.3f sec\n', ... string(timestamp), event_type, message, duration_sec); fid = fopen(log_file, 'a'); fwrite(fid, log_entry, 'char'); fclose(fid); end % 在主流程中调用 log_file = 'batch_processing_log.txt'; log_event(log_file, 'START', '开始批量处理', 0); tic; % ... 执行文件扫描 ... toc_val = toc; log_event(log_file, 'INFO', sprintf('扫描到 %d 个 Excel 文件', length(full_paths)), toc_val); tic; % ... 执行读取与清洗 ... toc_val = toc; log_event(log_file, 'INFO', '完成数据清洗', toc_val); % 最终记录 log_event(log_file, 'END', '全流程结束', 0);

注意datetime(..., 'Format', ...)精确到毫秒;fopen(..., 'a')追加写入,避免覆盖历史日志;日志文件名固定,便于运维监控。

5.2 大文件内存优化:用readtable'Range'参数分块读取

当单个 Excel 超过 10 万行时,全量读取易爆内存。改用分块:

function tbl_chunk = read_large_excel(filename, sheet, chunk_size) % 先获取总行数(不读数据) info = spreadsheetInfo(filename, sheet); total_rows = info.LastRow; tbl_chunk = table(); start_row = 3; % 跳过标题行 while start_row <= total_rows end_row = min(start_row + chunk_size - 1, total_rows); range_str = sprintf('A%d:%c%d', start_row, char(64 + info.LastColumn), end_row); try chunk = readtable(filename, 'Sheet', sheet, 'Range', range_str, ... 'ReadVariableNames', true, 'HeaderLines', 2); tbl_chunk = [tbl_chunk; chunk]; catch ME warning('读取范围 %s 失败: %s', range_str, ME.message); end start_row = end_row + 1; end end % 调用示例(仅对 >50000 行的文件启用) if info.LastRow > 50000 tbl = read_large_excel(full_paths{i}, sheet_name, 10000); else tbl = readtable(...); % 原逻辑 end

提示spreadsheetInfo返回工作表元数据,不加载数据;'Range'参数格式为'A1:C1000',需动态计算列字母;char(64 + n)将列号转为字母(A=1, B=2...)。

5.3 错误恢复机制:保存中间状态,失败后可续跑

% 在循环前检查是否存在中间状态文件 state_file = 'processing_state.mat'; if exist(state_file, 'file') load(state_file); fprintf('检测到中断状态,从文件 %d 继续\n', resume_idx); else resume_idx = 1; end % 主循环中 for i = resume_idx:length(full_paths) try % ... 处理逻辑 ... % 成功后更新状态 save(state_file, 'i', 'all_data', 'log_file'); catch ME % 记录错误并保存当前进度 log_event(log_file, 'ERROR', sprintf('文件 %s 处理失败: %s', excel_files{i}, ME.message), 0); save(state_file, 'i', 'all_data', 'log_file'); warning('已保存中断状态,可重新运行继续'); break; end end

注意save保存变量名而非值,'i'记录当前索引;exist(..., 'file')是跨平台检查文件存在的标准方法;break退出循环而非return,确保后续清理代码执行。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询