MATLAB文件名自然排序:彻底解决frame_10排在frame_2前面的难题
2026/9/8 1:29:33 网站建设 项目流程

简介:在MATLAB开发环境中,文件名或路径的排序常因传统字符顺序而出现file2排在file10之后的问题,NaturalOrderFilenameSort正是为解决此类自然排序需求而设计。该工具将数字视为数值而非字符,快速准确地对含编号的文件进行排序,适合MATLAB开发者与数据分析师使用,适用范围涵盖数据批处理、日志归档、仿真结果整理等场景。压缩包共包含15个文件,体积仅19KB,其中5个m文件为源码与测试脚本,9个txt文件提供测试数据集或许可证信息,另有1个html格式的使用文档。核心函数natsort.m实现了数字解析与比较;natsortfiles.m进一步支持带路径的文件列表;natsortfiles_test.m包含覆盖正常、边界与异常情况的测试用例,便于验证和扩展。目前已有238人浏览学习。借助该资源,开发者不仅能获得可直接调用的排序工具,还能透过文档和测试样例掌握自然排序算法的实现思路,提升在MATLAB中编写、测试自定义函数的能力。 做批量图像处理的时候,我最烦的就是 MATLAB 里sort函数给出的文件名顺序。文件夹里明明是frame_1.png, frame_2.png, frame_10.png,用sort一跑就变成frame_1.png, frame_10.png, frame_2.png。原因不复杂——默认的sort是按字符逐位比较的,'10'的第二个字符是'0',比'2'小,所以frame_10被排到了frame_2前面。这也就是常说的“字典序”和“自然顺序”的区别。

今天把我在项目里一直在用的NaturalOrderFilenameSort思路完整拆一遍,包括核心原理、MATLAB 实现代码、实际批量读图的用法,以及各种容易踩的坑。如果你经常处理类似data_001.csvimage_100.pnglog_20240101.txt这类带数字编号的文件,那这篇文章应该能帮你省下不少时间。

1. 为什么文件名排序要“自然”:从 MATLAB 的 sort 说起

1.1 字典序和自然序的本质区别

字典序,就是英文词典里那种排列方式:先比较第一个字符,相同再比较第二个,以此类推。在 MATLAB 里对字符串元胞数组执行sort,用的就是这套逻辑。字母部分没问题,问题出在数字部分——字符'0''9'在 ASCII 码表里是连续的,但是'10''2'比较时,第一个字符'1'小于'2',所以'10'就会排在'2'前面。对人来说,2 比 10 小,但机器不知道“数字整体应该按数值大小判断”。

自然顺序则不同。它的核心思想是:把字符串里的连续数字当成一个完整的“数值”来看待,数值参与比较时按大小排序,其他字符仍然按字典序处理。于是frame_2会排在frame_10前面。这个规则非常符合直觉,也是很多文件管理器、视频剪辑软件里默认的排序方式。

具体差别看下面的例子就能秒懂:

names = {'file2.mat', 'file10.mat', 'file1.mat', 'file20.mat', 'file3.mat'}; % 字典序 sort(names)' % 结果:'file1.mat' 'file10.mat' 'file2.mat' 'file20.mat' 'file3.mat' % 自然序 naturalSort(names)' % 结果:'file1.mat' 'file2.mat' 'file3.mat' 'file10.mat' 'file20.mat'

1.2 哪些场景铁定需要自然排序

只要文件名里带有编号,并且编号位数不固定,几乎都会遇到这个问题。最常见的场景有:

  • 图像序列:工业相机连续采集的图片,命名通常是cam_0001.bmp, cam_0002.bmp, ..., cam_0100.bmp,但遇到cam_1.bmpcam_100.bmp混在一起时,字典序就乱了。
  • 日志文件:程序每天输出一个log_20240301.txt,连续几十个文件按日期排序,如果不做自然排序,log_20240310.txt会排在log_20240302.txt前面。
  • 遥感影像、医学影像数据:很多公开数据集按编号命名,比如subject_1.nii, subject_2.nii, ..., subject_100.nii,批量导入时排序不对,后面对应关系全乱。
  • 版本号排序:v1.0, v1.2, v1.10,想按版本先后排序,不能直接字典序。

我在做深度学习数据集预处理时,经常要把按帧号命名的图片和标签一一对应,排序错了,训练数据就废了。所以写一个稳定的自然排序工具,几乎成了批处理任务的标配。

2. 实现思路:把文件名拆成“数字”和“非数字”

2.1 用正则表达式切割文本

自然排序要解决的根本问题,是把“数字”从字符串中识别出来。实现上第一件事就是切割:把abc123def456这种字符串拆成{'abc', '123', 'def', '456'}这样的片段,数字片段和非数字片段交替出现。

MATLAB 里这一行正则就够了:

tokens = regexp(str, '\d+|\D+', 'match');

解释一下:\d+匹配 1 个或多个数字,\D+匹配 1 个或多个非数字,中间的|表示或逻辑,整体匹配出的结果就是连续的同类字符块。比如:

regexp('frame_10_v2.png', '\d+|\D+', 'match') % 得到 {'frame_', '10', '_v', '2', '.png'}

这个模式是“最长匹配”,所以'10'会被当成一个完整的数字块,而不会拆成'1''0'。这一步是整个工具的地基。

2.2 为什么选择“补宽度”而不是“逐段比较”

拿到拆好的片段后,最容易想到的办法是逐段比较:两个字符串先比第一段,如果都是数字就按数值比,否则按字符比;相等再比第二段。Java、Python 里很多自然排序库都是这么写的,逻辑也很清晰。

但 MATLAB 有个限制:内置sort函数不支持自定义比较器。你没法写一个compare(a, b)函数交给sort去调用。这就意味着,如果不想去调用 Java 或者写复杂的sortrows技巧,就得把所有字符串转成一种“可排序的键”,然后直接sort

最通用也最稳妥的转换方式就是“补宽度”。具体做法是:找出所有数字片段里的最大位数maxLen,然后把每个数字片段都用sprintf补零到maxLen位,再拼回完整字符串。比如最大位数是 4,那么'2'变成'0002''10'变成'0010'。这样排序时,'0002'在字典序上一定小于'0010',和数值大小顺序完全一致。

这种做法的好处是:一次转换,调用一次sort就能结束,速度很快,代码量也小。缺点是它会丢失原字符串里数字的前导零信息,但很少有人在排序时还关心'001''1'的区别,一般默认它们看作同一个数字。

2.3 输入规范与边界约定

写代码前先明确两件事:输入范围和处理规则。

输入方面,我规定函数接收一个字符串元胞数组,也就是{'a1.m', 'a10.m', 'a2.m'}这种结构。如果你用的是string类型,比如["a1.m" "a10.m"],函数开头转一下即可。

处理规则方面,我的约定是:

  • 连续数字按一个整体,按数值大小排序。
  • 非数字部分保持字符原样,按 MATLAB 内置的字典序规则排序。
  • 数字片段前导零不参与大小判断,但也不至于报错。
  • 排序是稳定的,即两个字符串生成的排序键完全相同时,保持它们在原数组中的相对顺序。

这些约定不一定适合所有场景,但在绝大多数文件名单场景里已经够用。

3. MATLAB 完整代码与使用演示

3.1 naturalSort 函数源码

下面这个函数是我一直在用的版本,做了基本错误处理和string数组兼容。你可以直接复制到.m文件里,函数名就叫naturalSort.m

function [sorted, idx] = naturalSort(cellstrArray) % NATURALSORT 对字符串元胞数组进行自然顺序排序 % 输入: % cellstrArray - 字符串元胞数组,也可以是 string 数组 % 输出: % sorted - 排序后的字符串元胞数组 % idx - 排序索引,满足 sorted = cellstrArray(idx) % % 示例: % naturalSort({'file2.txt','file10.txt','file1.txt'}) % ans = {'file1.txt','file2.txt','file10.txt'} if nargin < 1 error('naturalSort:InvalidInput', '需要提供一个字符串数组'); end % string 数组转元胞数组 if isstring(cellstrArray) cellstrArray = cellstr(cellstrArray); end if ~iscellstr(cellstrArray) error('naturalSort:InvalidInput', '输入必须为字符串元胞数组或 string 数组'); end % 第一步:按数字 / 非数字切割所有字符串 tokens = regexp(cellstrArray, '\d+|\D+', 'match'); % 第二步:统计所有数字片段的最大长度,用于补零 maxDigitLen = 1; for i = 1:numel(tokens) tk = tokens{i}; for j = 1:numel(tk) if ~isempty(regexp(tk{j}, '^\d+$', 'once')) maxDigitLen = max(maxDigitLen, numel(tk{j})); end end end % 第三步:生成排序键 sortKeys = cell(size(cellstrArray)); for i = 1:numel(cellstrArray) tk = tokens{i}; key = ''; for j = 1:numel(tk) if ~isempty(regexp(tk{j}, '^\d+$', 'once')) % 数字部分转为数值后补零,自动忽略前导零 key = [key, sprintf('%0*d', maxDigitLen, str2double(tk{j}))]; else key = [key, tk{j}]; end end sortKeys{i} = key; end % 第四步:对排序键排序,返回索引 [~, idx] = sort(sortKeys); sorted = cellstrArray(idx); end

代码里的核心就三步:切割、补零、排序。str2double把数字片段转为数值,再sprintfmaxDigitLen位补零。这样'2''10'会变成'0002''0010',字典序就对了。

3.2 基础用法与对比测试

把这个函数保存好后,跑下面这段测试:

files = {'img_1.png', 'img_10.png', 'img_2.png', 'img_20.png', 'img_3.png'}; % 默认 sort sort(files)' % ans = 'img_1.png' 'img_10.png' 'img_2.png' 'img_20.png' 'img_3.png' % naturalSort naturalSort(files)' % ans = 'img_1.png' 'img_2.png' 'img_3.png' 'img_10.png' 'img_20.png'

再看一个稍微复杂的例子,文件里有下划线、版本号和多段数字:

names = {'v1.10beta', 'v1.2beta', 'v1.10alpha', 'v1.2alpha'}; naturalSort(names)' % 结果:'v1.2alpha' 'v1.10alpha' 'v1.2beta' 'v1.10beta'

因为数字片段'2''10'被补成等宽后,排序键先按 alpha/beta 的字典序区分,再按数字大小区分。如果希望数字优先于字母,就需要调换切割策略,但文件名排序一般不需要这么细。

3.3 进阶:大小写不敏感和忽略前导零

有些场景要求忽略文件名大小写,比如ABC_1.jpgabc_2.jpg不应该因为 ASCII 码差异排得乱七八糟。做法很简单:构造排序键时,把非数字部分统一转成小写(或大写),原字符串保持不变。

修改的地方只有一处,在生成key的循环里:

if ~isempty(regexp(tk{j}, '^\d+$', 'once')) key = [key, sprintf('%0*d', maxDigitLen, str2double(tk{j}))]; else key = [key, lower(tk{j})]; % 统一转小写 end

注意,lower只作用在排序键上,输出的sorted仍然是你原来的大小写形式。

如果你希望保留前导零的比较,也就是'001''1'不相等,那就不应该用str2double,而是直接把原始数字片段补到maxDigitLen位。比如'001'本身已经 3 位,补到 4 位变成'0001',而'1'补成'0001',它俩排序键相同,顺序会保持原样。这个行为到底合理不合理,取决于你的业务需求,我在后面常见问题里会展开说。

4. 实战:批量读取图像序列的完整流程

4.1 从文件夹读取并自然排序

自然排序最常见的落地场景,就是配合dir函数读文件列表。比如我有一个文件夹,里面全是frame_1.pngframe_300.png,正常写法是:

folder = 'D:\experiment\frames'; fileInfo = dir(fullfile(folder, '*.png')); fileNames = {fileInfo.name}; sortedNames = naturalSort(fileNames);

这里注意,dir返回的是结构体数组,{fileInfo.name}可以一次性把所有文件名取出来,格式正好是元胞数组,直接喂给naturalSort

如果文件名带有路径,语法稍微变一下:

sortedPaths = fullfile(folder, sortedNames);

fullfile会自动处理系统路径分隔符,Windows 是反斜杠,Linux/macOS 是正斜杠,不用自己拼。

4.2 按排序结果批量读取和处理

拿到排序后的文件列表,后面就顺理成章了。以读取图像序列并计算每张图的均值灰度为例:

folder = 'D:\exp\images'; fileInfo = dir(fullfile(folder, '*.png')); fileNames = naturalSort({fileInfo.name}); numFiles = numel(fileNames); meanGray = zeros(numFiles, 1); for i = 1:numFiles img = imread(fullfile(folder, fileNames{i})); if size(img, 3) == 3 img = rgb2gray(img); end meanGray(i) = mean(img(:)); end

有了自然排序,meanGray的第一行就是第一帧,第二行就是第二帧,不会出现 10 帧排在 2 帧前面的情况。做时间序列分析、生成训练数据、批量化预处理的时候,这一步很关键。

4.3 性能建议和内存优化

如果文件数量特别大,比如几万个文件,naturalSort本身的性能仍然可以接受,因为核心只是regexpsort。但有几个细节需要注意:

  • 尽量只对文件名排序,不要提前构建完整路径元胞数组再排序。路径文件虽然不多,但每个字符串更长,比较成本更高。
  • dir返回的所有文件会包含子文件夹名,如果只想看文件,需要过滤~[fileInfo.isdir]。这一点我经常忘,先提个醒。
  • 如果文件命名比较规范,比如固定前缀加纯数字后缀,其实可以不用正则,直接解析数字再sortrows,但那属于特化方案,代码复用性差。通用性优先时,naturalSort更合适。
  • 排序时需要一个和原数组等大的sortKeys元胞数组,如果文件名特别长且数量极大,内存占用会翻倍。不过对普通场景来说,这个开销可以忽略。

5. 踩坑记录与常见问题

5.1 数字超过 double 精度怎么办

str2double对超过 15~16 位有效数字的整型会丢失精度。比如'99999999999999999999',转成 double 后近似为1e20,再sprintf出来的补零键就会失真,排序顺序可能出错。

解决办法有两个:

一是去掉str2double,直接对原始数字片段补零。因为文件名里的数字通常不会刻意写前导零,补足到相同长度后,字典序仍然能反映数值大小。例如'2'补成'0002''10'补成'0010',没问题。只有遇到'002''2'这种混用前导零的情况才会认为相等。修改后的代码片段如下:

key = [key, sprintf('%0*d', maxDigitLen, str2double(tk{j}))]; % 改为 key = [key, sprintf('%0*s', maxDigitLen, tk{j})];

二是使用uint64转换,但 MATLAB 的uint64最大到 18446744073709551615,超过这个范围仍然无能为力。文件名里出现超过 20 位数字的概率极低,所以我日常还是用第一种方案。

5.2 小数点和负号怎么处理

严格来说,\d+|\D+这个正则不会把-1当成一个负数,它会把'-'当字符串,把'1'当数字。所以'data_-1.csv'排序时,负号会排在正号前面,'-2''-''-1''-'一样,再比较后面数字'1''2',得到的是-1小于-2,这和数值直觉相反。

如果确实需要支持负数排序,正则要改成:

tokens = regexp(str, '-?\d+|\D+', 'match');

小数是另一个坑。文件名里如果出现v1.2v1.10,按整数分段处理后,'1''2''10'都是独立数字段,结果会把v1.1010看作比v1.22大,这在小版本号语义下可能是错的。如果想按真正的数值(1.2 < 1.10)排序,正则要改成匹配浮点数:

tokens = regexp(str, '\d+(?:\.\d+)?|\D+', 'match');

但注意,这个正则会把'v1.2.3'里的1.2.3拆得不太直观,反而引入新的乱序。所以我的建议是:如果明确处理版本号,最好单独写版本比较逻辑;如果是普通文件名的数字编号,整数排序就足够了。

5.3 中文和特殊字符排序异常

MATLAB 的sort对字符串排序按字符编码进行。早先版本里中文字符排序结果和你系统默认 locale 有关,不同机器可能不一样。更麻烦的是,文件名里的空格、#&等特殊字符,排序时会被当成普通字符,它们的 ASCII 码比数字和字母都小,可能导致'a file'排在'afile'前面,有时和资源管理器的排序结果不同。

这不是naturalSort能解决的问题,它只是把数字语义化,非数字部分仍然交给了sort。如果希望完全复刻某款文件管理器的排序规则,需要额外设计字符映射表,但实际工程中很少有人这么干。我的建议是:在项目初期就统一文件命名规则,尽量用字母 + 数字 + 下划线,避免空格和特殊符号,能省掉大量排序相关的麻烦。

5.4 排序稳定性问题

关于两个文件生成相同排序键的情况,比如'img_001.png''img_1.png',在忽略前导零的策略下,它们的排序键完全相同。此时最终顺序取决于sort的稳定性。MATLAB 官方文档明确说明sort对相等元素保持它们在输入中的相对顺序,也就是稳定排序。所以'img_001.png'在前的顺序会被保留下来。

但这里我要提醒一点:MATLAB 的稳定排序行为,在sortrows上同样适用。如果你后续修改代码,用sortrows按多列排序,文档里也有对应说明。依赖稳定性不是坏事,但最好在代码注释里写明,否则几个月后你自己都会忘记为什么是这个顺序。

我的一点个人体会

这个naturalSort函数我在好几个项目里都用过,小到整理几十个实验数据文件,大到处理上万张医疗影像序列,代码基本没改过。最实用的技巧是把它和dir的一行流组合在一起:sortedNames = naturalSort({dir(fullfile(folder, '*.jpg')).name}),读取列表、排序、后续处理一气呵成。如果哪天你也被 MATLAB 的“字典序”整得心烦,不妨把这段代码存下来,下次遇到文件名排序问题直接用。

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

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

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

立即咨询