一、 前言

先说结论,目前无法下载无损音乐,也无法下载vip音乐。
此代码模拟web网页js加密的过程,向api接口发送参数并获取数据,仅供参考学习,如果需要下载网易云音乐,不如直接在客户端下载,客户端还可以下载无损音乐。
代码还是半成品,打算再做个音乐播放器,直接打包成exe,等有时间做好了再传到github上去,现在先把解析过程记录下来发布。
至于音乐搜索器,我所知道的有一个,地址:https://iw233.cn/music/
在这里插入图片描述
上面这个网页直接返回所有的搜索结果和音乐信息,如要使用,请自行解析(很简单的一个页面)。
网上流传的网易云音乐外链地址:http://music.163.com/song/media/outer/url?id=534544522.mp3,我也不知道怎么来的,输入音乐id即可获得下载链接,本着学习的态度,我认为还是从头到尾解析下载链接才好,因此不考虑使用该外链。
接口文档已发布
链接: https://25ukpfkme3.apifox.cn 访问密码: qtlXaZPH

二、解析过程

来到网易云音乐首页,输入音乐名称,得到搜索结果
搜索结果
按F12,打开开发者工具,重新刷新一下界面,点击网络,在筛选器里只筛选XHR和Fetch数据,点击预览,一个一个链接往下找,直到找到我们需要的数据为止。
返回数据
得到音乐搜索的api接口:https://music.163.com/weapi/cloudsearch/get/web?csrf_token=
api

csrf_token只有你登录时才有,有没有这个值不影响返回的结果。
需要注意,这个api接口的POST请求,后面所有的api接口都是POST请求

点击负载,查看我们需要传入哪些数据
参数
可以看到我们需要传入params和encSecKey两个参数,才能获取数据,否则得到的数据为空。
那么如何获取这两个参数呢?点击发起程序,随便点击一个,进入js脚本界面
发起程序
点下面的{},将js代码格式化,这样我们更好查看代码
js
在js页面里按ctrl+f,直接搜索encSecKey,总共有三个结果,encSecKey
在这里插入图片描述

可以看到,在执行window.asrsea()函数后,生成了params和encSecKey,
这里我们贴一下js源码,后面还会用到

  1. var bMr5w = window.asrsea(JSON.stringify(i8a), bsg1x(["流泪", "强"]), bsg1x(TH5M.md), bsg1x(["爱心", "女孩", "惊恐", "大笑"]));
  2. e8e.data = j8b.cr9i({
  3. params: bMr5w.encText,
  4. encSecKey: bMr5w.encSecKey
  5. })

那么window.asrsea()是什么呢,搜索asrsea,可以看到window.asrsea()=d
asrsea
找到d函数,就在window.asrsea()上面,d
把js代码复制出来

  1. function d(d, e, f, g) {
  2. var h = {}
  3. , i = a(16);
  4. return h.encText = b(d, g),
  5. h.encText = b(h.encText, i),
  6. h.encSecKey = c(i, e, f),
  7. h
  8. }

其共涉及到三个函数a,b,c。我们先不急研究这三个函数的作用,先查看d函数传进去的四个参数d,e,f,g是什么。
其实从前面的window.asrsea()函数里我们就知道,这四个参数分别为JSON.stringify(i8a), bsg1x([“流泪”, “强”]), bsg1x(TH5M.md), bsg1x([“爱心”, “女孩”, “惊恐”, “大笑”]),
在这行打一个断点,重新刷新一下界面,进入调试模式。调试
在控制台依次输入这四个参数,获得其值
控制台
点击执行,继续执行下一步,再次查看四个参数的值,可以在监视里面输入四个参数,每次执行后会显示参数的值
执行
经过多次调试后我们发现,除了第一个参数有变化之外,后面三个参数都是固定的,window.asrsea()函数会将这四个参数传入到d函数中,也就是d,e,f,g

  1. e = '010001'
  2. f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
  3. g = '0CoJUm6Qyw8W8jud'

那么d参数是什么呢?在d函数里打个断点,调试页面直至进入我们的api接口https://music.163.com/weapi/cloudsearch/get/web?csrf_token=为止
接口

  1. Y8Q = Y8Q.replace("api", "weapi");

仔细查看此段代码,这段代码是将链接里的api替换成weapi,因此可以根据Y8Q来定位到当前的链接地址,然后进行下一步调试。
在该行打一个断点,开始调试。
这里有个调试的小技巧,我们可以先在Y8Q那一行打一个断点,先进行调试执行,当Y8Q变为搜索的api接口后,再在window.asrsea()那里打一个断点,然后在d函数打一个断点,进行调试。
记得在Y8Q那里调试时先刷新一下页面,但是后面调试不要
定位
这里我们定位到了api接口,注意api还没有换成weapi,点击执行下一步后,api被替换了
api
然后在window.asrsea打一个断点,点击执行
在这里插入图片描述
然后在d函数打一个断点,点击执行,得到d的值
在这里插入图片描述
直接将d复制过来,注意字符串前要加个r,要不然字符串里的”会被当做转义字符处理。

  1. d = r'{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"自由の翅","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}'
  2. e = '010001'
  3. f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
  4. g = '0CoJUm6Qyw8W8jud'

可以看到,s里面的就是我们搜索的音乐名称了,后面可以更改s的值来改变搜索的音乐。

好了,讲了这么多,结果只分析出四个参数的值是什么,接下来我们发现a,b,c,d这四个函数的作用。

  1. function a(a) {
  2. var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
  3. for (d = 0; a > d; d += 1)
  4. e = Math.random() * b.length,
  5. e = Math.floor(e),
  6. c += b.charAt(e);
  7. return c
  8. }

根据我十分粗糙的js知识来看,这个函数返回的是一个b中的随机字符串,函数接收字符串的长度,我们将它改写成Python代码。

  1. # 获取一个随意字符串,length是字符串长度
  2. def generate_str(lenght):
  3. str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  4. res = ''
  5. for i in range(lenght):
  6. index = random.random() * len(str) # 获取一个字符串长度的随机数
  7. index = math.floor(index) # 向下取整
  8. res = res + str[index] # 累加成一个随机字符串
  9. return res

其实这个随机字符串完全可以定死,但为了尽量还原js脚本的执行过程,我们还是直接照搬过来吧。

  1. function b(a, b) {
  2. var c = CryptoJS.enc.Utf8.parse(b)
  3. , d = CryptoJS.enc.Utf8.parse("0102030405060708")
  4. , e = CryptoJS.enc.Utf8.parse(a)
  5. , f = CryptoJS.AES.encrypt(e, c, {
  6. iv: d,
  7. mode: CryptoJS.mode.CBC
  8. });
  9. return f.toString()
  10. }

b函数是一个AES加密过程,a是加密内容,也就是encText,b是一个key,是一个固定值,也就是上面四个参数中的g

  1. g = '0CoJUm6Qyw8W8jud'

加密的模式为CBC,参照Python AES的加密过程,将js代码改写成了Python代码

  1. # AES加密获得params
  2. def AES_encrypt(text, key):
  3. iv = '0102030405060708'.encode('utf-8') # iv偏移量
  4. text = text.encode('utf-8') # 将明文转换为utf-8格式
  5. pad = 16 - len(text) % 16
  6. text = text + (pad * chr(pad)).encode('utf-8') # 明文需要转成二进制,且可以被16整除
  7. key = key.encode('utf-8') # 将密钥转换为utf-8格式
  8. encryptor = AES.new(key, AES.MODE_CBC, iv) # 创建一个AES对象
  9. encrypt_text = encryptor.encrypt(text) # 加密
  10. encrypt_text = base64.b64encode(encrypt_text) # base4编码转换为byte字符串
  11. return encrypt_text.decode('utf-8')
  1. function c(a, b, c) {
  2. var d, e;
  3. return setMaxDigits(131),
  4. d = new RSAKeyPair(b,"",c),
  5. e = encryptedString(d, a)
  6. }

c函数是RSA加密过程,其中a是随机字符串,b是一个key,也就是上面四个参数中的e,f也是四个参数中的f,返回的是encSeckey

  1. e = '010001'
  2. f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'

由于本人学习的js知识十分粗浅,因此难以看懂这段代码,参考网上流传的版本,改写了代码

  1. # RSA加密获得encSeckey
  2. def RSA_encrypt(str, key, f):
  3. str = str[::-1] # 随机字符串逆序排列
  4. str = bytes(str, 'utf-8') # 将随机字符串转换为byte类型的数据
  5. sec_key = int(codecs.encode(str, encoding='hex'), 16) ** int(key, 16) % int(f, 16) # RSA加密
  6. return format(sec_key, 'x').zfill(256) # RSA加密后字符串长度为256,不足的补x

RSA加密规则不是很熟悉,感兴趣的自行百度

  1. function d(d, e, f, g) {
  2. var h = {}
  3. , i = a(16);
  4. return h.encText = b(d, g),
  5. h.encText = b(h.encText, i),
  6. h.encSecKey = c(i, e, f),
  7. h
  8. }

最后就是d函数了,i是一个16位的随机字符串,可以定死,使用b函数先对d函数进行了AES加密,由于d,g都是固定值,所以得到的encText也是固定值,可以通过调试来获得第一次加密后的encText,然后在运行一下你的Python代码,查看encText是否一致,用来验证d是否正确。
第一次加密得到encText,再次对encText进行第二次加密,不过key换成了随机字符串i,两次加密后得到encText。

  1. # 获取参数
  2. def get_params(d, e, f, g):
  3. i = generate_str(16) # 生成一个16位的随机字符串
  4. # i = 'aO6mqZksdJbqUygP'
  5. encText = AES_encrypt(d, g)
  6. # print(encText) # 打印第一次加密的params,用于测试d正确
  7. params = AES_encrypt(encText, i) # AES加密两次后获得params
  8. encSecKey = RSA_encrypt(i, e, f) # RSA加密后获得encSecKey
  9. return params, encSecKey

至此,参数params和encSecKey都解析完毕,由于字符串是随机的,因此每次运行后得到的params和encSecKey都不一样。

知道参数和接口后,就可以向服务器发送请求,获取返回结果了。注意请求为post请求
由于后续api的解析过程基本一致,因此将代码封装起来。

  1. e = '010001'
  2. f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
  3. g = '0CoJUm6Qyw8W8jud'
  4. # 传入msg和url,获取返回的json数据
  5. def get_data(msg, url):
  6. encText, encSecKey = get_params(msg, e, f, g) # 获取参数
  7. params = {
  8. "params": encText,
  9. "encSecKey": encSecKey
  10. }
  11. re = requests.post(url=url, params=params, verify=False) # 向服务器发送请求
  12. return re.json() #返回结果
  13. # 搜索返回的数据
  14. serch_msg = r'{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"自由の翅","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}'
  15. serch_url = 'https://music.163.com/weapi/cloudsearch/get/web?csrf_token='
  16. print(get_data(serch_msg, serch_url))

三个参数e,f,g固定不变,只更改msg的值。
注意msg字符串前要加个r,防止编译器将字符串里的\当做转义字符处理。
返回结果如下

  1. {
  2. "needLogin": true,
  3. "result": {
  4. "searchQcReminder": null,
  5. "songs": [
  6. {
  7. "name": "自由の翅",
  8. "id": 473403600,
  9. "pst": 0,
  10. "t": 0,
  11. "ar": [
  12. {
  13. "id": 17672,
  14. "name": "佐藤ひろ美",
  15. "tns": [
  16. "佐藤裕美"
  17. ],
  18. "alias": [
  19. "さとう ひろみ",
  20. "Sato Hiromi"
  21. ],
  22. "alia": [
  23. "さとう ひろみ",
  24. "Sato Hiromi"
  25. ]
  26. }
  27. ],
  28. "alia": [
  29. "PCゲーム『月影のシミュラクル -解放の羽-』OPテーマ"
  30. ],
  31. "pop": 85,
  32. "st": 0,
  33. "rt": null,
  34. "fee": 0,
  35. "v": 12,
  36. "crbt": null,
  37. "cf": "",
  38. "al": {
  39. "id": 35377102,
  40. "name": "月影のシミュラクル -解放の羽- オリジナルサウンドトラック",
  41. "picUrl": "http://p3.music.126.net/jUm6aclu8k5fNUdwEODz0w==/18598239185710248.jpg",
  42. "tns": [],
  43. "pic_str": "18598239185710248",
  44. "pic": 18598239185710250
  45. },
  46. "dt": 276866,
  47. "h": {
  48. "br": 320000,
  49. "fid": 0,
  50. "size": 11077007,
  51. "vd": -79678,
  52. "sr": 44100
  53. },
  54. "m": {
  55. "br": 192000,
  56. "fid": 0,
  57. "size": 6646222,
  58. "vd": -77200,
  59. "sr": 44100
  60. },
  61. "l": {
  62. "br": 128000,
  63. "fid": 0,
  64. "size": 4430829,
  65. "vd": -76001,
  66. "sr": 44100
  67. },
  68. "sq": {
  69. "br": 1052522,
  70. "fid": 0,
  71. "size": 36426061,
  72. "vd": -79658,
  73. "sr": 44100
  74. },
  75. "hr": null,
  76. "a": null,
  77. "cd": "1",
  78. "no": 1,
  79. "rtUrl": null,
  80. "ftype": 0,
  81. "rtUrls": [],
  82. "djId": 0,
  83. "copyright": 0,
  84. "s_id": 0,
  85. "mark": 262144,
  86. "originCoverType": 0,
  87. "originSongSimpleData": null,
  88. "tagPicList": null,
  89. "resourceState": true,
  90. "version": 12,
  91. "songJumpInfo": null,
  92. "entertainmentTags": null,
  93. "single": 0,
  94. "noCopyrightRcmd": null,
  95. "rtype": 0,
  96. "rurl": null,
  97. "mst": 9,
  98. "cp": 663018,
  99. "mv": 0,
  100. "publishTime": 1485446400000,
  101. "tns": [
  102. "自由的翅膀"
  103. ],
  104. "privilege": {
  105. "id": 473403600,
  106. "fee": 0,
  107. "payed": 0,
  108. "st": 0,
  109. "pl": 320000,
  110. "dl": 999000,
  111. "sp": 7,
  112. "cp": 1,
  113. "subp": 1,
  114. "cs": false,
  115. "maxbr": 999000,
  116. "fl": 320000,
  117. "toast": false,
  118. "flag": 256,
  119. "preSell": false,
  120. "playMaxbr": 999000,
  121. "downloadMaxbr": 999000,
  122. "maxBrLevel": "lossless",
  123. "playMaxBrLevel": "lossless",
  124. "downloadMaxBrLevel": "lossless",
  125. "plLevel": "exhigh",
  126. "dlLevel": "lossless",
  127. "flLevel": "exhigh",
  128. "rscl": null,
  129. "freeTrialPrivilege": {
  130. "resConsumable": false,
  131. "userConsumable": false,
  132. "listenType": null
  133. },
  134. "chargeInfoList": [
  135. {
  136. "rate": 128000,
  137. "chargeUrl": null,
  138. "chargeMessage": null,
  139. "chargeType": 0
  140. },
  141. {
  142. "rate": 192000,
  143. "chargeUrl": null,
  144. "chargeMessage": null,
  145. "chargeType": 0
  146. },
  147. {
  148. "rate": 320000,
  149. "chargeUrl": null,
  150. "chargeMessage": null,
  151. "chargeType": 0
  152. },
  153. {
  154. "rate": 999000,
  155. "chargeUrl": null,
  156. "chargeMessage": null,
  157. "chargeType": 1
  158. }
  159. ]
  160. }
  161. },
  162. {
  163. "name": "自由の翅 (BEAST-Ⅵ Bootleg)",
  164. "id": 1946185953,
  165. "pst": 0,
  166. "t": 0,
  167. "ar": [
  168. {
  169. "id": 49024337,
  170. "name": "Nero",
  171. "tns": [],
  172. "alias": []
  173. }
  174. ],
  175. "alia": [],
  176. "pop": 5,
  177. "st": 0,
  178. "rt": "",
  179. "fee": 0,
  180. "v": 3,
  181. "crbt": null,
  182. "cf": "",
  183. "al": {
  184. "id": 144732637,
  185. "name": "HyperRave01",
  186. "picUrl": "http://p3.music.126.net/C3YZ8fAg8TJ1pgTdlAbxnA==/109951167398067153.jpg",
  187. "tns": [],
  188. "pic_str": "109951167398067153",
  189. "pic": 109951167398067150
  190. },
  191. "dt": 182987,
  192. "h": {
  193. "br": 320000,
  194. "fid": 0,
  195. "size": 7321644,
  196. "vd": -85382,
  197. "sr": 44100
  198. },
  199. "m": {
  200. "br": 192000,
  201. "fid": 0,
  202. "size": 4393004,
  203. "vd": -83101,
  204. "sr": 44100
  205. },
  206. "l": {
  207. "br": 128000,
  208. "fid": 0,
  209. "size": 2928684,
  210. "vd": -81941,
  211. "sr": 44100
  212. },
  213. "sq": null,
  214. "hr": null,
  215. "a": null,
  216. "cd": "01",
  217. "no": 10,
  218. "rtUrl": null,
  219. "ftype": 0,
  220. "rtUrls": [],
  221. "djId": 0,
  222. "copyright": 0,
  223. "s_id": 0,
  224. "mark": 262144,
  225. "originCoverType": 0,
  226. "originSongSimpleData": null,
  227. "tagPicList": null,
  228. "resourceState": true,
  229. "version": 3,
  230. "songJumpInfo": null,
  231. "entertainmentTags": null,
  232. "single": 0,
  233. "noCopyrightRcmd": null,
  234. "rtype": 0,
  235. "rurl": null,
  236. "mst": 9,
  237. "cp": 2707442,
  238. "mv": 0,
  239. "publishTime": 0,
  240. "privilege": {
  241. "id": 1946185953,
  242. "fee": 0,
  243. "payed": 0,
  244. "st": 0,
  245. "pl": 320000,
  246. "dl": 320000,
  247. "sp": 7,
  248. "cp": 1,
  249. "subp": 1,
  250. "cs": false,
  251. "maxbr": 320000,
  252. "fl": 320000,
  253. "toast": false,
  254. "flag": 128,
  255. "preSell": false,
  256. "playMaxbr": 320000,
  257. "downloadMaxbr": 320000,
  258. "maxBrLevel": "exhigh",
  259. "playMaxBrLevel": "exhigh",
  260. "downloadMaxBrLevel": "exhigh",
  261. "plLevel": "exhigh",
  262. "dlLevel": "exhigh",
  263. "flLevel": "exhigh",
  264. "rscl": null,
  265. "freeTrialPrivilege": {
  266. "resConsumable": false,
  267. "userConsumable": false,
  268. "listenType": null
  269. },
  270. "chargeInfoList": [
  271. {
  272. "rate": 128000,
  273. "chargeUrl": null,
  274. "chargeMessage": null,
  275. "chargeType": 0
  276. },
  277. {
  278. "rate": 192000,
  279. "chargeUrl": null,
  280. "chargeMessage": null,
  281. "chargeType": 0
  282. },
  283. {
  284. "rate": 320000,
  285. "chargeUrl": null,
  286. "chargeMessage": null,
  287. "chargeType": 0
  288. },
  289. {
  290. "rate": 999000,
  291. "chargeUrl": null,
  292. "chargeMessage": null,
  293. "chargeType": 1
  294. }
  295. ]
  296. }
  297. }
  298. ],
  299. "songCount": 2
  300. },
  301. "code": 200
  302. }

这里简单说明一下,code的响应状态,可以根据这个来判断请求是否成功,
result里面,songCount代表搜索结果有几个,song里面是音乐的一些信息,
name,id,ar是艺术家artist的意思,也就是歌手,al是所属专辑,包括名称,封面之类的。
h, l,m,sq分别代表音质的等级,h是极高,l是较高,m的标准,sq是无损。
privilege里面是一些音乐的音质信息,包括可下载的最大音质,会员下载信息等,chargeInfoList列出了各个音质下载所需的权限。

点击一个音乐进入播放界面,打开F12,筛选后一个一个链接寻找https://music.163.com/weapi/song/lyric?csrf_token=,此链接返回歌词信息
歌词

  1. {
  2. "sgc": false,
  3. "sfy": false,
  4. "qfy": false,
  5. "transUser": {
  6. "id": 2204059,
  7. "status": 99,
  8. "demand": 1,
  9. "userid": 76837043,
  10. "nickname": "烈焰中舞动的火花",
  11. "uptime": 1493803368844
  12. },
  13. "lyricUser": {
  14. "id": 2204040,
  15. "status": 99,
  16. "demand": 0,
  17. "userid": 114415020,
  18. "nickname": "another_tonary",
  19. "uptime": 1493803368844
  20. },
  21. "lrc": {
  22. "version": 5,
  23. "lyric": "[00:00.000] 作词 : 羽生みいな\n[00:00.515] 作曲 : Meis Clauson\n[00:01.30]自由の翅\n[00:02.78]月影のシミュラクル -解放の羽- 0P主題歌\n[00:10.45]\n[00:23.35]ここから見る景色は 何故どこか狭く悲しく ah\n[00:32.97]声にならない声で そう君を呼んでいたんだ ah\n[00:43.17]逃げられない蝶のように\n[00:48.00]最期を待つだけじゃないと\n[00:53.03]温かい手 重ねた瞬間(とき)\n[00:58.09]差し込んだ光\n[01:02.03]絡みつくこの糸が 交わされた契約が\n[01:07.14]どれほど命 縛ろうとも\n[01:11.97]いつの日かこの翅(はね)を精一杯広げて\n[01:17.10]君が傍に居てくれるなら\n[01:22.24]きっと飛び立てるの あの空へと\n[01:48.09]仕方のないことだと 何故諦めようとしてた ah\n[01:57.94]紅く染まる暗闇から 抜け出せない気がして ah\n[02:08.16]それでもまだ君と生きたい\n[02:13.07]繫いだ手は震えるけど\n[02:18.05]熱い涙 溢れた瞬間(とき)\n[02:23.06]湧き上がる勇気\n[02:27.08]捕らわれた運命が 立ちはだかる試練が\n[02:32.11]どれほどこの身操ろうとも\n[02:37.00]立ち向かいたい 強く信じるの もっと強く\n[02:42.14]独りじゃないと思えた 君となら\n[02:48.93]飛び立てるの あの空へと\n[02:53.53]忘れかけてた 遠い記憶 あの約束 思い出して\n[03:03.27]取り戻せるの 二人ならば 広い世界を\n[03:12.20]絡みつくこの糸が 交わされた契約が\n[03:17.08]どれほど命 縛ろうとも\n[03:22.01]いつの日かこの翅を 精一杯広げて\n[03:27.06]君が傍に居てくれるなら\n[03:32.00]きっと 光の向こう\n[03:37.11]捕らわれた運命が 立ちはだかる試練が\n[03:42.08]どれほどこの身操ろうとも\n[03:46.93]立ち向かいたい 強く信じるの もっと強く\n[03:52.09]独りじゃないと思えた 君となら\n[03:58.82]飛び立てるの あの空へと\n"
  24. },
  25. "tlyric": {
  26. "version": 5,
  27. "lyric": "[by:所間]\n[ti:自由の翅]\n[ar:佐藤ひろ美]\n[al:月影のシミュラクル -解放の羽- 初回限定同梱 オリジナルサウンドトラック]\n[00:01.30]\n[00:02.78]\n[00:10.45]\n[00:23.35]这里所看到的景色 为何会感到如此狭小又悲伤 ah\n[00:32.97]以泣不成声的声音 不断呼喊着你 ah\n[00:43.17]如同无法挣脱的蝴蝶一般\n[00:48.00]只能默默等候终焉的到来\n[00:53.03]温暖的双手重合的瞬间\n[00:58.09]感受到了照射的光芒\n[01:02.03]不管这纠缠不清的丝线与这被迫签下的契约\n[01:07.14]究竟束缚了多少的生命\n[01:11.97]总有一天要用这双翅膀 用尽全力展翅翱翔\n[01:17.10]只要你能够陪伴在我身边\n[01:22.24]一定就能够展翅高飞 向着那片天空\n[01:48.09]为何要说着“这是无可奈何的事情”而准备去放弃一切呢 ah\n[01:57.94]就算觉得无法从这渐渐染红的黑暗中逃脱出去 ah\n[02:08.16]即便如此仍旧想要与你一同活下去\n[02:13.07]虽然紧牵着的手止不住颤抖\n[02:18.05]温热的泪水 满溢的瞬间\n[02:23.06]心中所涌出的勇气\n[02:27.08]不管这被囚禁的命运与这艰辛的试炼\n[02:32.11]会让这幅身躯会承受多少伤害\n[02:37.00]就算如此也想要奋发向上 不断坚信着 变得更加坚强\n[02:42.14]与你在一起的话 就不会感到孤独\n[02:48.93]向着那片天空展翅高飞\n[02:53.53]从将要遗忘的记忆中找回了那个约定\n[03:03.27]我们一起的话 就能夺回那个宽广的世界\n[03:12.20]不管这纠缠不清的丝线与这被迫签下的契约\n[03:17.08]究竟束缚了多少的生命\n[03:22.01]总有一天要用这双翅膀 用尽全力展翅翱翔\n[03:27.06]只要你能够陪伴在我身边\n[03:32.00]肯定就在那光芒的彼岸\n[03:37.11]不管这被囚禁的命运与这艰辛的试炼\n[03:42.08]会让这幅身躯会承受多少伤害\n[03:46.93]就算如此也想要奋发向上 不断坚信着 变得更加坚强\n[03:52.09]与你在一起的话 就不会感到孤独\n[03:58.82]向着那片天空展翅高飞"
  28. },
  29. "code": 200
  30. }

其中transUser为歌词贡献者,lyricUser为歌词翻译贡献者,lrc里有原版歌词,tlyric里有歌词翻译。
解析过程和上面一样,调试页面找到d的值即可。

  1. d = '{"id":"473403600","lv":-1,"tv":-1,"csrf_token":""}'

id为音乐id,可更改。

  1. # 歌词文件
  2. lyric_msg = '{"id":"427419615","lv":-1,"tv":-1,"csrf_token":""}'
  3. lyric_url = 'https://music.163.com/weapi/song/lyric?csrf_token='
  4. print(get_data(lyric_msg, lyric_url))

https://music.163.com/weapi/comment/resource/comments/get?csrf_token=返回用户评论信息,目前还不需要,不使用。
评论

点击蓝色的播放按钮,发现由多出了一些链接,一个一个找下来。
链接
https://music.163.com/weapi/v3/song/detail?csrf_token=返回音乐的详细信息。
注意,这里调试时需要一点技巧,刷新页面,首先在源码里打个断点
断点
点击播放,注意必须是蓝色的那个播放按钮
在这里插入图片描述
然后调试获得api地址https://music.163.com/weapi/v3/song/detail和d
继续调试可获得音乐的下载地址。
地址

  1. # 音乐详细信息,包含了音质等级和可下载权限
  2. detail_msg = r'{"id":"473403600","c":"[{\"id\":\"473403600\"}]","csrf_token":""}'
  3. detail_url = 'https://music.163.com/weapi/v3/song/detail?csrf_token='
  4. print(get_data(detail_msg, detail_url))

注意msg要加上r,id为音乐id,可更改
其实没有这个也行,在搜索时,返回的数据里就有音乐的详细信息了。

https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=返回的是下载链接,
下载地址
调试找到d,

  1. # 音乐下载地址,level代表音质等级,encodeType代表编码类型,flac可存储无损音质,目前无法下载无损音乐
  2. # 音质 standard标准 higher较高 exhigh极高 lossless无损 hires
  3. # 编码类型 aac flac
  4. song_msg = '{"ids":"[473403600]","level":"lossless","encodeType":"flac","csrf_token":""}'
  5. song_url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
  6. print(get_data(song_msg, song_url))

ids里面是音乐id,既然是一个数组,那想必可以多加几个音乐id,返回多个下载地址。level为音质水平,分四个等级,standard代表标准,higher代表较高,exhigh代表极高,lossless代表无损,还有hires,其中lossless和hires都无法下载,不知道加上有会员权限的csrf_token能不能下载。

在网页版有一个功能叫生成外链播放器,点击一下
外链播放器
它会叫你嵌入一段代码
嵌入
我们将src里面的内容复制出来,添加上头部组成https://music.163.com/outchain/player?type=2&id=473403600&auto=1&height=66
你可以嵌入自己的网站中去(如果你的位置支持嵌入ifram的话),也可以自己写一个前端播放器,然后爬音乐信息,将数据放进去。
打开开发者工具,这里也有两个可用的api接口
一个是音乐详细信息https://music.163.com/weapi/song/detail
详情
另外一个是音乐下载地址https://music.163.com/weapi/song/enhance/player/url
在这里插入图片描述
调试页面获取d

  1. # # 通过外链播放器获取解析的链接
  2. # # 音乐下载地址,br代表音质,依旧无法下载无损音乐
  3. # br四个等级 标准128000 较高192000 极高320000 无损999000
  4. song_msg = '{"ids":"[473403600]","br":1052522,"csrf_token":""}'
  5. song_url = 'https://music.163.com/weapi/song/enhance/player/url'
  6. print(get_data(song_msg, song_url))
  7. #
  8. # # 音乐详情,有更加详细的信息
  9. detail_msg = r'{"id":"473403600","ids":"[\"473403600\"]","limit":10000,"offset":0,"csrf_token":""}'
  10. detail_url = 'https://music.163.com/weapi/song/detail'
  11. print(get_data(detail_msg, detail_url))

返回的数据和前面的差不多,稍微有点出入。

三、其他api

还记得上面提到过的一段代码吗?

  1. Y8Q = Y8Q.replace("api", "weapi");

这段代码将链接里的api换成了weapi,如果我们用原来的链接会怎么样?
以歌词文件的api为例
在这里插入图片描述
补上前缀得到https://music.163.com/api/song/lyric,当然,现在这个链接还用不了,需要传递参数。
展开query,发现里面一些关于音乐的参数,带入到链接里去,https://music.163.com/api/song/lyric?id=473403600&lv=-1&tv=-1
此请求为get请求,自己获取返回数据。
在这里插入图片描述
有没有发现这个lv=-1,tv=-1这么像d里面的参数?

  1. d = '{"id":"427419615","lv":-1,"tv":-1,"csrf_token":""}'

对比一下就知道了。
在这里插入图片描述
几乎一致,通过更改参数的值,发现返回的结果,可以知道各个参数的含义。
其他api类似,如果你不想模拟js的加密过程,可以使用这些api直接获取到数据。
更多api接口请查看接口文档。注意如果要下载会员音乐和无损音乐(如果有的话)的话,要使用具有会员权限的账号cookie,post请求放cookies里,get请求放headers里。

四、最终实现代码

weapi接口的代码实现

  1. """
  2. webapi接口
  3. 搜索结果:https://music.163.com/weapi/cloudsearch/get/web?csrf_token=(post)
  4. 评论:https://music.163.com/weapi/comment/resource/comments/get?csrf_token=
  5. 歌词:https://music.163.com/weapi/song/lyric?csrf_token=
  6. 详情(包括音质):https://music.163.com/weapi/v3/song/detail?csrf_token=
  7. 歌曲下载:https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=
  8. iw233网站解析链接
  9. https://iw233.cn/music/?name=コトダマ紬ぐ未来&type=netease
  10. 外链:http://music.163.com/song/media/outer/url?id=534544522.mp3
  11. 音乐外链播放器:https://music.163.com/outchain/player?type=2&id=473403600&auto=1&height=66
  12. """
  13. import base64
  14. import codecs
  15. import json
  16. import math
  17. import random
  18. import requests
  19. from Crypto.Cipher import AES
  20. from urllib3.exceptions import InsecureRequestWarning
  21. requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  22. '''
  23. var bKB3x = window.asrsea(JSON.stringify(i3x), buU1x(["流泪", "强"]), buU1x(Rg7Z.md), buU1x(["爱心", "女孩", "惊恐", "大笑"]));
  24. e3x.data = j3x.cr3x({
  25. params: bKB3x.encText,
  26. encSecKey: bKB3x.encSecKey
  27. })
  28. window.asrsea = d,
  29. d: {"hlpretag":"<span class="s-fc7">","hlposttag":"</span>","s":"コトダマ紬ぐ未来","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}
  30. e:010001
  31. f:00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
  32. g:0CoJUm6Qyw8W8jud
  33. function d(d, e, f, g) {
  34. var h = {}
  35. , i = a(16);
  36. return h.encText = b(d, g),
  37. h.encText = b(h.encText, i),
  38. h.encSecKey = c(i, e, f),
  39. h
  40. }
  41. function a(a) {
  42. var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
  43. for (d = 0; a > d; d += 1)
  44. e = Math.random() * b.length,
  45. e = Math.floor(e),
  46. c += b.charAt(e);
  47. return c
  48. }
  49. function b(a, b) {
  50. var c = CryptoJS.enc.Utf8.parse(b)
  51. , d = CryptoJS.enc.Utf8.parse("0102030405060708")
  52. , e = CryptoJS.enc.Utf8.parse(a)
  53. , f = CryptoJS.AES.encrypt(e, c, {
  54. iv: d,
  55. mode: CryptoJS.mode.CBC
  56. });
  57. return f.toString()
  58. }
  59. function c(a, b, c) {
  60. var d, e;
  61. return setMaxDigits(131),
  62. d = new RSAKeyPair(b,"",c),
  63. e = encryptedString(d, a)
  64. }
  65. '''
  66. class wangyiyun:
  67. def __init__(self):
  68. self.e = '010001'
  69. self.f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
  70. self.g = '0CoJUm6Qyw8W8jud'
  71. # 获取一个随意字符串,length是字符串长度
  72. def generate_str(self, lenght):
  73. str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  74. res = ''
  75. for i in range(lenght):
  76. index = random.random() * len(str) # 获取一个字符串长度的随机数
  77. index = math.floor(index) # 向下取整
  78. res = res + str[index] # 累加成一个随机字符串
  79. return res
  80. # AES加密获得params
  81. def AES_encrypt(self, text, key):
  82. iv = '0102030405060708'.encode('utf-8') # iv偏移量
  83. text = text.encode('utf-8') # 将明文转换为utf-8格式
  84. pad = 16 - len(text) % 16
  85. text = text + (pad * chr(pad)).encode('utf-8') # 明文需要转成二进制,且可以被16整除
  86. key = key.encode('utf-8') # 将密钥转换为utf-8格式
  87. encryptor = AES.new(key, AES.MODE_CBC, iv) # 创建一个AES对象
  88. encrypt_text = encryptor.encrypt(text) # 加密
  89. encrypt_text = base64.b64encode(encrypt_text) # base4编码转换为byte字符串
  90. return encrypt_text.decode('utf-8')
  91. # RSA加密获得encSeckey
  92. def RSA_encrypt(self, str, key, f):
  93. str = str[::-1] # 随机字符串逆序排列
  94. str = bytes(str, 'utf-8') # 将随机字符串转换为byte类型的数据
  95. sec_key = int(codecs.encode(str, encoding='hex'), 16) ** int(key, 16) % int(f, 16) # RSA加密
  96. return format(sec_key, 'x').zfill(256) # RSA加密后字符串长度为256,不足的补x
  97. # 获取参数
  98. def get_params(self, d, e, f, g):
  99. i = self.generate_str(16) # 生成一个16位的随机字符串
  100. # i = 'aO6mqZksdJbqUygP'
  101. encText = self.AES_encrypt(d, g)
  102. # print(encText) # 打印第一次加密的params,用于测试d正确
  103. params = self.AES_encrypt(encText, i) # AES加密两次后获得params
  104. encSecKey = self.RSA_encrypt(i, e, f) # RSA加密后获得encSecKey
  105. return params, encSecKey
  106. # 传入msg和url,获取返回的json数据
  107. def get_data(self, msg, url):
  108. encText, encSecKey = self.get_params(msg, self.e, self.f, self.g) # 获取参数
  109. params = {
  110. "params": encText,
  111. "encSecKey": encSecKey
  112. }
  113. re = requests.post(url=url, params=params, verify=False) # 向服务器发送请求
  114. return re.json() #返回结果
  115. # 返回搜索数据
  116. def get_search_data(self, s='', type=1, offset=0, total='true', limit=30, csrf_token=''):
  117. msg = r'{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>",' + f'"s":"{s}","type":"{type}","offset":"{offset}","total":"{total}","limit":"{limit}","csrf_token":"{csrf_token}"' + '}'
  118. url = f'https://music.163.com/weapi/cloudsearch/get/web?csrf_token={csrf_token}'
  119. return self.get_data(msg, url)
  120. # 返回歌词数据
  121. def get_lyric_data(self, id, lv=-1, tv=-1, csrf_token=''):
  122. msg = '{' + f'"id":"{id}","lv":"{lv}","tv":"{tv}","csrf_token":"{csrf_token}"' + '}'
  123. url = f'https://music.163.com/weapi/song/lyric?csrf_token={csrf_token}'
  124. return self.get_data(msg, url)
  125. # 返回音乐详情,包含了音质等级和可下载权限
  126. def get_detail_data(self, id, csrf_token=''):
  127. msg = '{' + f'"id":"{id}",' + r'"c":"[{\"id\":\"' + str(id) + r'\"}]",' + f'"csrf_token":"{csrf_token}"' + '}'
  128. url = f'https://music.163.com/weapi/v3/song/detail?csrf_token={csrf_token}'
  129. return self.get_data(msg, url)
  130. # 返回下载数据,level代表音质等级,encodeType代表编码类型,flac可存储无损音质,目前无法下载无损音乐
  131. # # 音质 standard标准 higher较高 exhigh极高 lossless无损 hires
  132. # # 编码类型 aac flac
  133. def get_download_data(self, id, level='exhigh', encodeType='flac', csrf_token=''):
  134. msg = '{' + f'"ids":"{id}","level":"{level}","encodeType":"{encodeType}","csrf_token":"{csrf_token}"' + '}'
  135. url = f'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token={csrf_token}'
  136. return self.get_data(msg, url)
  137. # 通过播放器外链的方式返回的音乐详情数据
  138. def get_detail_outdata(self, id, limit=10000, offset=0, csrf_token=''):
  139. msg = '{' + f'"id":"{id}",' + r'"ids":"[\"' + str(id) + r'\"]",' + f'"limit":{limit},"offset":{offset},"csrf_token":"{csrf_token}"' + '}'
  140. url = 'https://music.163.com/weapi/song/detail'
  141. return self.get_data(msg, url)
  142. # 通过播放器外链的方式返回的音乐下载数据
  143. # br代表音质,四个等级 标准128000 较高192000 极高320000 无损999000
  144. def get_download_outdata(self, id, br=320000, csrf_token=''):
  145. msg = '{' + f'"ids":"{id}","br":{br},"csrf_token":"{csrf_token}"' + '}'
  146. url = 'https://music.163.com/weapi/song/enhance/player/url'
  147. return self.get_data(msg, url)

api接口的代码实现

  1. '''
  2. api接口
  3. '''
  4. import json
  5. import requests
  6. from urllib3.exceptions import InsecureRequestWarning
  7. requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
  8. class wangyiyun:
  9. # 获取数据
  10. def get_data(self, url, data):
  11. re = requests.post(url=url, data=data, verify=False)
  12. return re.json()
  13. # 返回搜索数据
  14. def get_search_data(self, s='', type=1, offset=0, total='true', limit=30, csrf_token=''):
  15. url = 'https://music.163.com/api/cloudsearch/get/web'
  16. data = {
  17. 'hlpretag': '<span class="s-fc7">',
  18. 'hlposttag': '</span>',
  19. 's': s,
  20. 'type': type,
  21. 'offset': offset,
  22. 'total': total,
  23. 'limit': limit,
  24. 'csrf_token': csrf_token
  25. }
  26. return self.get_data(url, data)
  27. # 返回歌词数据
  28. def get_lyric_data(self, id, lv=-1, tv=-1, csrf_token=''):
  29. url = 'https://music.163.com/api/song/lyric'
  30. data = {
  31. 'id': id,
  32. 'lv': lv,
  33. 'tv': tv,
  34. 'csrf_token': csrf_token
  35. }
  36. return self.get_data(url, data)
  37. # 返回音乐详情,包含了音质等级和可下载权限
  38. def get_detail_data(self, id, csrf_token=''):
  39. url = 'https://music.163.com/api/v3/song/detail'
  40. c = '[{' + f'"id":"{id}"' + '}]'
  41. data = {
  42. 'id': id,
  43. 'c': c,
  44. 'csrf_token': csrf_token
  45. }
  46. return self.get_data(url, data)
  47. # 返回下载数据,level代表音质等级,encodeType代表编码类型,flac可存储无损音质,目前无法下载无损音乐
  48. # # 音质 standard标准 higher较高 exhigh极高 lossless无损 hires
  49. # # 编码类型 aac flac
  50. def get_download_data(self, id, level='exhigh', encodeType='flac', csrf_token=''):
  51. url = 'https://music.163.com//api/song/enhance/player/url/v1'
  52. data = {
  53. 'encodeType': encodeType,
  54. 'ids': str(id),
  55. 'level': level,
  56. 'csrf_token': csrf_token
  57. }
  58. return self.get_data(url, data)
  59. # 通过播放器外链的方式返回的音乐详情数据
  60. def get_detail_outdata(self, id, limit=10000, offset=0, csrf_token=''):
  61. url = 'https://music.163.com/api/song/detail'
  62. data = {
  63. 'id': id,
  64. 'ids': f'[{str(id)}]',
  65. 'limit': limit,
  66. 'offset': offset,
  67. 'csrf_token': csrf_token
  68. }
  69. return self.get_data(url, data)
  70. # 通过播放器外链的方式返回的音乐下载数据
  71. # br代表音质,四个等级 标准128000 较高192000 极高320000 无损999000
  72. def get_download_outdata(self, id, br=320000, csrf_token=''):
  73. url = 'https://music.163.com/api/song/enhance/player/url'
  74. data = {
  75. 'br': br,
  76. 'ids': str(id),
  77. 'csrf_token': csrf_token
  78. }
  79. return self.get_data(url, data)

五、总结

全部过程爬取下来,发现网易云对数据的加密方式还是挺单调的,只要弄懂了原理就好办了,基本上都一致。
后续考虑做个音乐播放器,添加网易云,QQ,酷狗,百度等源,等做好了再发个教程和项目地址,不过感觉用处也不大,毕竟不能下载vip音乐和无损音乐,就当做是学习了。
小伙伴们有什么不懂的地方可以私信我,也可以在评论区留言。

版权声明:本文为pikeduo原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/pikeduo/p/16938738.html