ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Anysort自定义网易云歌单排序:从排序算法到JavaScript实践

用Anysort自定义网易云歌单排序:从排序算法到JavaScript实践 不知道你有没有这种经历歌单建了好几年早先收藏的歌和最近循环的歌混在一起想找一首老歌得往下翻好几屏。网易云自带的排序常年只有“按添加时间”“按播放次数”几个固定选项想按歌手排、按歌名排、按时长排甚至按我自己心里的“好听程度”排基本做不到。这个项目就是我用 Anysort 这个排序库自己写一套歌单排序逻辑把所有歌曲按我想要的规则重新编排。这不是什么高深的大工程说白了就是“把歌单变成结构化数据再用一个足够灵活的排序库去折腾它”。这篇东西适合三类人看一是歌单特别长、被网易云固定排序折磨得不行的人二是想学 JavaScript 排序但不想只看教科书例子的初学者三是对 Anysort 这个排序库感兴趣、想看看它到底能干什么的人。我会把数据怎么拿、排序怎么写、结果怎么用、坑在哪里一次讲清楚。1. 先搞清楚为什么要自己写排序1.1 网易云自带排序的痛点在哪儿网易云的“排序”功能并不是不好用而是太固定了。以我自己这个七百多首歌的歌单为例网页端能选的排序维度大概就是添加时间、播放次数、歌曲时长这么几个手机端稍微多一点但也绕不开热度、时间这些基础维度。一旦你想按“歌手名”“专辑发行年份”“我最喜欢的顺序”来整理就只能手动拖动一次拖一首七百首曲子拖到天亮也拖不完。更麻烦的是网易云网页端和客户端对“排序”的处理还不是同一套逻辑。网页端改完顺序客户端不一定同步在某个端点上“按播放次数排序”它会立刻把整个列表打乱我之前手动分好的分组全部白费。而且这种排序是“临时视图”我没法把一套自定义顺序保存成固定歌单顺序下次打开又回到默认了。这就引出一个很实际的需求我需要的不是“在网易云里面多一个排序按钮”而是一个“独立于网易云之外、可以重复运行、规则我自己定”的排序工具。排序对象是歌单里的歌曲数据排序规则就是一段我写的比较逻辑。这个思路一旦成立后面所有事情都很顺。1.2 为什么偏偏选 Anysort 这个排序库有人会问JavaScript 不是自带Array.prototype.sort()吗为什么还要引入 Anysort这个质疑很合理但实际用下来原生sort有几个绕不过去的问题。原生sort默认把数组元素转成字符串再按 UTF-16 编码排序。数字数组[10, 9, 100]直接排序会得到[10, 100, 9]这种结果因为你得先记得传入比较函数(a, b) a - b。这个算是常识但真正麻烦的是它不支持“算法选择”。原生排序在不同引擎里的底层实现不一样你没法指定“我要用归并排序保持稳定性”也没法在特定场景改用插入排序来加速。对大多数开发者来说这无所谓但一旦你要处理一些极端场景比如几万条本地音乐文件按文件名排序原生sort的能力边界就暴露了。Anysort 这个排序库解决的就是“把排序算法和排序规则解耦”这件事。它内置了多种排序算法比如快速排序、归并排序、插入排序、堆排序、希尔排序、选择排序这些你只需要传入数组、可选的算法名、可选的比较函数剩下的交给它。默认情况下它是稳定排序而且不会修改原始数组而是返回一个新数组——这一点对我这种不想把歌单原始数据弄乱的人来说非常关键。还有一个加分项Anysort 支持自定义算法。官方文档里你可以写一个自己的排序实现传进去虽然日常用不到但说明这个库的设计思路是开放的不是把死逻辑焊死在里面。我后面做自定义权重排序的时候就是在“自定义比较函数”这一层发力整个体验非常顺手。1.3 排序库绕不开的几个核心概念在往下写代码之前先把几个概念用大白话捋一遍。排序算法本质上就是“整理一摞扑克牌的思路”。快速排序的思路是“先随便抽一张把比它小的放左边比它大的放右边再对左右两堆重复同样的操作”插入排序的思路是“从第二张开始每次把当前这张抽出来插到前面已经排好序的牌堆里”归并排序则是“把整副牌拆成单张再两两合并成有序小堆最后合并成一摞”。比较函数comparator就是“你觉得两张牌谁该排前面”的裁判。它接收两个元素a和b返回负数表示a应该排在前面返回正数表示b应该排在前面返回 0 表示两者并列。这个裁判逻辑是排序的灵魂因为算法只负责“按裁判的判断结果去交换位置”至于“什么叫排在前面”完全由你定义。稳定性这个概念也要知道。一个稳定的排序算法在比较结果相等时会保持元素原本的相对顺序。比如我先按“添加时间”排好再按“歌手名”排如果第二次用的不是稳定排序那么同一个歌手下面的歌可能会乱掉如果用的是归并排序这种稳定算法它们会继续保持“按添加时间排好的顺序”。Anysort 默认的排序行为是稳定的这点在实际多级排序时价值很大。2. 准备数据把歌单变成可排序的结构化数据2.1 怎么把歌单变成 JSON 数据流排序库只认结构化数据不认网易云里那个花花绿绿的界面。所以第一步是把歌单里的歌曲信息整理成统一的 JSON 数组。这一步有好几种方式我按推荐程度排个序。最推荐的是“网页端控制台抓取数据”。打开网易云音乐网页版进入你的歌单页按 F12 打开开发者工具切到 Network网络面板刷新页面然后在筛选器里选 XHR 或者 Fetch你会看到很多接口请求。找名字里带playlist、detail之类的响应点开后如果返回的是 JSON里面一般就有tracks字段这就是当前歌单的歌曲列表。通常每首歌会包含name歌名、ar或artists歌手、al专辑、dt时长单位一般是毫秒、playCount播放量之类字段。右键复制响应内容保存成playlist.json数据就拿到了。第二种方式是手动整理。如果歌单不太长或者你只需要少数几个字段直接在表格软件里维护一个 CSV歌名、歌手、时长、收藏时间、备注想加什么维度加什么维度然后转成 JSON 就行。这个方法土但胜在稳定可靠不会受接口格式影响。第三种方式是用第三方开源项目比如 NeteaseCloudMusicApi 这类非官方接口包。但这里我要专门提醒一句非官方接口随时可能失效而且大规模调用存在账号风控风险。我的建议是如果只是做个个人排序工具用第一种方式拿一次数据就够了完全没有必要搭一个常驻服务去反复请求。处理对象就是你自己歌单里能看到的数据这本身没问题但不要去做任何绕过平台规则的事。2.2 字段清洗与类型统一拿到原始 JSON 之后不能直接排序因为接口返回的字段类型很乱。dt可能是毫秒数也可能是字符串playCount可能为空addedAt可能是标准的 ISO 时间字符串也可能是个时间戳字符串歌名和歌手名里还经常混着全角空格、换行符。这些不处理干净排序结果会非常诡异。我写了一个简单的清洗函数把每首歌统一成自己定义的内部结构function normalizeTrack(raw) { return { name: String(raw.name || ).trim(), artist: String(raw.artist || raw.ar?.[0]?.name || 未知歌手).trim(), album: String(raw.album || raw.al?.name || ).trim(), duration: Number(raw.duration || raw.dt || 0), // 统一转成毫秒 heat: Number(raw.heat || raw.playCount || 0), // 播放次数缺失时当 0 addedAt: new Date(raw.addedAt || raw.addTime || Date.now()).getTime() }; }这里有个小陷阱duration在网易云接口里通常是毫秒但你自己整理的 CSV 里很可能习惯用“分:秒”格式。两种情况必须要统一我建议内部一律存毫秒展示时再换算成“分钟:秒”避免排序时出现“3:45 4:12”这种因为字符串比较导致的奇怪结果。清洗完字段后把整个数组过滤一遍去掉明显的脏数据没有歌名的、时长小于 1000 毫秒的、重复项。然后用JSON.stringify导出一份clean-playlist.json后面所有的排序实验都在这个干净数据上进行。2.3 安装 Anysort 并做一次冒烟测试Anysort 是一个 npm 包项目根目录下执行npm init -y npm install anysort如果你不想搭 Node 环境也可以在 HTML 页面里用 CDN 方式加载script srchttps://unpkg.com/anysort/script不管是哪种方式装完先做一个冒烟测试确认库能正常工作const anysort require(anysort); const test [3, 1, 4, 1, 5, 9, 2, 6]; const sorted anysort(test); console.log(sorted); // [1, 1, 2, 3, 4, 5, 6, 9] console.log(test); // [3, 1, 4, 1, 5, 9, 2, 6] 原数组不变如果终端能打印出排好序的数组且原数组没变说明环境没问题了。接下来正式干活。3. 用 Anysort 实现各种想要的排序3.1 基础排序按添加时间、时长、热度最基础的需求就是“按添加时间从新到旧排”。这个直接用 Anysort 的比较函数就能实现const playlist require(./clean-playlist.json); const byAddedTimeDesc anysort(playlist, { comparator: (a, b) b.addedAt - a.addedAt }); console.log(byAddedTimeDesc.slice(0, 5).map(t ${t.name} - ${new Date(t.addedAt).toLocaleDateString()}));比较函数里b.addedAt - a.addedAt返回正数说明b的添加时间更晚应当排在前面也就是降序。反过来a.addedAt - b.addedAt就是升序老歌在前。按时长排序也很简单。但这种排序的实用性在于“筛选场景”比如我想找一首 3 分钟左右、适合通勤路上听的歌可以先按时长升序排然后肉眼扫前几十首按播放热度排序则可以帮助我快速看到这个歌单里最被大众认可的 Top 10。有两个细节值得说。一是时长字段如果你只存了秒数排序没问题但展示时最好格式化成“4:32”这种格式不然满屏数字会看吐。二是 heat 字段可能出现极端值比如一首歌播放量过亿另一首只有几千在排序场景下不影响结果但如果你后面要做“自定义权重”和“归一化缩放”这个数量级差异会直接把小数据淹没后面我会详细讲。3.2 中文排序按歌名和歌手排中文字符串排序在国内应用里是个永远的坎。很多新手直接写anysort(playlist, { comparator: (a, b) a.name b.name ? 1 : -1 });这个写法在英文歌名上勉强能用但中文歌名基本就是乱序因为它比较的是 Unicode 编码不是拼音也不是汉字笔画。正确做法是使用 JavaScript 内置的localeCompare方法const byNameAsc anysort(playlist, { comparator: (a, b) a.name.localeCompare(b.name, zh-Hans-CN) });localeCompare会让 JavaScript 引擎按区域设置locale的规则来比较字符串。这里传zh-Hans-CN也就是简体中文大多数现代浏览器和 Node 版本会按拼音顺序排列。这个参数很关键因为不传参数时不同环境的默认行为差异很大有的环境按拼音有的环境按 Unicode还有的会受操作系统语言影响。按歌手名排同理const byArtistAsc anysort(playlist, { comparator: (a, b) a.artist.localeCompare(b.artist, zh-Hans-CN) });这里有个体验优化同一个歌手有多首歌的时候我不想它们之间完全按歌名拼音排而是希望保留“我添加进歌单的时间顺序”。这就需要稳定排序 多级排序才能实现也正是下一节要讲的。3.3 多级排序先按歌手再按添加时间多级排序是所有排序方案里最实用的一个。核心思路不复杂比较函数先比较第一关键字如果不相等就直接返回结果如果相等再比较第二关键字以此类推。const byArtistThenTime anysort(playlist, { comparator: (a, b) { const artistCompare a.artist.localeCompare(b.artist, zh-Hans-CN); if (artistCompare ! 0) return artistCompare; return b.addedAt - a.addedAt; // 同一歌手下新收藏的在前 } });这个写法非常直白先看歌手名是否相同不同就以歌手的拼音排序相同再看添加时间时间晚的靠前。因为 Anysort 默认是稳定排序即便你选择的是非稳定算法只要你指定了稳定的算法或者使用默认设置同一歌手下的相对顺序也会保持在上一级排好的顺序。同样的思路可以无限扩展。比如“先按发行年份再按专辑名再按曲目序号”等于完全自定义了一个专辑浏览视图。这个可比网易云默认的排序好用太多了。3.4 自定义权重排序把“爱听程度”量化到这一步常规需求基本已经满足了但 Anysort 的真正魅力在于“完全自定义比较规则”。我有一个场景想给自己排一个“最近最想听的歌”歌单但又不想只用热度排序——因为热门歌曲不一定是我喜欢的也不希望只用添加时间排序——因为旧歌也有宝藏。我给每首歌设计了一个综合得分规则很简单播放热度取对数再乘以一个权重加上一个“最近收藏”的加成再减掉一个“听过太多次”的惩罚系数。写成函数就是function scoreTrack(track, now Date.now()) { const heatScore Math.log10(track.heat 1); const recencyDays (now - track.addedAt) / (1000 * 60 * 60 * 24); const recencyScore Math.max(0, 30 - recencyDays / 10); const overplayPenalty Math.min(20, track.playCountInMyAccount * 2 || 0); return heatScore * 2 recencyScore - overplayPenalty; } const dailyMix anysort(playlist, { comparator: (a, b) scoreTrack(b) - scoreTrack(a) });这个评分公式完全可以按个人喜好调整我举这个例子想说明的是两件事。第一热度必须做缩放。如果直接用播放量做分数一万播放和一百万播放差一百倍而收藏时间这个维度的贡献几乎可以忽略。取对数之后1000 播放是 3100 万播放是 6差距从“千倍”变成了“一倍”这样收藏时间的权重才能真正参与进来。这就是我在前面提到“数量级差异会被忽略”的原因。第二比较函数不一定非要基于字段本身它可以是一个计算值。Anysort 的comparator完全由你控制你甚至可以这样用点击按钮给某首歌加一分实时重排整个实现速度非常快。我后来把这个逻辑做成了一个本地小页面每天打开就是一份“今天想听什么”的排序结果体验相当舒服。4. 排序结果的展示与回写4.1 用控制台验证排序结果是否真的正确排序代码写完别急着把结果拿去建歌单先做一步结果校验。最忌讳的是排完序后只看一眼前几首就认为没问题后面可能藏着大量顺序错误的歌。我习惯写一个小的检查函数function assertSorted(arr, compareFn) { for (let i 1; i arr.length; i) { if (compareFn(arr[i - 1], arr[i]) 0) { console.warn(排序异常第 ${i - 1} 项和第 ${i} 项顺序不对); console.warn(arr[i - 1]); console.warn(arr[i]); return false; } } console.log(排序校验通过); return true; }然后抽样打印开头、中间、结尾各 10 首肉眼确认一下是否有明显不合理的排列。特别是中文排序和多级排序一定要抽一个“同一歌手有多首歌”的片段出来看确认第二关键字是否生效。这个校验函数在你修改比较逻辑时非常有用。你会发现有时候你觉得自己写的逻辑是对的但实际执行时因为字段里有空值导致歌手名比较时localeCompare报错或者返回奇怪的结果校验函数能最快暴露问题。4.2 做一个本地页面可视化对比控制台打印终归不够直观我建议做一个简单的 HTML 页面来展示排序前后的对比。这个页面不需要任何后端直接用 CDN 引入 Anysort 和你的 JSON 数据全部在浏览器本地运行。核心代码大概长这样input typefile idupload accept.json / button onclicksortByArtist()按歌手排序/button button onclicksortByWeight()按自定义权重排序/button table idresult/table script srchttps://unpkg.com/anysort/script script let playlist []; document.getElementById(upload).addEventListener(change, async (e) { const file e.target.files[0]; const text await file.text(); playlist JSON.parse(text); renderTable(playlist); }); function renderTable(data) { const tbody data.slice(0, 50).map(track tr td${track.name}/td td${track.artist}/td td${formatDuration(track.duration)}/td /tr ).join(); document.getElementById(result).innerHTML trth歌名/thth歌手/thth时长/th/tr${tbody}; } function formatDuration(ms) { const totalSeconds Math.floor(ms / 1000); const minutes Math.floor(totalSeconds / 60); const seconds totalSeconds % 60; return ${minutes}:${String(seconds).padStart(2, 0)}; } function sortByArtist() { const sorted anysort(playlist, { comparator: (a, b) a.artist.localeCompare(b.artist, zh-Hans-CN) }); renderTable(sorted); } /script这个页面的好处是“所见即所得”而且不会动原数据。你可以一边调整比较函数一边刷新页面对比效果比在终端里反复 console.log 舒服得多。我建议把不同的排序规则做成一排按钮直接拿真实的歌单数据试很快就能找到最适合自己的那个规则。4.3 把排序结果应用回网易云歌单排序结果满意之后最关心的问题就是怎么让网易云歌单真正变成这个顺序这里要坦白说网易云并没有提供一个公开的、便捷的“按自定义顺序重排歌单”的 API。网页端支持拖动排序但一次拖一首不适合大范围重排手机端也有置顶、移动功能同样不适合批量操作。我试过几种思路最终一个比较稳妥的做法是按排序结果生成一个新歌单名称类似“我的DJ排序 2025-07-01”按排序后的顺序从第一首开始把歌逐首加入新歌单加入成功后原歌单保留不动日常听歌直接用新歌单。这个操作可以手动做也可以写脚本模拟点击来做。但我要强烈建议不要为了效率去高频调用非官方接口批量操作账号很容易触发风控轻则操作失败重则限制歌单功能。我的经验是如果你的歌单有几百首就花点时间手动添加一次如果歌单上千首分批建几个子歌单比硬塞一个超长歌单更合理。这个“重排”方案虽然不能直接在网易云内部实现一键自动化但思路本身没问题排序逻辑在本地完成结果再用于手动回写整个过程完全可控。至少我不需要再去网易云里面一首一首拖了排序结果已经明明白白打印在表格里照着顺序添加就行。5. 实战中避坑与常见问题5.1 中文排序总是排不对怎么办中文排序排不对九成是localeCompare的 locale 参数问题。有些环境默认不传参数也能按拼音排有些环境则不行所以请务必显式传zh-Hans-CN或者zh。如果你发现某些汉字顺序还是不对比如多音字“重”被排到了奇怪的位置这是 JS 内置拼音排序的局限目前没有特别完美的解决方案但整体上不影响使用。还有一类情况是歌手名里带着额外字符比如“周杰伦 ”末尾有空格和“周杰伦”在localeCompare眼里不是同一个字符串。清洗阶段一定要做trim()把首尾空格、全角空格、不可见字符全部去掉。5.2 异步获取数据导致排序错乱我在最早做这个项目时踩过一个大坑从不同接口并发拉取歌曲数据然后直接在回调里排序结果排序经常乱。排查下来发现问题不在 Anysort而在于我排序的数组只是一个部分数据其他请求还没返回导致排序结果看似“被算法弄乱了”。正确做法是先把所有数据请求用Promise.all等回来合并完、清洗完之后再喂给 Anysort。如果数据量很大还可以加一个简单的时间戳缓存比如一小时之内不重新请求接口直接用本地缓存的 JSON。排序是个同步操作它不关心数据来源但它要求你给它一个完整、一致的数据快照。5.3 字段类型不一致引发的排序异常清洗阶段漏掉字段排序时就会出现各种诡异现象。最常见的是heat字段有的歌是数字有的是字符串。JavaScript 的比较函数a.heat - b.heat会把字符串自动转成数字但如果某个值是undefined或者null相减的结果就是NaN任何和NaN比较的结果都是false排序算法会认为“两者相等”顺序就乱了。解决方法是在清洗函数里做兜底所有参与比较的字段都要保证是数字或字符串。数字字段用Number(value || 0)字符串字段用String(value || ).trim()。排序之前再跑一遍校验函数确保没有 NaN 混进去。5.4 算法选错导致性能翻车普通歌单几百首曲子用任何排序算法都感觉不出差别。但如果你想用这套思路去排本地音乐库比如几千上万首文件算法选择就成了一个需要考虑的问题。Anysort 允许你指定算法默认情况下它会选择一种适合数据规模的策略。我的建议是数据量小几千以下用默认就行如果你需要稳定排序、而且数据量很大显式指定归并排序如果数据已经接近有序插入排序反而比快速排序更快。反过来不要用冒泡排序和选择排序去处理大数据集平方级复杂度在大数组上会卡到怀疑人生。算法平均时间复杂度稳定性数据量小数据量很大快速排序O(n log n)不稳定好好归并排序O(n log n)稳定好好插入排序O(n²)稳定极好差堆排序O(n log n)不稳定一般较好冒泡排序O(n²)稳定一般极差还有一个使用 Anysort 时比较容易忽略的点如果你需要“原数组位置也改变”默认返回新数组的行为反而更适合因为你可以保留原始顺序作为“撤销”的手段。我强烈建议不要把排序结果直接覆盖原始数据万一新规则不满意想恢复原顺序就麻烦了。5.5 常见问题速查表问题现象原因解决办法中文歌名顺序乱没有传 locale 参数localeCompare(b, zh-Hans-CN)歌手名带空格排不齐原始数据没清洗干净.trim() 去全角空格heat 为 null 导致排序全乱比较结果出现 NaN清洗时 Number(value异步拉数据后排序结果不稳定数据没等全就排序Promise.all合并后再排排序后原数组被改用了会原地排序的方法Anysort 默认不改原数组确认没有赋值覆盖数据量很大排序卡顿用了平方级算法指定归并排序或快速排序最后再分享两个小技巧第一个技巧是关于“随机重排”的。Anysort 虽然主打各种排序算法但你也可以用比较函数实现“洗牌”的效果comparator: () Math.random() - 0.5。这样每次排序结果都不同。我给自己做了一个“犹豫听什么”按钮在本地页面上点一下歌单就会随机换一种顺序比网易云自带的随机播放更有“可控随机”的感觉。第二个技巧是多级排序里第二关键字的稳定性。如果你先按歌名排再按歌手排最后得到的是“同一歌手下歌名按拼音排”的效果但如果你先按歌手排再按歌名排得到的就是“所有歌按歌名排同名歌再按歌手排”。这两种结果差别很大一定要想清楚自己需要哪种层级结构再决定比较函数的书写顺序。这个用 Anysort 给网易云歌单排序的项目前前后后折腾了大半个周末最让我满意的反而不是某个具体排序结果而是“排序规则完全由我定义”这件事。你不需要被产品经理预设好的那几个按钮绑住想怎么排就怎么排这大概就是自己动手的乐趣。
返回列表