用MATLAB做批量重命名,这事儿第一次听起来可能有点冷门,但一旦你需要给几百个文件统一改名字,就会觉得真香。我先说个实际场景:项目收尾要归档数据,文件夹里两百多张实验截图,命名全是Figure_1、untitled_3这种,平台要求统一改成日期_项目代号_三位序号。手动一个个改不现实,Windows文件管理器的多选重命名又只能加前缀和序号,不能把旧名字清掉。我当时的数据本来就在MATLAB里,顺手写了个循环,十几行代码半分钟收工。这篇文章就把这套思路完整拆开:从最底层的dir和movefile两个函数讲起,再到加前缀、改扩展名、替换关键字、正则规则、处理子文件夹这些实际场景,最后附上我踩过的坑和一个能直接拿去用的通用批量重命名函数。
1. 批量重命名的核心思路其实很简单
1.1 任何语言做这件事都是同样的四步
批量重命名听起来像是文件管理器的活,但本质就是四步,跟用什么工具没关系:先是拿到文件清单,然后一条条遍历,接着算出每个文件的新名字,最后把旧文件变成新文件。区别只在于这四步分别用什么手段完成。
在MATLAB里,四步的推荐搭配我直接用实际经验给你列出来:
- 拿清单用
dir - 遍历用
for循环 - 算新名字用
fileparts、strrep、regexprep、sprintf按需组合 - 执行改名用
movefile
有人会问,MATLAB里没有rename这种函数吗?严格说没有针对文件的重命名专用内置函数,但换个角度想一下就通了:重命名的本质就是把一个文件在同一个目录里从旧名字变成新名字,这不就是一次移动操作吗?所以movefile这个用于移动文件的神器,就成了最顺手的改名工具。这也是这套方案最关键的一个思维转换。
多说一句,很多教程会让人用system命令去调用操作系统自己的ren或mv,那样也能行,但跨平台兼容性差,返回信息的可读性也差。所以我个人优先用纯MATLAB函数,只有在极端大批量的场景才会考虑系统命令,这一点后面会专门展开。
1.2 为什么拿MATLAB来干这个活
先说明,我不是说所有批量重命名都应该用MATLAB。文件数量少、规则也简单,直接用操作系统自带的资源和第三方小工具可能更快。但如果碰到下面任意一种情况,MATLAB的优势就很明显:
- 命名规则比较复杂,比如"把第3到第8个字符抽出来重新拼成新名字""根据修改时间生成日期前缀""把文件夹里所有包含特定关键字的文件按某种顺序重新编号"。这种规则在图形界面工具里常常无从下手,在MATLAB里只是几行表达式的事。
- 重命名需要纳入现有工作流。比如数据分析和可视化本来就在MATLAB里,跑完一批图顺便重命名归档,不需要切到别的软件。
- 规则可能会改。脚本里改一个变量重新跑一次就行,比在图形界面里重新勾选几百个文件省事得多。
我做了个直观对比:
| 方案 | 规则灵活度 | 跨平台 | 适合场景 |
|---|---|---|---|
| 系统自带多选重命名 | 低,只能加前缀和序号 | 仅当前系统 | 简单、数量少 |
| 第三方批量重命名工具 | 中等 | 通常限当前系统 | 桌面文件批处理 |
| MATLAB脚本(dir+movefile) | 高,可用函数句柄和正则 | 全平台 | 规则复杂、数量大、嵌入流程 |
所以这个方案不是用来替代所有重命名工具的,而是在"规则复杂""追求可重复""避免反复手动操作"这几个诉求下,最顺手的一条路。
2. 认识两个核心函数:dir和movefile
2.1 dir:文件清单全靠它
dir返回的是一个结构体数组。每次调用,返回结果里的每个元素就代表目录下的一个条目,常用字段如下:
| 字段 | 含义 |
|---|---|
| name | 文件名,不含路径 |
| folder | 文件所在文件夹的完整路径 |
| bytes | 文件大小,单位字节 |
| isdir | 逻辑值,1表示文件夹,0表示文件 |
| datenum / datetime | 修改日期 |
调用方式上有两个细节必须注意。
第一个,dir(folder)会返回当前目录下的所有条目,包括表示当前目录的.和上一级的..,新手经常踩这个坑。所以在实际脚本里,我一般会写成dir(fullfile(folder, '*.jpg'))这样的形式,让dir只返回匹配文件名模式的文件,这样.和..就自动不在结果里了,省事很多。
第二个,dir返回的文件名只有名字部分,不含路径。你要做的是把文件所在目录和文件名重新拼起来,这时候一定不要自己用字符串拼接[folder '\' name]这种方式,因为Windows用反斜杠、Linux和Mac用正斜杠,不同系统下会出问题。老老实实用fullfile,它能自动适配当前系统的路径分隔符。
2.2 movefile:改名的本质是一次移动
movefile是MATLAB里移动文件和文件夹的函数,最基本的用法是这样:
movefile('D:\data\old.txt', 'D:\data\new.txt');源路径和目标路径在同一个目录下的时候,这个操作的效果就是重命名。说得再直白一点,任何重命名在文件系统层面都是"从一个名字移动到另一个名字",movefile正好把这个动作完整暴露出来了。
movefile还有一个可选参数'f',作用是允许覆盖只读属性文件。但这个参数要谨慎用,后面讲坑的时候会专门提到——绝大多数情况下的数据丢失,不是因为有只读文件挡路,而恰恰是默认的"同名文件直接覆盖"行为。
另外,movefile有两个返回值:
[ok, msg] = movefile(oldFile, newFile);ok为1表示成功,为0表示失败,msg是失败时的错误信息。在批量循环里我强烈建议用这种带返回值的写法,即使某个文件失败了,循环也能继续跑下去,做完之后统一看哪些失败了,比一个文件报错就中断整个批处理要舒服得多。
2.3 第一版脚本跑通
把一个思路落成代码,最朴素的第一版是这样的:
folder = pwd; % 换成你自己的目录 files = dir(fullfile(folder, '*.jpg')); for k = 1:length(files) oldFile = fullfile(folder, files(k).name); newFile = fullfile(folder, ['backup_' files(k).name]); [ok, msg] = movefile(oldFile, newFile); if ~ok warning(msg); end end这段脚本把当前文件夹下所有jpg文件统一加上backup_前缀。你可以先在某个测试文件夹里放两个文件跑一遍,确认效果没问题,再换成正式目录。其实整个批量重命名就是从这个几句话的小循环扩散开来的——后面所有看似复杂的场景,都是在"算新名字"这一步上做文章。
需要提醒一件事:脚本一旦跑起来,旧文件名就没了,而且movefile没有undo机制。所以第一版务必放在测试目录里试跑,这一点我后面还会反复强调。
3. 从能跑到好用:三个高频场景
3.1 批量加前缀
加前缀恐怕是出现频率最高的需求。实验数据归档、给素材打标签、给一批图片加系列编号,都是一个套路。
folder = 'D:\experiment\result'; prefix = '20250402_归档标记_'; files = dir(fullfile(folder, '*.png')); for k = 1:length(files) oldFile = fullfile(folder, files(k).name); newFile = fullfile(folder, [prefix files(k).name]); movefile(oldFile, newFile); end注意prefix变量是写死的,你也可以改成用sprintf生成带序号的:prefix = sprintf('20250402_%02d_', k)。这样每条文件的前缀里还能带上序号,归档的时候看一眼文件名就知道它在处理流程里的顺序。如果担心k和旧文件顺序对不上,先对files做一次排序再跑,排序问题我在第5节会专门讲。
3.2 批量改扩展名
另一个高频场景是把一批文件的扩展名统一修改。比如平台只收jpg不收png,或者之前导出时格式选错了。用fileparts把文件名拆成"主名+扩展名",然后重组:
files = dir(fullfile(folder, '*.png')); for k = 1:length(files) [~, name] = fileparts(files(k).name); oldFile = fullfile(folder, files(k).name); newFile = fullfile(folder, [name '.jpg']); movefile(oldFile, newFile); endfileparts函数有三个常见输出参数:路径、主文件名、扩展名。这里我只需要主文件名,所以前面写了~来忽略路径。用[name '.jpg']拼接新扩展名,比用正则去替换更直接,因为fileparts已经把原来的扩展名剥掉了,不会发生误替换文件名中间某个点和字符的情况。
改扩展名这件事要小心:把png改成jpg并不会真的让图片格式转换,只是文件名后缀变了,文件内容还是PNG编码。如果下游工具严格按照扩展名判断格式,可能还是会出现读不了或显示异常的情况。这种需求真正要做的是格式转换,那就得用imread加imwrite了。文件名层面的批量修改只是名义上改后缀,两者别混为一谈。
3.3 批量替换文件名中的关键字
第三种最常见就是把一批文件里的某个关键字换成另一个。这里分为字面替换和正则替换两档,先讲字面的。
files = dir(fullfile(folder, '*')); for k = 1:length(files) if files(k).isdir continue end oldName = files(k).name; newName = strrep(oldName, '旧项目号', '新项目号'); if strcmp(oldName, newName) continue end movefile(fullfile(folder, oldName), fullfile(folder, newName)); endstrrep是纯文本替换,不认特殊符号。比如文件名叫data.1.txt想替换成data.2.txt,直接strrep(oldName, 'data.1.txt', 'data.2.txt')即可,不会把.当成特殊字符。很多人一开始用regexprep反而会踩正则的坑,所以我的建议是:能字面说明白的需求,一律用strrep;确实需要模式匹配的,再上正则。这段代码里还用到了files(k).isdir判断,这是为了跳过文件夹,只处理文件。
4. 进阶玩法:正则替换和子目录递归
4.1 正则表达式替换:打开规则的大门
当规则从"某一段字符换成另一段"变成"某种模式换成另一种模式"时,就该regexprep上场了。
举一个我实际处理过的例子。一批数据文件叫data.2023.raw.png、data.2023.processed.png这种,需要把年份从2023批量改成2024。新手很容易写成这样:
newName = regexprep(oldName, '.2023.', '.2024.'); % 错误写法这里的.在正则里代表"任意一个字符",所以表达式'.2023.'会匹配"任意字符 + 2023 + 任意字符",结果就是它不光匹配到了年份,还可能把前后不该动的字符也替换掉,甚至可能匹配到完全不相干的位置。正确写法是给点号加反斜杠转义:
newName = regexprep(oldName, '\.2023\.', '.2024.');再复杂一点的例子:想统一把文件名里的日期格式从20230405改成2023-04-05,用正则也能完成,但定位和替换的复杂度会明显上升。我的建议是别在正则里堆太复杂的表达式,MATLAB正则可以在命令行里反复实验,先拿两三个样例文件名试到准确为止,再批量执行。每一个正则表达式在正式跑之前都值得花两分钟验证一下,否则误伤一批文件你会非常后悔。
4.2 用**扫描所有子目录
实际归档时,文件不会老老实实躺在一个文件夹里,更多的场景是散落在多级子目录中。MATLAB从R2016b开始,dir支持**这个路径模式,一个通配符就能递归匹配多层子目录:
files = dir(fullfile(root, '**', '*.jpg'));比如root下面有实验1/正常组、实验1/异常组、实验2这些子目录,上面一行就能把所有这些目录下的jpg全部找出来。而且返回结果里的folder字段会对应到每个文件实际所在的子目录路径,重命名的时候用fullfile(files(k).folder, files(k).name)拼路径,不会弄错位置。
如果你的MATLAB版本比较老,**用不了,需要自己写递归。核心逻辑其实不难:先在函数内部处理当前目录,再对每个子目录调用自身:
function batchRenameRecursive(folder, prefix) files = dir(fullfile(folder, '*.jpg')); for k = 1:length(files) movefile(fullfile(folder, files(k).name), ... fullfile(folder, [prefix files(k).name])); end subDirs = dir(folder); for k = 1:length(subDirs) if subDirs(k).isdir && ~ismember(subDirs(k).name, {'.', '..'}) batchRenameRecursive(fullfile(folder, subDirs(k).name), prefix); end end end递归版本有个好处:处理规则想扩展也容易,比如可以只处理某个文件名模式的子集,也可以对每个子目录带上不同的前缀。不过日常用**已经足够,递归函数可以作为版本兼容的备选方案存着。
4.3 封装一个通用的批量重命名函数
学到这里,每次写循环其实蛮烦的,而且循环体和"算新名字"的代码混在一起,规则复杂一点就会很乱。更好的做法是封装一个通用函数,把"遍历和执行"固定下来,只留"命名规则"给使用者去定义。
我最终沉淀下来的函数是这样的:
function okCount = batchRename(folder, pattern, nameRule) % folder : 目标文件夹,例如 'D:\data' % pattern : 文件匹配模式,例如 '*.jpg' % nameRule : 函数句柄,输入旧文件名,返回新文件名 items = dir(fullfile(folder, pattern)); okCount = 0; for k = 1:length(items) if items(k).isdir continue end oldName = items(k).name; newName = nameRule(oldName); if isempty(newName) || strcmp(oldName, newName) continue end [ok, msg] = movefile(fullfile(folder, oldName), ... fullfile(folder, newName)); if ok okCount = okCount + 1; else warning('重命名失败: %s -> %s, 原因: %s', oldName, newName, msg); end end endnameRule是一个函数句柄,你只需要定义"给旧名字,返回新名字"的规则,剩下的遍历、拼路径、错误处理交给函数统一做。调用方式非常直白:
% 统一加前缀 batchRename(pwd, '*.jpg', @(n) ['归档_' n]); % 把扩展名从 png 改成 jpg batchRename(pwd, '*.png', @(n) [n(1:end-4) '.jpg']); % 替换关键字 batchRename(pwd, '*', @(n) strrep(n, '旧', '新'));一个小细节:n(1:end-4)字符串切片时,我假设文件名末尾是4个字符的扩展名.png,所以去掉最后4个字符。如果你的文件名或扩展名长度不确定,就先用fileparts拆一下再拼,那样更稳。这个函数我自己的项目里用了很久,每次碰到批量重命名需求,都是先改一行nameRule,再跑一次,结果很稳定。
5. 常见坑与排查方法实录
5.1 文件名的排序和我们想的不一样
dir返回的文件列表是按字典序排列的,不是自然排序。什么是字典序?就是image_10.jpg会排在image_2.jpg前面,因为字符串比较的时候,到第三个字符开始逐位比较,1小于2,所以10排在2前面。你想按数字顺序给文件编序号,直接拿dir的顺序来用就错了。
解决思路是提取文件名里的数字,按数字排序:
files = dir(fullfile(folder, '*.jpg')); nums = zeros(length(files), 1); for k = 1:length(files) token = regexp(files(k).name, '\d+', 'once', 'match'); if isempty(token) nums(k) = inf; else nums(k) = str2double(token); end end [~, idx] = sort(nums); files = files(idx);这是一个朴素的数字排序,能解决大多数编号需求。如果文件名里有多个数字或者数字位数不固定,这个写法就不够用,更稳妥的办法是找一个支持自然排序的现成工具函数,或者在排序之前先统一改名的前缀规则,让文件名本身变得更有规律。我自己的习惯是:如果最终要给文件重新编号,先按修改时间排序,再依据当前顺序生成三位序号,这样跟"处理先后"的直觉一致。
5.2 同名文件会被静默覆盖,这是最容易丢数据的地方
前面提到过,movefile在目标文件已存在、且目标文件不是只读的情况下,会直接覆盖,而且不报错。这是个非常隐蔽的坑。比如你有report_2023.pdf和report_2024.pdf两个文件,批量改成final_report.pdf,后者就会悄悄把前者覆盖掉,数据找不回来。
我在封装batchRename时故意加了一步:新老名字完全相同时就跳过。但这只能避免重复改名造成的自我覆盖,真正的同名覆盖还得自己在命名规则里规避。执行前最稳妥的做法是先检查目标是否存在:
existingNames = {files.name}; for k = 1:length(files) newName = nameRule(files(k).name); if isempty(newName) || strcmp(newName, files(k).name) || ... ismember(newName, existingNames) continue end % 到这里说明新名字没有冲突,可以执行 movefile 了 end其中ismember(newName, existingNames)就是检查目标名字是否已经被列表里的某个文件占用。虽然执行时仍有竞态风险,但单线程顺序执行时,这个检查已经能挡住绝大多数误覆盖。
5.3 文件被其他程序占用,就是改不动
Windows下,如果一个文件正被编辑器、看图软件、Office等程序打开着,movefile通常拿它没辙,会报类似"文件被占用""无法访问"这类错误。这种情况不是脚本逻辑问题,纯粹是文件被锁住了。
排查思路很简单:先看报错信息里是哪个文件,把它在打开它的程序里关掉,然后重新跑脚本。批量循环中用了[ok, msg] = movefile(...)这种写法的话,单个文件失败不会中断整个任务,最后检查输出记录,把失败的那几个单独处理即可。我试过在重命名之前把所有会占用文件的软件都关掉,成功率会提高不少,这属于操作习惯上的细节。
5.4 中文文件名在旧版本可能乱码
如果你用的MATLAB版本比较老,在Windows系统上处理中文文件名,有时会看到一串意义不明的乱码,这是因为文件系统返回的编码和MATLAB内部字符编码不一致。老版本对这块的支持一直不算好。
解决建议依次是:能升级就升级;不能升级的话,尽量避免在脚本里直接拼中文,可以改用通配符加英文规则匹配;实在要处理中文文件名,先单独打一段调试代码看看dir返回的名字到底是什么,再决定怎么适配。新版本里中文文件名读写基本稳定,我这两年在较新的环境里处理过带中文的归档文件,没有遇到乱码。
5.5 必不可少的"先预览再执行"
最后一条与其说是坑,不如说是习惯:任何批量重命名脚本,正式执行前先跑一遍"只打印不执行"的预览模式。
for k = 1:length(items) if items(k).isdir continue end oldName = items(k).name; newName = nameRule(oldName); if ~strcmp(oldName, newName) fprintf('%s -> %s\n', oldName, newName); end end就这几行,值得每次都跑一遍。它不会花多少时间,但能让你在执行前看到完整的新旧对应关系。我的经验是,有七成左右的命名问题在预览阶段就能一眼发现,比如新名字里有空格、后缀重复、某几个文件没匹配上规则。别嫌这步麻烦,它比改错之后再想办法恢复要划算得多。
6. 实操心得:从能用变成好用
6.1 动手之前的三件事
这几条是我自己吃了亏之后养成的习惯,不管多急的重命名需求,我都不会跳过。
第一,备份。把要批量的整个文件夹复制一份到临时目录再操作。文件数量不大时直接复制就行;文件很大时,至少把命名规则最复杂的文件单独备份一份。movefile没有内置撤销机制,别迷信"应该没问题"这句话。
第二,小范围试跑。正式执行之前,先用dir确认匹配到了哪些文件,再拉出前三个文件做一遍预览。这一步可以和备份穿插进行:先在备份目录里跑第一版脚本,确认没问题,再去正式目录跑第二版。
第三,记录规则。在脚本开头注释写清楚这次重命名做了什么,比如"把2023替换成2024,范围是data文件夹下所有raw图像"。归档类的活过几个月回头看,有注释能省下大量回忆成本。
6.2 文件特别多的时候怎么提速
movefile本质是一次一次调用系统文件操作,几千个文件可能就要好几秒甚至更久。如果文件数量到了上万级别,或者你在处理大量小文件,可以考虑绕开movefile,直接调用系统命令。
Windows上可以用ren:
system(['ren "' fullfile(folder, oldName) '" "' newName '"']);需要提醒,ren的旧路径可以带完整路径,但新文件名不能带路径,也就是说新名字只能是文件名。Linux和Mac系统是mv,两个参数都可以带完整路径:
system(['mv "' fullfile(folder, oldName) '" "' fullfile(folder, newName) '"']);用system的代价是跨平台代码不通用,字符串拼接时路径里有空格也容易出错,我加了一对双引号来规避空格问题。所以我的建议是:几千个文件以内,直接用movefile,稳定、跨平台、报错信息友好;到了万级规模再考虑系统命令。另外,循环里每调一次system都要启动一个新的系统进程,其实也有开销,更彻底的提速是把所有改名命令写进一个批处理脚本一次性执行,但那样出错后排查反而麻烦,我就不推荐了。
6.3 还能扩展出哪些玩法
批量重命名这个能力本身很小,但它能嵌到更多流程里。
比如根据文件的修改时间生成新名字。dir返回结果里有datenum或datetime字段,用datetime(files(k).datenum, 'ConvertFrom', 'datenum')拿到时间对象,再格式化成yyyyMMdd_HHmmss拼进文件名。这样归档时不需要打开每个文件看属性,就能知道它们的大致产生时间,非常实用。
再比如结合表格的映射关系。如果有一张表存着"旧文件名到新文件名"的对应关系,可以在MATLAB里用readtable读进来,然后遍历文件列表,到表里查新名字,查不到就跳过。这种玩法对交接类的重命名任务特别管用,规则是别人给定的,你只要负责执行。
还有我后来做的一个改动:把batchRename函数放进了自己的个人工具箱,启动MATLAB时自动加载。每次收到重命名需求,只要写好nameRule那一行,剩下的全部交给函数。这个函数处理过的文件数量可观,出了问题的次数很少,而且问题基本都是规则本身写错了,在预览阶段就已经发现。
再提一个我自己的小习惯:命名规则里尽量少用中文。不是歧视中文文件名,而是跨平台、跨版本、跨编码环境下,中文文件名始终是那个"可能出问题""不好排查"的点。能用字母、数字、下划线表达清楚意思,就尽量用,归档文件会省心很多。如果实在需要中文描述,建议单独维护一个对照说明文件,而不是把中文直接写死在文件名里。
批量重命名这事本身不大,但做对做错差别很大。规则明确、先预览、再加一层备份,基本不会翻车。我个人处理了大量文件之后,最大的体会是:越是看起来小的自动化脚本,越值得把边界条件想清楚。下次你面对几百个乱糟糟的文件名时,不妨让MATLAB试试。