《MATLAB 批量把振动 CSV(含中文“序号/采样频率”)稳健转成 .mat:自动解析+统一换算+按 H/I/O/F-rpm-fs-load 命名》 - 教程
一文搞定:批量把中文头信息的 CSV(含“序号/采样频率”等)稳健转成 .mat,并按规则重命名(H/I/O/F-rpm-fs-load)
1. 项目背景
在振动/故障诊断采集里,我们经常得到一批 CSV 文件,文件名形如:
jiankang-100rpm-2kHz-0%Load_all.csv
neiquan-1300rpm-2kHz-0%Load_all.csv
waiquan-500rpm-8kHz-10%Load_all.csv
每个文件前面是若干中文元信息(比如“采样频率,2.000kHz”),接着是表头行(通常包含**“序号”),再往下是多列数据**。但是这些 CSV 在编码、分隔符、列类型上并不统一(GBK/UTF-8、英文/中文逗号、文本列混入等),直接 readtable 往往会报错或读乱。
本文给出一套鲁棒的 MATLAB 脚本,把这类 CSV 批量转换为 .mat,并把关键信息(采样频率、转速、负载、列名、时间轴等)保存到结构体里,最后按规则重命名输出文件。
2. 明确需求(已全部实现)
- 批量读取目录下的 CSV/XLSX(含中文头信息、表头“序号”、多列数据)。
- 采样频率以文件内部为准(优先读取“采样频率,2.000kHz”),找不到再回退文件名中的 2kHz/8kHz。
- 仅保留数值数据列;遇到文本列自动尝试转数字,全 NaN 的列丢弃;若存在“序号”列,从数据中自动剔除。
- 按固定换算对每列做统一变换:
/1000 *100 *60 /4(即 ×0.15)。 - 输出 .mat 顶层变量名为
data(结构体),字段:signal(N×C) 换算后的信号矩阵varNames(C×1 string) 列名N,C样本数/通道数fs采样频率(Hz)rpm转速(r/min)load_pct负载(%)(若只有 HP 也会尝试转读;缺失时置 0)t时间轴(秒),按fs自动生成
- 重命名规则(来自实际需求):
- 条件码(来自文件名中第一段):
jiankang/健康→ Hguzhang/故障→ Fneiqian/neiquan/内圈/内环→ Iwaiquan/外圈/外环→ O- 未识别 → X
- 文件名格式:
<条件码>-<rpm>-<fs_kHz>-<load>.mat- 例如:
jiankang-100rpm-2kHz-0%Load→H-100-2-0.mat
- 例如:
- 条件码(来自文件名中第一段):
- 强鲁棒性:自动尝试编码(UTF-8/GB18030/ISO-8859-1)、自动识别分隔符(英文/中文逗号、分号、Tab)、保留原始列名(消除“列名被修改”警告)、兼容旧版
WhitespaceRule选项。
3. 解析思路
- 定位数据起始:优先寻找包含“序号”的表头行;若找不到,按“第一行像数据(数字+分隔符+数字)”来定位,上一行视为表头。
- 读表策略:
detectImportOptions+readtable,强制VariableNamingRule='preserve',保留中文列名;遇到老版本不支持的选项自动跳过。 - 列类型清洗:对每列做统一转换:数字列直取,文本/元胞/类别列转字符串后
str2double;全 NaN 的列剔除。 - 剔除“序号”列:只把它用作定位,不进入最终
signal。 - 元信息解析:把表头之前的“Key,Value”行转成 KV 表;优先从中解析“采样频率”(kHz/Hz 均兼容)。
- 文件名解析:提取条件/转速/采样率/负载(HP 或 %Load)。
- 统一换算:
signal = data * 0.15。 - 时间轴:若
fs有效,t = (0:N-1)/fs。
4. 完整代码(保存为 batch_csv2mat.m)
直接把下面整段保存为
batch_csv2mat.m。MATLAB 路径切到该文件所在目录后调用即可。
function batch_csv2mat(inDir, outDir)
% 批量把类似
% jiankang-100rpm-2kHz-0%Load_all.csv
% neiquan-1300rpm-2kHz-0%Load_all.csv
% jiankang-variable-speed-2kHz-0%Load_all.csv
% waiquan-variable-speed-2kHz-0%Load_all-1.csv <-- 重复测试1
% 转为 .mat
%
% 命名:
% 定速: <code>-<rpm>-<fs_kHz>-<load>[_RPT_<n>].mat
% 变速: <code>-VS-<fs_kHz>-<load>[_RPT_<n>].mat
% 例: H-100-2-0.mat, H-VS-2-0_RPT_1.mat
%
% 顶层变量名:data(struct),仅包含:
% data.signal [N x C] (已做统一换算 ×0.15)
% data.varNames [C x 1] 列名(保留中文)
% data.N, data.C
% data.fs (Hz), data.rpm (r/min), data.load_pct (%)
% data.t [N x 1] 时间(秒;fs 有效时生成)
if nargin <
2 || isempty(outDir), outDir = fullfile(inDir,'mat');
end
if ~exist(inDir,'dir'), error('输入目录不存在:%s', inDir);
end
if ~exist(outDir,'dir'), mkdir(outDir);
end
files = [dir(fullfile(inDir,'*.csv'));
dir(fullfile(inDir,'*.CSV'));
...
dir(fullfile(inDir,'*.txt'));
dir(fullfile(inDir,'*.TXT'));
...
dir(fullfile(inDir,'*.xlsx'));
dir(fullfile(inDir,'*.XLSX'))];
if isempty(files)
warning('目录中未发现 CSV/TXT/XLSX:%s', inDir);
return;
end
warning('off','MATLAB:table:ModifiedVarnames');
warning('off','MATLAB:table:ModifiedAndSavedVarnames');
for k = 1:numel(files)
fpath = fullfile(files(k).folder, files(k).name);
try
recFull = parse_one_file(fpath);
% 解析(含 is_vs / rpt_idx)
outname = make_outname(recFull, files(k).name);
% 生成目标 .mat 名
data = prune_and_convert(recFull);
% 仅保留 + 换算 + 生成 t
save(fullfile(outDir,outname), 'data', '-v7.3');
% 顶层变量名为 data
fprintf('OK -> %s\n', outname);
catch ME
fprintf(2,'FAIL -> %s\n %s\n', files(k).name, ME.message);
end
end
end
%% =================== 仅保留字段并做换算(并生成 t) ===================
function data = prune_and_convert(R)
scale = (10*60)/(4*1000);
% /1000 * 10 * 60 / 4 = 0.15
signal = R.data * scale;
data = struct();
data.signal = signal;
% [N x C]
data.varNames = R.varNames;
% 列名
data.N = size(signal,1);
data.C = size(signal,2);
data.fs = R.fs;
data.rpm = R.rpm;
data.load_pct = R.load_pct;
if ~isnan(R.fs) && R.fs >
0
data.t = (0:data.N-1).' / R.fs;
else
data.t = [];
end
end
%% =================== 单文件解析(鲁棒:支持 VS / RPT,仅提数值列) ===================
function rec = parse_one_file(fpath)
[~, base, ext] = fileparts(fpath);
isCSV = ismember(lower(ext), {
'.csv','.txt'
});
% ---------- 0) 提取重复测试编号,并得到用于解析的 baseCore ----------
% 支持:xxx_all-1, xxx-1(结尾为 -数字)
rpt_idx = NaN;
tok = regexp(base, '(?:_all)?-(\d+)$', 'tokens', 'once');
if ~isempty(tok), rpt_idx = str2double(tok{
1
});
end
baseCore = regexprep(base, '(?:_all)?-(\d+)$', '');
% 去掉尾部编号
% ---------- 1) 多编码读取并清洗(仅 CSV/TXT) ----------
lines = strings(0,1); encList = {
'UTF-8','GB18030','ISO-8859-1'
};
if isCSV
for e = 1:numel(encList)
try
lines = readlines(fpath, "Encoding", encList{e
});
if ~isempty(lines);
break;
end
catch, end
end
if isempty(lines), error('无法按常见编码读取此文件');
end
lines = normalize_lines(lines);
end
% ---------- 2) 表头行定位 ----------
headerLineIdx = [];
if isCSV
headerLineIdx = find(contains(lines, "序号"), 1, 'first');
if isempty(headerLineIdx)
pat = "^\s*\d+\s*[,;
\t]\s*[-\d\.]+";
isData = ~cellfun('isempty', regexp(cellstr(lines), pat, 'once'));
dataStart = find(isData, 1, 'first');
if ~isempty(dataStart) && dataStart >
1
headerLineIdx = dataStart - 1;
else
error('未找到“序号”表头且无法定位数据起始行。');
end
end
end
% ---------- 3) detectImportOptions + readtable ----------
if isCSV
opts = detectImportOptions(fpath, 'NumHeaderLines', headerLineIdx-1);
if isempty(opts.Delimiter) || isequal(opts.Delimiter,' ')
opts.Delimiter = {
',',';','\t',','
};
end
try, opts.VariableNamingRule = 'preserve';
catch, end
try, opts.PreserveVariableNames = true;
catch, end
try
txtVars = opts.VariableNames( ismember(opts.VariableTypes, {
'char','string','categorical'
}) );
if ~isempty(txtVars)
try, opts = setvaropts(opts, txtVars, 'WhitespaceRule','preserve');
catch, end
try, opts = setvaropts(opts, txtVars, 'EmptyFieldRule','auto');
catch, end
end
catch, end
try
T = readtable(fpath, opts);
catch
try, T = readtable(fpath, 'VariableNamingRule','preserve');
catch
T = readtable(fpath, 'PreserveVariableNames', true);
end
end
else
try, T = readtable(fpath, 'VariableNamingRule','preserve');
catch
T = readtable(fpath, 'PreserveVariableNames', true);
end
end
if isempty(T), error('表格为空:%s', fpath);
end
% ---------- 4) 仅提取数值列(自动数值化,剔除全 NaN / 不齐列) ----------
[A, vnames] = table_to_numeric(T);
if isempty(A) || size(A,2) == 0
error('未能从表格中提取到任何数值列:%s', fpath);
end
% 若存在“序号”,从数据中移除
idxCol = find(contains(vnames, "序号"), 1, 'first');
if ~isempty(idxCol)
data = A(:, setdiff(1:size(A,2), idxCol));
varNames = vnames(setdiff(1:numel(vnames), idxCol));
else
data = A;
varNames = vnames;
end
% ---------- 5) 元信息 & 采样频率(内部优先) ----------
meta = struct(); meta.raw = strings(0,1);
if isCSV && headerLineIdx>
1, meta.raw = lines(1:headerLineIdx-1);
end
meta.kv = table(string.empty, string.empty,'VariableNames',{
'Key','Value'
});
if ~isempty(meta.raw)
K = strings(0,1); V = strings(0,1);
for i = 1:numel(meta.raw)
s = char(meta.raw(i));
if isempty(s), continue;
end
s = strrep(s,',',','); s = strrep(s,';',';');
parts = split(string(s), ",");
if numel(parts)>=2
K(end+1,1) = strtrim(parts(1));
V(end+1,1) = strtrim(strjoin(parts(2:end), ","));
%#ok<AGROW>
end
end
meta.kv = table(K, V, 'VariableNames', {
'Key','Value'
});
end
fs = NaN;
if ~isempty(meta.kv.Key)
hit = contains(meta.kv.Key, "采样频率");
if any(hit)
val = meta.kv.Value(find(hit,1,'first'));
tok = regexp(val, '([\d\.]+)\s*([kK]?[Hh]z)?', 'tokens', 'once');
if ~isempty(tok)
v = str2double(tok{
1
}); unit = lower(strtrim(tok{
2
}));
if isempty(unit)||strcmp(unit,'hz'), fs=v;
elseif strcmp(unit,'khz'), fs=v*1000;
else, fs=v;
end
end
end
end
% ---------- 6) 文件名解析:定速 or 变速 VS ----------
cond=""; rpm=NaN; fs_name=NaN; load_pct=NaN; load_hp=NaN; is_vs=false;
% 定速:xxx-1000rpm-2kHz-0%Load / xxx-1000rpm-2kHz-10HP
m1 = regexp(baseCore,'^(?<cond>[^-]+)-(?<rpm>\d+)rpm-(?<fs>[\d\.]+)[kK]Hz-(?<hp>\d+)HP','names');
m2 = regexp(baseCore,'^(?<cond>[^-]+)-(?<rpm>\d+)rpm-(?<fs>[\d\.]+)[kK]Hz-(?<pct>\d+)\%Load','names');
% 变速:xxx-variable-speed-2kHz-0%Load / xxx-vs-2kHz-...
vsToken = '(?:variable[-_ ]?speed|variablespeed|vs|bianzhuansu|bian_su|bian_zs|变转速|变速|变转)';
mVS1 = regexp(baseCore, ['^(?<cond>[^-]+)-' vsToken '-(?<fs>[\d\.]+)[kK]Hz-(?<hp>\d+)HP'], 'names');
mVS2 = regexp(baseCore, ['^(?<cond>[^-]+)-' vsToken '-(?<fs>[\d\.]+)[kK]Hz-(?<pct>\d+)\%Load'], 'names');
if ~isempty(m1)
cond=string(m1.cond); rpm=str2double(m1.rpm);
fs_name=str2double(m1.fs)*1000; load_hp=str2double(m1.hp);
elseif ~isempty(m2)
cond=string(m2.cond); rpm=str2double(m2.rpm);
fs_name=str2double(m2.fs)*1000; load_pct=str2double(m2.pct);
elseif ~isempty(mVS1)
cond=string(mVS1.cond); is_vs=true;
fs_name=str2double(mVS1.fs)*1000; load_hp=str2double(mVS1.hp);
elseif ~isempty(mVS2)
cond=string(mVS2.cond); is_vs=true;
fs_name=str2double(mVS2.fs)*1000; load_pct=str2double(mVS2.pct);
end
if isnan(fs), fs = fs_name;
end
% ---------- 7) 输出(供命名与裁剪使用) ----------
rec = struct();
rec.data = data;
rec.varNames = varNames;
rec.fs = fs;
rec.rpm = rpm;
rec.load_pct = load_pct;
rec.load_hp = load_hp;
rec.condition = cond;
rec.is_vs = is_vs;
% 是否变速
rec.rpt_idx = rpt_idx;
% 重复测试编号(NaN 表示无编号)
end
%% =================== 把 table 列转成纯数值 ===================
function [A, vnames] = table_to_numeric(T)
V = T.Properties.VariableNames;
n = height(T);
cols = []; vnames = strings(0,1);
for i = 1:numel(V)
x = T.(V{
i
});
if isrow(x), x = x.';
end
if isnumeric(x)
num = double(x);
elseif islogical(x)
num = double(x);
elseif iscell(x) || isstring(x) || ischar(x) || iscategorical(x)
if iscategorical(x), x = cellstr(x);
end
if iscell(x)
try
s = string(x);
catch
s = string(cellfun(@(z)string(z), x, 'UniformOutput', false));
end
else
s = string(x);
end
s = strrep(s, " ", "");
s = strrep(s, ",", "");
% 千分位逗号
num = str2double(s);
else
continue;
% 其它类型不处理
end
if ~isnumeric(num) || all(isnan(num)) || numel(num)~=n
continue;
% 丢掉全 NaN 或长度不匹配的列
end
cols = [cols, num];
%#ok<AGROW>
vnames(end+1,1) = string(V{
i
});
%#ok<AGROW>
end
A = cols;
end
%% =================== 命名:定速/变速 + 重复测试后缀 ===================
function outname = make_outname(rec, origName)
% 条件码映射:H(健康) F(故障) I(内圈) O(外圈)
condMap = containers.Map( ...
{
'jiankang','healthy','health','jk','normal','健康', ...
'guzhang','fault','gz','faulty','故障', ...
'neiqian','neiquan','inner','nei','内圈','内环','内', ...
'waiquan','outer','wai','外圈','外环','外'
}, ...
{
'H','H','H','H','H','H', ...
'F','F','F','F','F', ...
'I','I','I','I','I','I','I', ...
'O','O','O','O','O','O'
} );
key = lower(string(rec.condition));
if condMap.isKey(key)
code = condMap(key);
elseif strlength(key)>
0
code = upper(extractBefore(key + " ", 2));
else
code = "X";
end
fs_khz = rec.fs/1000;
if isnan(fs_khz), fs_khz = 0;
end
fs_khz = round(fs_khz);
if ~isnan(rec.load_hp)
loadVal = rec.load_hp;
elseif ~isnan(rec.load_pct)
loadVal = rec.load_pct;
else
loadVal = 0;
end
loadVal = round(loadVal);
if isfield(rec,'is_vs') && rec.is_vs
baseName = sprintf('%s-VS-%d-%d', code, fs_khz, loadVal);
else
rpm = rec.rpm;
if isnan(rpm), rpm = 0;
end
baseName = sprintf('%s-%d-%d-%d', code, round(rpm), fs_khz, loadVal);
end
% 重复测试编号后缀
if isfield(rec,'rpt_idx') &&
~isnan(rec.rpt_idx)
baseName = sprintf('%s_RPT_%d', baseName, rec.rpt_idx);
end
outname = [baseName '.mat'];
if code=="X"
warning('无法从条件解析出代码:%s -> 用 X 代替(文件:%s)', string(rec.condition), origName);
end
end
%% =================== 工具:规范行文本 ===================
function lines = normalize_lines(lines)
lines = replace(lines, char(65279), "");
% BOM
lines = replace(lines, ",", ",");
lines = replace(lines, ";", ";");
end
5. 使用方法
% 1) 放置
% 将 batch_csv2mat.m 放到 MATLAB 当前工作目录
% 2) 执行(输出目录可省略,默认在输入目录下新建 mat/)
batch_csv2mat('F:\input', ...
'F:\mat_out');
% 3) 查看一个转换结果
S = load('F:\2025.9.6-欧瑞-6305-轴承\mat_out\H-100-2-0.mat');
% 举例
data = S.data;
plot(data.t, data.signal(:,1)); grid on
xlabel('Time (s)');
ylabel(data.varNames(1));
title('Channel 1');
6. 输出内容说明
- MAT 文件名:
H/I/O/F-<rpm>-<fs_kHz>-<load>.mat - MAT 内部变量:顶层变量
data(struct)signal:N×C,已统一换算(×0.15)t:N×1秒fs/rpm/load_pct:数值信息varNames:列名(中文保留)N/C:样本数/通道数
7. 常见问题与已处理
- “变量名被修改”警告 → 已强制保留原始列名(
VariableNamingRule='preserve'),并静默相关警告。 WhitespaceRule未知 → 仅在文本列存在且当前版本支持时才设置,不支持自动跳过。- “无法串联 double 和 cell” → 读取后对每列做数值化,全 NaN 列剔除,彻底避免此类报错。
- 找不到“序号”表头 → 启用“数据模式”兜底:定位第一行“像数据”的行,自动确定表头。
- 编码/分隔符混乱 → 自动尝试 UTF-8/GB18030/ISO-8859-1,分隔符支持英文/中文逗号、分号、Tab。
8. 一致性校验(可选)
想确认换算与数据无丢失,可做如下对比(把 CSV 原始数值×0.15 后与 .mat 比较):
csvf = '...原CSV路径...';
S = load('...对应的.mat');
data = S.data;
T = readtable(csvf,'VariableNamingRule','preserve');
A = table2array(T);
% [序号, 数据...]
X = A(:,2:end) * 0.15;
% 同步换算
fprintf('max abs diff = %.3g\n', max(abs(X(:)-data.signal(:))));
max abs diff 应该接近 0(浮点微小误差内)。
9. 可扩展方向
- 并行加速:外层
for可改parfor(需要 Parallel Toolbox)。 - 统一合并:把所有 MAT 聚合成一个大矩阵 + 索引表(condition/rpm/fs/load)。
- 自定义换算:把
scale = 0.15改成配置项,或为不同列设置不同系数。 - 更丰富的命名映射:在
condMap内继续扩展你的条件类别。
10. 结语
这套脚本针对“中文头信息 + 序号表头 + 多源编码/分隔符 + 列类型不一致”的工业 CSV 做了较强的兼容性处理,并把研究中常用的关键信息全部沉淀进 .mat(变量名、采样率、时间轴等),开箱即用。欢迎在此基础上继续定制:比如统一单位、自动频谱、批量可视化、或者和后续深度学习数据管线打通等。

浙公网安备 33010602011771号