进入歌曲页面后,url中有一个参数id,因此可以猜想:如果当前页面中能找到歌曲的url,只需要用id构造url,就能获取歌曲的链接,但事实上没有那么简单(后面会详细分析)
按F12 ,打开调试面板,刷新。发现,当前页面是一个html页面
搜索歌曲id,但是网页源代码中并没有(所以前面的猜想不成立,得另寻他法)
既然这样,歌曲可能是动态加载的,需要到js中找,点击 XHR,从第一个到最后一个连接,发现并没有当前页面的id(1398663411)
既然是动态加载,再点击播放试试。功夫不负有心人,果然找到了
既然找到了,那就打开看看它的庐山站面目,看到url,并且是m4a后缀,是不是很惊喜
果然没错,就是它
既然这样我们就试试吧,结果发现,能拿到数据
import requests
url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
headers = {
'origin': 'https://music.163.com',
'referer': 'https://music.163.com/',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
}
data = {
'params': '+i3P/nsviot5Ms6bDX+wUmyKAFRm7u3J7pdECwtbeVV10k41za7DjYIXSgCbNngYOO7GH+ADMz/pULLM6bItfAGGfjatL9xirPAWnwzGbr9uTul31ITnC9es8lu01n5eyP8svVHDCecGH57SU0u+hA==',
'encSecKey': '21c157c5c208498782f0cecee8b518ee8726bba93e2dcb6e280d4d0e4c6ceea3107e5eaab6ad55ca53c713cd9893d5cf11491dae043bebccacc1c95680fff17ba34ccdfc3e3c4863eca69b671ef510a44e3f68d6fc182222d6e55b6d0fa320bf166364c82aa6a2adb641d60e7f480d5809f9c7e1963f884c9c5cf80cd81e5e08'
}
res = requests.post(url, headers=headers, data=data)
print(res.json())
但是问题来了,params和encSeckey是啥玩意儿?这么长,一看就知道是加过密的,接下来要开启解密模式了
点击面板右上角的全局搜索,搜索encSeckey
有3个js文件,一个一个去找吧
先别着急找,像下面这样,结合起来,在第一个js文件中找到的可能性大一些,既然这样,那就开始找吧
小插曲:上面为什要搜encSeckey,而不搜params呢?因为用params命名的使用范围要比encSeckey广,所以用encSeckey搜好一些(经验)
点击第一个js,进去后点击格式化,搜索encSeckey,发现有3处,
滑到第二处,为什么是第二处呢?很简单,因为params和encSeckey在一起嘛,不在这你说在哪?好了,到这里,另外的两个js可以忽略了。
接下来就要分析参数了,我们往上嫖,发现两个参数分别是由bWv7o的encText、encSecKey两个属性。
接下来才是真正的js逆向了,敲黑板了!!!
既然params和encKeckey是由bWv7o
产生的,那么我们就要分析bWv7o
是什么了。不难发现bWv7o
是由window.asrsea(JSON.stringify(i1x), bsK8C(["流泪", "强"]), bsK8C(XR1x.md), bsK8C(["爱心", "女孩", "惊恐", "大笑"]));
产生的,那么接下来就找找window.asrsea是啥玩意儿
搜索一下window.asrsea,window.asrsea=d
这里d就不能去搜索了,不现实,原因你懂的。这里的d肯定是和window.asrsea在同一作用域内,要不然咋赋值,稀里哗啦宝一大堆错,既然这样就往上翻吧(因为是赋值,所以不会在下面,没人这么干)
在同一作用域内的就这么点,就一个d函数
!function() {
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
}
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()
}
function c(a, b, c) {
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}
function d(d, e, f, g) {
var h = {}
, i = a(16);
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}
function e(a, b, d,, e) {
var f = {};
return f.encText = c(a + e, b, d),
f
}
window.asrsea = d,
window.ecnonasr = e
}();
接下来分析d函数是干嘛的,看来看去该函数不就返归一个h对象嘛,不就相当于window.asrsea=h嘛,而params就是encText ,encSecKey 就是encSecKey;encText 是b(h.encText, i)产生的,encSecKey是由c(i, e, f)产生的,接下来打上断点,开始调试吧
刷新,点击播放,发现有4个参数
再点一首歌,发现e,f,g三个参数是固定的
综上,d参数是变化的,而且变化的地方是id,分析的时候就当它是固定的,接着往后走
h={},i=a(16),那么a(16)是什么?点击去,一探究竟
经过调试和分析,a函数返回一个16位随机字符串(既然是随机的,那么就可以让它固定)
接着往后走
由此可见b函数,是用来加密的,而且是encText经过了两次加密,跳进去,看看这个加密函数
原来是AES加密,熟悉AES的,肯定敲开心,不熟悉也没关系,很容易
function b(a, b) {
// urf-8编码
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708") // 固定值
, e = CryptoJS.enc.Utf8.parse(a)
// AES加密,e加密文本,c秘钥,d偏移量,CBC模式
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.CBC
});
return f.toString()
}
function c(a, b, c) { // a,b,c是固定值
var d, e;
return setMaxDigits(131), // 跳进去,调试... 创建长度为132的数组
d = new RSAKeyPair(b,"",c), //
e = encryptedString(d, a)
}
由此可知,encSeckey是经过RSA加密的,但是这里的a,b,c三个参数是固定值,因此这个参数也可以用固定值
好了,到这里js逆向分析就结束了
class Encrypt:
def __init__(self, text):
self.data = {
'encSecKey': '01ec48cb405730aa77f993a988cc1f5bc1938511d75f49eddc581f2fe2aaf18988853200564b2d4b1312cf6e0bb344425addce5a4c81b38b89a5973900946bd100b0f1865d22d2a8e5dd8be208eb5d6eb2f71309a165daeffe95355e1e44edd65bdf28088fe4f5e835a7d9f7569fc2530f9d17c00b51cfafbe421eb462247ea3'
}
self.text = text
self.key = '0CoJUm6Qyw8W8jud'
def get_form_data(self):
"""生成表单参数"""
# 随机秘钥参数,可以用固定值
i = "4JknCzx6uEXUwxpU"
# 两次加密
first_encrypt = self.AES_encpyt(self.text, self.key)
self.data['params'] = self.AES_encpyt(first_encrypt, i)
return self.data
def AES_encpyt(self, text, key):
"""AES加密"""
# AES加密明文必须为16的整数倍
padding = 16 - len(text.encode()) % 16
text += padding * chr(padding)
aes = AES.new(key.encode(), AES.MODE_CBC, b'0102030405060708')
enctext = aes.encrypt(text.encode())
return b64encode(enctext).decode('utf-8')
class NeteaseCloudMusic:
def __init__(self, song):
self.url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
reqstr = '''
authority: music.163.com
method: POST
path: /weapi/song/enhance/player/url/v1?csrf_token=
scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
content-length: 434
content-type: application/x-www-form-urlencoded
cookie: __root_domain_v=.163.com; _qddaz=QD.28yaab.3jymc6.kf0ihnaf; _ntes_nnid=e7c5f90265b4d5a5bcb511efebf7a890,1600596980395; _ntes_nuid=e7c5f90265b4d5a5bcb511efebf7a890; _iuqxldmzr_=32; WM_TID=OlHvFOuIVclAQFQUAEJvJZyLuh3MwtGb; NMTID=00ODCot1Uq8CvcXIUIMmKBlPfRiyfoAAAF3NHwibw; WM_NI=%2BWiHzgkFWg%2BON3YYI0rQzlpsOW8x4BPGt%2FWRNpkD3r2Utv8U1gx6RZgvmmJQ0IpSBgdk1GvY9uIQW6BfIN7lVoHo8z1BIoa%2FdLUgKwpx6twUKJtgDlexKOu7LqWGuYApZzg%3D; WM_NIKE=9ca17ae2e6ffcda170e2e6eeb9c844a3b1aba3b24489eb8eb6d15b929a9baaaa5cace70087b64e8ab18299d02af0fea7c3b92ae989a7a9f96da99a9988aa458eed97bacc3cb28fb68df3798d89f899b74a9499bcd0d65a8eb0a5a5b27af28bbc97bb5ff3b9b8d7d152a5aaa38ec95bf497c0b4c16da8b5ffa8f553fbab87b2d63e82ba87afb66896b18890bb72f39e8790e425a8949b88ca7db4a8fa95f65f8996bc88c768a7a885b0f83d90af99a8f85383b0969be637e2a3; hb_MA-9F44-2FC2BD04228F_source=www.baidu.com; JSESSIONID-WYYY=bERBG86BVbD29X%5C35acjg8ndIoGYPEZvQ8fc0t7WUnMu3KTujvG1zqfSMIG%2By4%2FZRz9hC%2FwBN0Mf%2B%2B1RJBK2TeR96X7l%2BmS%2FHhuuqBwl7yxwe4jQ%5ChzFoFgKylb3ZdOnw6%2FqsqaUYUrJ12EVVy0m66JVlQez0T5ijmgZuOsk0KcMnUe4%3A1611553513123; WEVNSM=1.0.0; WNMCID=kctjbv.1611551714155.01.0
origin: https://music.163.com
pragma: no-cache
referer: https://music.163.com/
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-origin
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
'''
self.headers = HeaderPrettyDict().pretty(reqstr)
self.text = '{"ids":"[' + str(song['song_id']) + ']","level":"standard","encodeType":"aac","csrf_token":""}'
self.name = song['song_name']
self.singer = song['singer']
def music(self):
"""获取音乐的url"""
data = Encrypt(self.text).get_form_data()
res = requests.post(self.url, headers=self.headers, data=data)
song_url = res.json()['data'][0]['url']
self.save(self.download(song_url))
def download(self, url):
"""下载音乐"""
headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'}
res = requests.get(url, headers=headers)
return res.content
def save(self, content):
"""保存音乐"""
# 当前文件目录
path = os.path.dirname(__file__)
# 检查'data'目录是否存在,不存在则创建目录
if not os.path.exists(path+'\\data'):
os.mkdir(path+'\\data')
# 音乐保存路径
music_path = path+'\\data'+f'\\{self.name} {self.singer}.m4a'
# 保存
if not os.path.exists(music_path):
with open(music_path, 'wb') as f:
f.write(content)
以上只能实现单曲下载,下面将实现歌曲搜索功能
先分析一下url的结构:url有连个参数,一个是s(歌曲名),一个是type(类型:单曲、视频、歌词等)
type类型总结如下:
数字 | 类型 |
---|---|
1 | 单曲 |
10 | 专辑 |
100 | 歌手 |
1014 | 视频 |
1006 | 歌词 |
1000 | 歌单 |
1009 | 声音主播 |
1002 | 用户 |
步骤和前面一样,先找到数据来源
接下来就要分析请求网址和参数了,乍一看,和前面一模一样啊
开心吧!!!但是事与愿违,虽然参数一样,但是用前面的生成参数却无法get到数据,别慌,方法还是一样,断点调试
经过调试之后,我们发现就是d参数不同(d={"hlpretag":"","hlposttag":"","s":"冬眠","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}
),因此只要构造d参数就能请求到数据了
class SearchMusic:
def __init__(self, text):
self.url = 'https://music.163.com/weapi/cloudsearch/get/web?csrf_token='
reqstr = '''
authority: music.163.com
method: POST
path: /weapi/song/enhance/player/url/v1?csrf_token=
scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
content-length: 434
content-type: application/x-www-form-urlencoded
cookie: __root_domain_v=.163.com; _qddaz=QD.28yaab.3jymc6.kf0ihnaf; _ntes_nnid=e7c5f90265b4d5a5bcb511efebf7a890,1600596980395; _ntes_nuid=e7c5f90265b4d5a5bcb511efebf7a890; _iuqxldmzr_=32; WM_TID=OlHvFOuIVclAQFQUAEJvJZyLuh3MwtGb; NMTID=00ODCot1Uq8CvcXIUIMmKBlPfRiyfoAAAF3NHwibw; WM_NI=%2BWiHzgkFWg%2BON3YYI0rQzlpsOW8x4BPGt%2FWRNpkD3r2Utv8U1gx6RZgvmmJQ0IpSBgdk1GvY9uIQW6BfIN7lVoHo8z1BIoa%2FdLUgKwpx6twUKJtgDlexKOu7LqWGuYApZzg%3D; WM_NIKE=9ca17ae2e6ffcda170e2e6eeb9c844a3b1aba3b24489eb8eb6d15b929a9baaaa5cace70087b64e8ab18299d02af0fea7c3b92ae989a7a9f96da99a9988aa458eed97bacc3cb28fb68df3798d89f899b74a9499bcd0d65a8eb0a5a5b27af28bbc97bb5ff3b9b8d7d152a5aaa38ec95bf497c0b4c16da8b5ffa8f553fbab87b2d63e82ba87afb66896b18890bb72f39e8790e425a8949b88ca7db4a8fa95f65f8996bc88c768a7a885b0f83d90af99a8f85383b0969be637e2a3; hb_MA-9F44-2FC2BD04228F_source=www.baidu.com; JSESSIONID-WYYY=bERBG86BVbD29X%5C35acjg8ndIoGYPEZvQ8fc0t7WUnMu3KTujvG1zqfSMIG%2By4%2FZRz9hC%2FwBN0Mf%2B%2B1RJBK2TeR96X7l%2BmS%2FHhuuqBwl7yxwe4jQ%5ChzFoFgKylb3ZdOnw6%2FqsqaUYUrJ12EVVy0m66JVlQez0T5ijmgZuOsk0KcMnUe4%3A1611553513123; WEVNSM=1.0.0; WNMCID=kctjbv.1611551714155.01.0
origin: https://music.163.com
pragma: no-cache
referer: https://music.163.com/
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-origin
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
'''
self.headers = HeaderPrettyDict().pretty(reqstr)
self.text = text
def search(self):
"""搜索音乐,返回音乐列表"""
data = Encrypt(self.text).get_form_data()
res = requests.post(self.url, headers=self.headers, data=data)
songlist = []
songs = res.json()['result']['songs']
for song in songs:
item = {}
# id、歌名、歌手、封面
item['song_id'] = song['id']
item['song_name'] = song['name']
item['singer'] = song['ar'][0]['name']
# item['song_pic_url'] = song['al']['picUrl']
songlist.append(item)
return songlist