使用 Anysort 排序库给网易云歌单排序

本文介绍了如何通过编写JavaScript脚本,实现网易云音乐歌单中歌曲的自定义排序功能。作者创建了一个名为Anysort的库,支持多属性排序、自定义排序逻辑以及多种排序规则,如按专辑名称、发布日期、歌曲时长等。Anysort库允许开发者以声明式的方式定义排序规则,如'album.name'、'-song.length'等,提高了排序的灵活性和便利性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

歌单排序

网易云音乐的歌单排序功能十分鸡肋,只能按照歌曲名、歌手名、专辑名排序。别说处理一些复杂的规则了,他连从 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 -1
  if (a > b) return 1
  else 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.length
  const 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.name
  const 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 firstRes
  else 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 = 0
    while(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 = x
    while (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 ? 
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值