来源丨公众号-法纳斯特:利用 Python 一键下载网易云音乐 10W+ 乐库 链接丨https://mp.weixin.qq.com/s/fl7gGE2dVLRiQkvrTe7-vw

如果你常听音乐的话,肯定绕不开网易云,作为一款有情怀的音乐 App,我对网易云也是喜爱有加。虽然说现在都已经是 5G 时代了,大家的手机流量都绰绰有余,但在线播放还是不如本地存着音乐文件靠谱,今天我们就用 Python 来一键下载网易云音乐乐库。

先来看下最终的效果。
利用 Python 一键下载网易云音乐 10W  乐库 - 图1
利用 Python 一键下载网易云音乐 10W  乐库 - 图2
其实下载音乐不难,只需要获取到音乐文件播放的地址就可以通过文件流读取的方式直接下载下来。那么问题就转化为如何获取音乐文件的播放地址了。

榜单分析

我们可以打开网易云排行榜 [https://music.163.com/#/discover/toplist?id=19723756](https://music.163.com/#/discover/toplist?id=19723756),仔细分析我们发现该网页左边一列全是排行榜,每个排行榜都对应这不同的排行榜 ID,具体 ID 是多少,直接打开开发者工具即可清晰的看到。
利用 Python 一键下载网易云音乐 10W  乐库 - 图3
由上图我们可以看到榜单是放在一个 class='f-cb'ul 列表里面的,所以只需要获取到该 ul 列表的 li 标签即可。而对于每一个 li 标签来说,其 data-res-id 属性则是榜单 id,而榜单名称则是属于该 li 标签下的 divclass='name'p 标签下的 a 标签的内容。因此我们获取到 li 标签的集合之后,遍历该集合依次取出榜单 id 和榜单名称即可。
于是我们有了下面的函数,获取所有的榜单,该函数返回值是一个字典,key 为 榜单 id,值为榜单名称。

  1. url = 'https://music.163.com/discover/toplist'
  2. hd = {
  3. 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36'
  4. }
  5. def get_topic_ids():
  6. r = requests.get(url, headers=hd)
  7. html = etree.HTML(r.text)
  8. nodes = html.xpath("//ul[@class='f-cb']/li")
  9. logger.info('{} {}'.format('榜单 ID', '榜单名称'))
  10. ans = dict()
  11. for node in nodes:
  12. id = node.xpath('./@data-res-id')[0]
  13. name = node.xpath("./div/p[@class='name']/a/text()")[0]
  14. ans[id] = name
  15. logger.info('{} {}'.format(id, name))
  16. return ans

歌曲分析

上面我们获取到了所有的榜单数据,那么针对单个榜单来说,就是要获取其下的所有歌曲了。
分析页面可知,歌曲列表是在一个 table 中的,但是通过 requests.get(url,headers=hd) 方式获取返回的网页文本内容的话,貌似是获取不到 table 元素的。于是我们将其返回值输出后做了仔细分析,发现歌曲是在 class="f-hide"ul 标签中。与获取榜单类似,同样需要先获取所有的 li 标签,然后在逐个获取歌曲 id 和歌曲 name 就可以了。

  1. def get_topic_songs(topic_id, topic_name):
  2. params = {
  3. 'id': topic_id
  4. }
  5. r = requests.get(url, params=params, headers=hd)
  6. html = etree.HTML(r.text)
  7. nodes = html.xpath("//ul[@class='f-hide']/li")
  8. ans = dict()
  9. logger.info('{} 榜单 {} 共有歌曲 {} 首 {}'.format('*' * 10, topic_name, len(nodes), '*' * 10))
  10. for node in nodes:
  11. id = node.xpath('./a/@href')[0].split('=')[1]
  12. name = node.xpath('./a/text()')[0]
  13. ans[id] = name
  14. logger.info('{} {}'.format(id, name))
  15. return ans

同样该函数返回一个字典,key 为歌曲 id,value 为歌曲名称。

下载歌曲

我们还需要一个下载歌曲的函数,该函数接收歌曲 id,然后以文件流的形式直接读取到本地。

  1. def down_song_by_song_id_name(id, name):
  2. if not os.path.exists(download_dir):
  3. os.mkdir(download_dir)
  4. url = 'http://music.163.com/song/media/outer/url?id={}.mp3'
  5. r = requests.get(url.format(id), headers=hd)
  6. is_fail = False
  7. try:
  8. with open(download_dir + name + '.mp3', 'wb') as f:
  9. f.write(r.content)
  10. except:
  11. is_fail = True
  12. logger.info("%s 下载出错" % name)
  13. if (not is_fail):
  14. logger.info("%s 下载完成" % name)

最后将所有的操作组合到 main 函数中,作为程序的入口函数。

  1. def main():
  2. ids = get_topic_ids()
  3. while True:
  4. print('')
  5. logger.info('输入 Q 退出程序')
  6. logger.info('输入 A 下载全部榜单歌曲')
  7. logger.info('输入榜单 Id 下载当前榜单歌曲')
  8. id = input('请输入:')
  9. if str(id) == 'Q':
  10. break
  11. elif str(id) == 'A':
  12. for id in ids:
  13. down_song_by_topic_id(id, ids[id])
  14. else:
  15. print('')
  16. ans = get_topic_songs(id, ids[id])
  17. print('')
  18. logger.info('输入 Q 退出程序')
  19. logger.info('输入 A 下载全部歌曲')
  20. logger.info('输入歌曲 Id 下载当前歌曲')
  21. id = input('请输入:')
  22. if str(id) == 'Q':
  23. break
  24. elif id == 'A':
  25. down_song_by_topic_id(id, ans[id])
  26. else:
  27. down_song_by_song_id_name(id, ans[id])
  28. if __name__ == "__main__":
  29. main()

总结

今天我们以网易云网页版为数据源来下载音乐文件,其中下载操作是最简单的,比较麻烦的是分析榜单 id 和获取榜单下的歌曲列表,但榜单下的歌曲列表其实远不止 10 条,而我们获取歌曲的函数 get_topic_songs 每次只可以获取 10 条歌曲,这是因为我们没有在 headers 添加 cookie 导致的,因为只有登录之后才会显示所有的歌曲。小伙伴们可以登录自己的账户然后添加 cookie 做下尝试。

完整代码:

  1. #!/usr/bin/env python
  2. # -*- encoding: utf-8 -*-
  3. """
  4. @File : music163.py
  5. @Modify Time @Author @Version @Description
  6. ------------ ------- -------- -----------
  7. 2021/5/11 08:48 SeafyLiang 1.0 爬取网易云榜单音乐
  8. """
  9. import requests
  10. from lxml import etree
  11. import os
  12. import time
  13. # 若需下载更多音乐,需登录,在hd中传入cookie
  14. url = 'https://music.163.com/discover/toplist'
  15. hd = {
  16. 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36'
  17. }
  18. def get_topic_ids():
  19. r = requests.get(url, headers=hd)
  20. html = etree.HTML(r.text)
  21. nodes = html.xpath("//ul[@class='f-cb']/li")
  22. print('{} {}'.format('榜单 ID', '榜单名称'))
  23. ans = dict()
  24. for node in nodes:
  25. id = node.xpath('./@data-res-id')[0]
  26. name = node.xpath("./div/p[@class='name']/a/text()")[0]
  27. ans[id] = name
  28. print('{} {}'.format(id, name))
  29. return ans
  30. def get_topic_songs(topic_id, topic_name):
  31. params = {
  32. 'id': topic_id
  33. }
  34. r = requests.get(url, params=params, headers=hd)
  35. html = etree.HTML(r.text)
  36. nodes = html.xpath("//ul[@class='f-hide']/li")
  37. ans = dict()
  38. print('{} 榜单 {} 共有歌曲 {} 首 {}'.format('*' * 10, topic_name, len(nodes), '*' * 10))
  39. for node in nodes:
  40. id = node.xpath('./a/@href')[0].split('=')[1]
  41. name = node.xpath('./a/text()')[0]
  42. ans[id] = name
  43. print('{} {}'.format(id, name))
  44. return ans
  45. def down_song_by_song_id_name(id, name, download_dir):
  46. if not os.path.exists(download_dir):
  47. os.mkdir(download_dir)
  48. url = 'http://music.163.com/song/media/outer/url?id={}.mp3'
  49. r = requests.get(url.format(id), headers=hd)
  50. is_fail = False
  51. try:
  52. with open(download_dir + name + '.mp3', 'wb') as f:
  53. f.write(r.content)
  54. except:
  55. is_fail = True
  56. print("%s 下载出错" % name)
  57. if (not is_fail):
  58. print("%s 下载完成" % name)
  59. def down_song_by_topic_id(topicId, topicName):
  60. download_dir = './data/%s' % topicName + '/'
  61. if not os.path.exists(download_dir):
  62. os.mkdir(download_dir)
  63. topicMusicList = get_topic_songs(topicId, topicName)
  64. for id in topicMusicList:
  65. name = topicMusicList[id]
  66. down_song_by_topic_id(id, name)
  67. url = 'http://music.163.com/song/media/outer/url?id={}.mp3'
  68. r = requests.get(url.format(id), headers=hd)
  69. is_fail = False
  70. try:
  71. with open(download_dir + name + '.mp3', 'wb') as f:
  72. f.write(r.content)
  73. except:
  74. is_fail = True
  75. print("%s 下载出错" % name)
  76. if not is_fail:
  77. print("%s 下载完成" % name)
  78. def main():
  79. ids = get_topic_ids()
  80. while True:
  81. print('')
  82. print('输入 Q 退出程序')
  83. print('输入 A 下载全部榜单歌曲')
  84. print('输入榜单 Id 下载当前榜单歌曲')
  85. id = input('请输入:')
  86. if str(id) == 'Q':
  87. break
  88. elif str(id) == 'A':
  89. for id in ids:
  90. down_song_by_topic_id(id, ids[id])
  91. else:
  92. print('')
  93. ans = get_topic_songs(id, ids[id])
  94. topicName = ids[id]
  95. print('')
  96. print('输入 Q 退出程序')
  97. print('输入 A 下载全部歌曲')
  98. print('输入歌曲 Id 下载当前歌曲')
  99. id = input('请输入:')
  100. if str(id) == 'Q':
  101. break
  102. elif id == 'A':
  103. for musicId in ans:
  104. down_song_by_song_id_name(musicId, ans[musicId], './data/%s-%s/' % (
  105. topicName, time.strftime("%Y-%m-%d", time.localtime())))
  106. else:
  107. down_song_by_song_id_name(id, ans[id], './data/')
  108. # 操作步骤:先输入榜单id,再输入A下载当前榜单全部歌曲
  109. if __name__ == "__main__":
  110. main()