歌单排序

网易云音乐的歌单排序功能十分鸡肋,只能按照歌曲名、歌手名、专辑名排序。别说处理一些复杂的规则了,他连从 Z-A 这种简单的倒序逻辑都搞不定。这对像我这种红星单动不动就几千首曲子或是喜欢给曲子分类、制作歌单的朋友来说简直就是灾难。

于是,我写了一个专门用来给歌单中的歌曲排序的爬虫脚本,能够实现各种有意思的排序功能。

比方说,这个歌单的歌曲排序按照以下规则排序:

  1. 按专辑名称排序
  2. 再按专辑发布日期排序。
  3. 再按歌曲时长排序。

再举个例子,这个歌单的曲子按照以下规则排序:

  1. 按专辑作者名称排序,由于专辑可能有多个作者,优先选择其中有歌手名为“CLOCKWORKS TRACER”的专辑。
  2. 再按专辑发布日期排序。
  3. 再按歌曲在对应专辑中的顺序排序。

哇哈,这个排序功能比网易云默认的歌单排序强哈,这才真有点意思~ 接下来,我们一步一步来把它实现吧。名称和标语我都想好了,就叫做 Anysort:灵活、优雅、无依赖的排序库

简单解决

确定问题

首先,得确定要解决什么问题。

在 JavaScript 中,排序其实是一个非常好写的功能。通过给 Array.prototype.sort 传入自定义排序方法,就能得到按期望顺序排好了的数组。

const nums = [1, 30, 4, 21, 100000]function customCompare(a, b) {if (a < b) return -1if (a > b) return 1else return 0
}nums.sort(customCompare)// 排序结果:[1, 4, 21, 30, 100000]

可以看到,给 Array.prototype.sort 传入的排序方法 customCompare,最终的返回值要么是 0,要么是 1 或 -1,这个返回值直接的指代了 a 和 b 的排序顺序。也就是说,无需关心 Array.prototype.sort 的具体实现到底是快速排序还是冒泡排序,只需要写一个比较两个值的大小的函数,返回它们的顺序到底是相等(0),还是 a 比 b 靠前(-1),或是 a 在 b 之后(1)就行了。

我们通常会使用 JSON 对象去描述一首曲子,比如《海阔天空》可能描述如下:

const song = {name: '海阔天空',authro: 'Beyond',// 专辑信息album: {name: '海阔天空',time: '1993-09-09',brief: '《海阔天空》是黄家驹为BEYOND成立十周年而作的,刻画着他们十年来的心路历程。'},// 评论信息comments: [{name: '冰凍七音',content: '有些人死了,他还活着',approve: '150283',hot: true,},{name: '独坐凭栏',content: '钢铁锅,含着泪喊修瓢锅。。。',approve: '77739'}]
}

给歌曲排序可要比给数字排序复杂。两个数字之间固然只有大于小于等于三种关系,但是同一首专辑的歌曲之间,也许我们还想继续按歌曲名、歌曲时间或其它属性排序。讲到这里,写排序函数时碰到的第一个问题就出来了:如何给多属性进行排序?

简单方案

先简单来写几个基础排序函数,尝试解决以下问题:

  1. 先按曲子的专辑名称的长度排序。
  2. 再按曲子的名称的字典顺序排序。
function sortByAlbumNameLen(songA, songB) {const lenA = songA.album.name.lengthconst lenB = songB.album.name.length// 专辑名称相同,则不改变顺序(0),名称不相同时,则按照专辑名称的长度由短到长排列。return lenA === lenB ? 0 : (lenA < lenB ? -1 : 1)
}
const songs = [{ name: 'songB', album: { name: 'wow' } },{ name: 'songC', album: { name: 'other' } },{ name: 'songA', album: { name: 'wow' } },
]
songs.sort(sortByAlbumNameLen)
// 得到结果:songB、songA、songC

然后再来处理专辑名称相同的情况。

function sortBySongName(songA, songB) {const nameA = songA.nameconst nameB = songB.name// 根据曲子的名称的字典顺序排序,如 'abort' 排在 'bank' 前面。return nameA === nameB ? 0 : (nameA < nameB ? -1 : 1)
}

由于 Array.prototype.sort 方法只接受一个函数,所以,需要把 sortByAlbumNameLen 和 sortBySongName 两个函数合二为一。

function sortByAlbumLenThenSongName(songA, songB) {const firstRes = sortByAlbumNameLen(songA, songB)// 先以 sortByAlbumNameLen 的结果为准,// 如果 sortByAlbumNameLen 没能得出顺序,// 返回了 0,// 那么继续排序,以 sortBySongName 的结果为准if (firstRes !== 0) return firstReselse return sortBySongName(songA, songB)
}
const songs = [{ name: 'songB', album: { name: 'wow' } },{ name: 'songC', album: { name: 'other' } },{ name: 'songA', album: { name: 'wow' } },
]
songs.sort(sortByAlbumNameLen)
// 得到结果:
// [
//   { name: 'songA', album: { name: 'wow' } },
//   { name: 'songB', album: { name: 'wow' } },
//   { name: 'songC', album: { name: 'other' } },
// ]

搞定。目前为止,先按曲子的专辑名称的长度排序,再按曲子的名称的字典顺序排序这个小功能就写好了。

问题抽象

有一个问题比较烦人,就是 Array.prototype.sort 只接受一个参数用作排序函数,还不能是数组。所以只要更改了排序的规则,那么传进去的参数(即上面的 sortByAlbumLenThenSongName)的逻辑就得跟着改。

这个小麻烦可以通过抽象解决。仔细观察,sortByAlbumLenThenSongName 的逻辑是:先按 sortByAlbumNameLen 的规则排序,如果 sortByAlbumNameLen 没能给出现后顺序(即返回 0),那么再按 sortBySongName 的顺序排序。这种现后顺序,不就是一个循环嘛!

function sortBy(...sortFns) {return function fn(obj1, obj2) {var fnsLen = sortFns.length,result = 0,i = 0while(result === 0 && i < fnsLen) {result = sort(sortFns[i], map)(obj1, obj2)i++}return result}
}
songs.sort(sortBy(sortByAlbumNameLen, sortBySongName
))

以后,就算我们继续更改排序规则,也不需要写新的函数去重构传入 Array.prototype.sort 的那个方法。假设替换一条排序规则:

  1. 先按曲子的专辑名称的长度排序。
  2. 再按曲子的名称的字典顺序排序。
  3. 再按曲子的时长排序。

只需要改动以下几行代码:

// ...
function sortBySongTime(a, b) {return a.time === b.time ? 0 : (a.time < b.time ? -1 : 1)
}
// ...
songs.sort(sortBy(sortByAlbumNameLen,sortBySongTime
))

妙哉妙哉~

逐步改进

上一小节,我们简单完成了一个具有多属性排序功能。接下来需进一步完善这个排序函数,使其功能完整,更加强大。

API 优化

程序员都是懒惰的。其实我甚至连基础的排序函数都不想写,要是能直接传入待排序的属性名那多好哇:

const songs = [{ name: 'songB', album: { name: 'wow' } },{ name: 'songC', album: { name: 'other' } },{ name: 'songA', album: { name: 'wow' } },
]
// 先按专辑名称、再按曲子名正序排列
songs.sort(sortBy('album.name', 'name')
)
// Results:songA、songB、songC

也就是说,需要写一个读取对象的属性值的方法。既然是涉及属性存取符号(也就是点运算符),那我们就用 eval(‘song.album.name’) 就能读到属性啦。咳咳,划掉,动态语言不意味着就要用 eval 解决问题。当然了,new Function 也不行。以下继续写一个 while 循环吧。

const getVal = name => {const pathsStore = name.split('.')return x => {const paths = [...pathsStore]let val = xwhile (val && paths.length) {next = paths.shift()val = val[next]}return val}
}
getVal('b.c')({ b: { c: 'test' } }
)
// Results:'test'

再接下来只要重构 sortBy 函数,把 getVal 的逻辑添加进去,就完成传入字符串属性的改造啦。

自定义排序函数

现在可以传入字符串了,我们再加一种 API:自定义排序函数,以支持某些特殊排序逻辑。比如以下代码:

songs.sort(sortBy(// 优先选择 2020 年发布的曲子,并按时间倒序排列(a, b) => {const year2019 = +new Date('2019-12-31 23:59:59')const year2020 = +new Date('2020-12-31 23:59:59')const timeA = Math.min(year2019, Math.max(a.pubTime, year2020))const timeB = Math.min(year2019, Math.max(b.pubTime, year2020))return timeA === timeB ? 0 : ( timeA < timeB ? 1 : -1)},// 再按专辑名称排序'album.name',
))

这个改造其实并不难,判断一下传入参数的类型就好了。

// 由于 sortBy 函数返回一个排序函数,
// 所以在这儿改名为 factory(函数工厂)
function factory(...cmd) {// 如果没有排序参数,那么返回 undefined,// 告诉 Array.prototype.sort 不需要排序if (cmd.length < 1) return undefined// 兼容字符串或自定义函数const sortFns = cmd.map((x, i) => x === 'function' ? x : generate(x))return (obj1, obj2) => {var fnsLen = sortFns.length,result = 0,i = 0while(result === 0 && i < fnsLen) {result = sort(sortFns[i], map)(obj1, obj2)i++}return result}
}// 从字符串指令(如 album.name)中得到排序函数
function generate(s) {// 指定排序是正序还是倒序,稍后会用到const sortOrder = 1return function fn(a,b) {var resultvar am = getVal(s)(a)var bm = getVal(s)(b)if (am === bm) result = 0if (am < bm) result = -1if (am > bm) result = 1return result * sortOrder}
}

增加倒序逻辑

在 generate 函数里,我们还遗留了一个字段 sortOrder 用来指定排序是正序还是倒序。当然,如果传入自定义排序函数,根本不会走 generate,只有字符串参数,才会走到这一步。这里可以改进一下 API,如传入 ‘-album.name’,前面加了一个负号,用来代表按倒序排序。

function generate(s) {const sortOrder = 1// 如果第一个字符为负号,则将排序结果取反if (s[0] === '-') {sortOrder = -1s = s.substr(1)}return function fn(a,b) {var resultvar am = getVal(s)(a)var bm = getVal(s)(b)if (am === bm) result = 0if (am < bm) result = -1if (am > bm) result = 1return result * sortOrder}
}

奈斯哇,经过这两次迭代,终于支持了自定义排序函数,也支持通过负号得到倒序的逻辑。

不过,这可远远没有结束,第三小节才是“真正的 Coding”。

层层抽象

坐稳啦,

使用 Anysort 排序库给网易云歌单排序相关推荐

  1. Python爬虫-selenium爬取网易云歌单

    文章目录 (一)工欲善其事必先利其器-安装工具 Selenium 浏览器 (二)实战 (一)工欲善其事必先利其器-安装工具 Selenium Selenium是一个强大的网络数据采集工具,其最初是为网 ...

  2. Python爬虫之网易云歌单音频爬取(解决urlretrieve爬取文件不能播放问题)

    网易云歌单音频爬取 写在前面:最近学习爬虫,对小说和图片都进行过简单爬取,所以打算爬取音频,但是其中遇到点问题也解决了,写下博客记录并希望对大家也有帮助. 爬取对象:对于目前主流的几个音频播放网站,我 ...

  3. [数据分析笔记] 网易云歌单分析系列02—pyecharts柱状图

    文章目录 0.前言 1.百分比堆叠柱状图 1.1 导入包,连接数据库 1.2 查看数据 1.3 数据预处理 1.4 生成图表 2.复合柱状图和折线图 2.1 数据预处理 2.2 生成图表 3.竖直缩放 ...

  4. Python爬虫实战: 爬取网易云歌单

    这篇文章,我们就来讲讲怎样爬取网易云歌单,并将歌单按播放量进行排序,下面先上效果图 1.用 requests 爬取网易云歌单 打开 网易云音乐 歌单首页,不难发现这是一个静态网页,而且格式很有规律,爬 ...

  5. 爬虫python代码网易云_爬虫实战(二) 用Python爬取网易云歌单

    最近,博主喜欢上了听歌,但是又苦于找不到好音乐,于是就打算到网易云的歌单中逛逛 本着 "用技术改变生活" 的想法,于是便想着写一个爬虫爬取网易云的歌单,并按播放量自动进行排序 这篇 ...

  6. python爬虫实例网易云-爬虫实战(二) 用Python爬取网易云歌单

    最近,博主喜欢上了听歌,但是又苦于找不到好音乐,于是就打算到网易云的歌单中逛逛 本着 "用技术改变生活" 的想法,于是便想着写一个爬虫爬取网易云的歌单,并按播放量自动进行排序 这篇 ...

  7. 爬虫实战(二) 用Python爬取网易云歌单

    最近,博主喜欢上了听歌,但是又苦于找不到好音乐,于是就打算到网易云的歌单中逛逛 本着 "用技术改变生活" 的想法,于是便想着写一个爬虫爬取网易云的歌单,并按播放量自动进行排序 这篇 ...

  8. Python爬虫爬取豆瓣TOP250和网易云歌单

    python爬虫(网易云)笔记 @(python学习) 先推荐看一下b站的视频链接如下:https://www.bilibili.com/video/BV12E411A7ZQ?from=search& ...

  9. iTunes音乐导入过程记录(专辑图片、歌手修改,iTunes使用,网易云歌单批量下载)

    所需工具 1.iTunes 2.音乐标签修改器. 3.下载好的音乐 网易云歌单在线下载. 4.一点耐心 详细步骤 1.先调整一下iTunes中的音乐存储位置 菜单栏选编辑-偏好设置-高级 更改iTun ...

  10. 网易云歌单重合率2.0

    前言: 之前写过一个小工具输入网易云音乐上的昵称,即可查看两人喜欢的音乐中,有哪些是相同的,重合率有多少. 感兴趣的可以看这里:网易云歌单重合率1.0 但是之前的版本存在几个问题: 速度慢,这个其实是 ...

最新文章

  1. linux redis 3.0.7,linux虚拟机上安装配置redis3.0.7
  2. Psych101(part2)--Day2
  3. SAP BTP 应用 mta.yaml 里的 sap-btp-project1-dest-content module
  4. linux定位哪个进程出发重启,定位Linux下定位进程被谁KILL
  5. Ajax Login Sample
  6. 我们一家三口不和双方父母来往了怎么办?
  7. 2020年CSDN最后一波上车机会,快来~~
  8. 基于matlab的谱估计,基于MATLAB的功率谱估计实验.doc
  9. 博文视点大讲堂20期:Windows 7来了——知道你所不知道的Windows 7
  10. OC中iO操作相关方法
  11. Jenkins+Spring Boot构建部署
  12. MySQL 约束语法
  13. linux下WMB通过ODBC连接数据库
  14. Python运算(五)统计statistic模块
  15. chrome浏览器崩溃,设置也崩溃的解决办法
  16. [ NeurIPS 2020 ] 一叶知秋 —— 基于“单目标域样本”的领域自适应方法
  17. 云端服务器跑python代码,断开后台运行
  18. [已解决]“TypeError: Cannot read property ‘xxx‘ of undefined“报错情况分析
  19. spec cpu 2017使用教程
  20. 清华大学赵明国:AI芯片 +机器人,突破算法瓶颈

热门文章

  1. 02-----音频通道数、采样频率、采样位数、采样个数(样本数)的概念及计算一帧音频的大小、每秒播放的音频字节大小、一帧的播放时长、音频重采样
  2. z反变换计算机控制,计算机控制4.Z变换.ppt
  3. ubuntu 12.04下Trackpoint 小红点灵敏度和速度调整
  4. 化工、食品外贸行业管理难点
  5. 下列哪个网站还未推出微博服务器,新浪微博笔试题与答案
  6. python数据分析房价预测_Kaggle入门级赛题:房价预测——数据分析篇
  7. WORKBENCHSCDM导入CAD文件报错?
  8. 步步为赢,做好数据分析的7个步骤
  9. 再抱一抱DataStore
  10. 如何使用SpanReporter接口生成链路数据