『Python』网易云音乐API爬虫(音乐搜索&音乐播放)

您所在的位置:网站首页 intersection网易云音乐 『Python』网易云音乐API爬虫(音乐搜索&音乐播放)

『Python』网易云音乐API爬虫(音乐搜索&音乐播放)

2024-01-25 18:24| 来源: 网络整理| 查看: 265

在这里插入图片描述

前言

永远相信美好的事情即将发生

背景

一直想做一个在线的音乐播放器,这个想法最早可以追溯到做毕设的那会,那时候做了个在线的商城系统, 里面有个在线听歌的模块,其实就是调用大佬们封装好的API进行搜索和播放。当时一直想着自己去找接口进行封装,但奈何一直没有时间(其实就是惰性),这段时间终于不怎么忙了,于是决定完成这个拖延了一年的 “需求” 。

准备

开发环境:Python 3.8 64位 开发工具:Pycharm 浏览器:Chrome

历程 接口获取

首先我们需要在浏览器中打开网易云音乐,别问我为什么要使用网易云

在这里插入图片描述 打开之后随便搜索一首歌曲,然后 F12 打开开发者模式

在这里插入图片描述 依次对这几条请求进行分析,其中有一条 web?csrf_token= 响应的数据比较可疑,对数据进行json格式化后如图

在这里插入图片描述 凭借我小学三年级的英语水平来看,基本就可以断定这条请求就是歌曲搜索的请求。然后

在这里插入图片描述

JS分析

接下来我们对请求的数据进行分析,点开 Headers 后我们可以发现,除了传统的请求头参数外,本次请求还携带了一个 Form 表单,其中有两个参数,分别是 params 和 encSecKey

在这里插入图片描述 经过对其他参数携带的信息分析后我们可以发现,这两个参数就是进行搜索的关键参数,于是我们对 encSecKey 参数进行如下搜索

在这里插入图片描述 双击第一个js,进行格式化后使用 Ctrl+F 进行搜索,如下图所示

在这里插入图片描述 不难发现, 这两个参数都是通过一个名为 bVZ7S(asrsea) 的函数获取到的,那我们现在就对这个 asrsea 进行搜索

在这里插入图片描述

搜索如图,可以看出 asrsea 函数其实就是一个名为 d 的函数

在这里插入图片描述

现在对这个 d 函数进行分析,首先它执行了一次 a 函数,然后又连续执行了两次 b 函数,最后执行了一次 c 函数,然后拿到一个最终的结果

现在我们依次对这几个函数进行解析:

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

a 函数的逻辑还是比较简单的,就是从大小写英文字母以及10个数字中随机抽取16个字符拼接成一个新的字符串返回结果

function b(a, b) { var c = CryptoJS.enc.Utf8.parse(b) , d = CryptoJS.enc.Utf8.parse("0102030405060708") , e = CryptoJS.enc.Utf8.parse(a) , f = CryptoJS.AES.encrypt(e, c, { iv: d, mode: CryptoJS.mode.CBC }); return f.toString() }

b 函数的逻辑还是稍微复杂,通过对关键词进行分析我们可以判断是通过 CBC 模式进行 AES 加密,将传入的 a 参数和 b 参数分别作为需要加密的内容和密钥,iv偏移量为一个固定的字符串 0102030405060708,完成加密

在这里插入图片描述

function c(a, b, c) { var d, e; return setMaxDigits(131), d = new RSAKeyPair(b,"",c), e = encryptedString(d, a) }

c 函数算是个大坑了,当时看到里面有个 RSA 字样,就自然而然的以为是一个RSA加密,等到找公钥私钥的时候才发现不对劲,这咋啥都没有啊,于是苦苦搜索js文件,发现就是一连串的字符串操作,和 RSA 没一点关系,代码部分参考了大佬的写法,大家可以研究一下js

对主函数 d 的分析就算结束了,接下来我们再返回js文件对传入的参数进行分析

首先,我们在网易云音乐的搜索框中输入需要搜索的歌曲名,然后在js中 d 函数处打上断点,多打几个

在这里插入图片描述

可以看到,d 函数分别传入了以下参数:

d:{"hlpretag":"","hlposttag":"","s":"Lily","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""} e:"010001" f:"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" g:"0CoJUm6Qyw8W8jud"

通过多首歌曲的测试我们发现,参数 e,f,g 始终保持不表,由此可见它们是3个常量,只是参数 d 中的歌曲名称在变化

接下来我们在测试一下歌曲播放时的请求参数

在这里插入图片描述

依旧对传入的几个参数进行获取

d:{"ids":"[1333159921]","level":"standard","encodeType":"aac","csrf_token":""} e:"010001" f:"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" g:"0CoJUm6Qyw8W8jud"

可以看出,播放歌曲其实和搜索歌曲用的是同一个方法,只是传入的参数不同,准确点说只是传入的d参数不同,一个传的是歌曲的名称,而另一个则是歌曲的id

OK,到此我们就算完成了对网易云音乐Web版整个逻辑的梳理,接下来就是用Python代码去实现

在这里插入图片描述

Python模拟 #!D:/Code/python # -*- coding: utf-8 -*- # @Time : 2020/8/22 12:32 # @Author : Am0xil # @Description : 网易云音乐模拟 import base64 import binascii import json import random import string from urllib import parse import requests from Crypto.Cipher import AES # 从a-z,A-Z,0-9中随机获取16位字符 def get_random(): random_str = ''.join(random.sample(string.ascii_letters + string.digits, 16)) return random_str # AES加密要求加密的文本长度必须是16的倍数,密钥的长度固定只能为16,24或32位,因此我们采取统一转换为16位的方法 def len_change(text): pad = 16 - len(text) % 16 text = text + pad * chr(pad) text = text.encode("utf-8") return text # AES加密方法 def aes(text, key): # 首先对加密的内容进行位数补全,然后使用 CBC 模式进行加密 iv = b'0102030405060708' text = len_change(text) cipher = AES.new(key.encode(), AES.MODE_CBC, iv) encrypted = cipher.encrypt(text) encrypt = base64.b64encode(encrypted).decode() return encrypt # js中的 b 函数,调用两次 AES 加密 # text 为需要加密的文本, str 为生成的16位随机数 def b(text, str): first_data = aes(text, '0CoJUm6Qyw8W8jud') second_data = aes(first_data, str) return second_data # 这就是那个巨坑的 c 函数 def c(text): e = '010001' f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' text = text[::-1] result = pow(int(binascii.hexlify(text.encode()), 16), int(e, 16), int(f, 16)) return format(result, 'x').zfill(131) # 获取最终的参数 params 和 encSecKey 的方法 def get_final_param(text, str): params = b(text, str) encSecKey = c(str) return {'params': params, 'encSecKey': encSecKey} # 通过参数获取搜索歌曲的列表 def get_music_list(params, encSecKey): url = "https://music.163.com/weapi/cloudsearch/get/web?csrf_token=" payload = 'params=' + parse.quote(params) + '&encSecKey=' + parse.quote(encSecKey) headers = { 'authority': 'music.163.com', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36', 'content-type': 'application/x-www-form-urlencoded', 'accept': '*/*', 'origin': 'https://music.163.com', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://music.163.com/search/', 'accept-language': 'zh-CN,zh;q=0.9', } response = requests.request("POST", url, headers=headers, data=payload) return response.text # 通过歌曲的id获取播放链接 def get_reply(params, encSecKey): url = "https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=" payload = 'params=' + parse.quote(params) + '&encSecKey=' + parse.quote(encSecKey) headers = { 'authority': 'music.163.com', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36', 'content-type': 'application/x-www-form-urlencoded', 'accept': '*/*', 'origin': 'https://music.163.com', 'sec-fetch-site': 'same-origin', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'referer': 'https://music.163.com/', 'accept-language': 'zh-CN,zh;q=0.9' } response = requests.request("POST", url, headers=headers, data=payload) return response.text if __name__ == '__main__': song_name = input('请输入歌曲名称,按回车键搜索:') d = {"hlpretag": "", "hlposttag": "", "s": song_name, "type": "1", "offset": "0", "total": "true", "limit": "30", "csrf_token": ""} d = json.dumps(d) random_param = get_random() param = get_final_param(d, random_param) song_list = get_music_list(param['params'], param['encSecKey']) print('搜索结果如下:') if len(song_list) > 0: song_list = json.loads(song_list)['result']['songs'] for i, item in enumerate(song_list): item = json.dumps(item) print(str(i) + ":" + json.loads(str(item))['name']) d = {"ids": "[" + str(json.loads(str(item))['id']) + "]", "level": "standard", "encodeType": "", "csrf_token": ""} d = json.dumps(d) param = get_final_param(d, random_param) song_info = get_reply(param['params'], param['encSecKey']) if len(song_info) > 0: song_info = json.loads(song_info) song_url = json.dumps(song_info['data'][0]['url'], ensure_ascii=False) print(song_url) else: print("该首歌曲解析失败,可能是因为歌曲格式问题") else: print("很抱歉,未能搜索到相关歌曲信息")

手机端经常出现代码被吞的情况,各位看官可以移步我的 GitHub , 不胜荣幸

感谢大佬 @一只不会爬的虫子 的指点,最开始的 AES 填充方法有点问题,导致部分歌曲搜索不到,经修改后正常,如果后期有什么新的问题欢迎各位留言

测试

接下来我们启动程序进行测试,测试结果如下:

在这里插入图片描述

可以看到,已经获取到歌曲 酷爱 的部分搜索结果,我们随机复制一条播放链接到浏览器(记得删掉前后的双引号)进行测试,结果如图

在这里插入图片描述

OK,大功告成

其实最开始是做成直接播放的模式,但Python好像只支持播放本地文件,不支持URL的形式,当然还有可能是因为我太菜了,如果谁有好的方案也欢迎各位大佬赐教,万分感谢

总结

爬取过程中碰到过如下几个比较耗费时间的问题,特此记录

1.进行AES加密时需要将加密内容填充满16位,尽量不要使用其他字符,否则会生成无效的params和encSecKey参数,使用该参数请求接口将返回空数据; 2.在访问接口时,切记对params和encSecKey参数进行URL编码,将其中的 “=” 转换为 “%3D” ,否则返回结果也将为空; 3.请求歌曲播放地址时目前只能请求到MP3格式,M4A格式的歌曲暂时无法获取,应该是参数设置的问题,后面有时间在解决,当然也欢迎各位大佬指点一二 4.搜索歌曲时偶尔会存在搜索失败的情况,重新搜索后又正常,暂时未发现是什么Bug,后面有时间的话再改吧



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3