Python爬虫——js逆向网易云音乐
下载地址
下载链接: github:https://github.com/13337356453/YunMusic
第三方库
1 pip install pycryptodome 2 pip install requests
1.前言
我在去年曾写过一个Python的音乐下载器,当时的代码放到现在任可以运行。但是我对其中的网易云爬虫十分不满意——太麻烦。需要安装很多东西,而且不大稳定。因此,在2021年的尾声,我重新拿起了键盘,打开了音乐下载器的项目,决心用js逆向的方式实现网易云的爬虫。

2.明确思路
首先明确下爬虫的思路,大概是这样一个流程

3.js逆向
3.1 搜索算法
按照上述思路图,我们第一步要对歌曲进行搜索,这样才能找到我们想要的歌曲。就像我们在听歌时,也要先搜索关键字一般。

在这里我以love story为例,进行搜索算法的逆向
3.1.1 查找加密算法
首先打开网易云的网站,在搜索位置输入关键字,按下回车。 可以看到弹出了搜索结果。
右键查看源代码中是否有音乐信息

一般来说,像这种比较好的网站,都是用的ajax异步加载,因此在源代码中没有信息是不意外的。
回到搜索界面,按下键盘上的F12,打开开发者工具抓包,选中Network里的XHR,抓取异步包
刷
刷新网页,可以看到出现了好几个请求
一个个查看请求内容,查找需要的信息。
在请求地址为web?csrf_token=的请求中,看到了相应的歌曲信息,因此,这就是搜索的请求包。
查看请求体,其中URL为https://music.163.com/weapi/cloudsearch/get/web?csrf_token=,请求方法为POST
再看请求参数
params: voBam4cxWx5/TF7tKqGyC6RzVMK5bVqJRrhjoJFJTRgq3myJOA3KaldECYT29HfHa1AtdmP+reiRNCQEhSZGY/xOD0hip4L4ygBL0WK/r9apJiPfzOtvzWIe84ieurQzP/WyL918APEH1ntTNwRbyFjZn0UNgpYNy9YEFrj8vS4ANlPrNSFAk3xYn+V2mFff9LeuEetDKqvczddxEiRQAkMNemoMpCO4bo33BLsem/5Hoh0bHLkuNg85fu/II63iLLadL9IiJmixQXiW/5HBFH8NElr2yxn9KFZe7qr5gE4= encSecKey: 6ae20c6f5d524f58edb7499b4a6dcfa7f125fa398cb86015a7b965da30f4cf192bb688b1ff729385da5d7c5aa0cdac0d1d7004b3e2cea1c7e187ecb3578aff2d57d5411fc4e1067e5378cc860d12cd113004d18383e1555a1fc2405d71f083d3e9794de16760272ec0d4777f2e26d209150aeb2e0e1ae35c84e2e16b620b00bc
这两个鬼东西一看就是进行过加密的,并且每次刷新都不一样,因此猜测是跟时间或随机有关。
接下来找一下这两个鬼东西是怎么整出来的。
选中开发者工具中的Sources,按下Ctrl+Shift+F进行全局搜索
在这里搜索encSecKey这个参数(不要问为什么,问就是经验),发现有3个js文件
一个个进行查看,先查看这个core开头的js文件(不要问,问就是经验)
打开文件后按下左下角的 {} 进行代码的格式化

接下来按下Ctrl+F搜索关键内容encSecKey,发现有三个结果,分别进行查看。
在第二个结果和第三个结果出现,发现两个参数params,encSecKey同时出现,判断就是这个位置。
在这里如果有一定的js基础更好,没有的话也问题不大。
var bUM2x = window.asrsea(JSON.stringify(i6c), bsG7z(["流泪", "强"]), bsG7z(WW3x.md), bsG7z(["爱心", "女孩", "惊恐", "大笑"])); e6c.data = j6d.cs6m({ params: bUM2x.encText, encSecKey: bUM2x.encSecKey })
这里很明显,params和encSecKey分别是bUM2x对象的两个属性。
那么bUM2x是个什么东西?
很明显,就在上一行,bUM2x是window.asrsea函数的一个返回值
在这几行打上断点,刷新一下网页


将鼠标放到window.asrsea上,自动弹出了这个函数真实函数——d(),点一下跳转到这个函数。

3.1.2 js逆向分析
看到d函数中返回了encText和encSecKey。这两个东西就是我们所需的参数。
其中,i是a函数的返回结果,接下来,用b函数对参数进行两次处理,用c函数对参数进行处理,返回处理后的结果。
同一定义域的函数就那么几个,干脆全部保存来分析
1 !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函数中打上断点,刷新网页
将鼠标放到参数上,即可查看参数的内容
在这里,我们发现参数e,f,g都是定值,值分别为
e : "010001" f : "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" g : "0CoJUm6Qyw8W8jud"
而这个d的值,却是常常在变化的
按下上方的小箭头,执行下一步代码,看是否有有效的信息
在按了很多下这个按钮后,函数d执行了好几次,总算是出现了这样的内容 
其中,d的内容为
"{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>", "s":"love story","type":"1","offset":"0","total":"true", "limit":"30","csrf_token":""}" 在里面出现了搜索关键字“love story”,因此这应该
就是我们需要的参数。
对这一串json数据进行查看,发现其s的内容是搜索关键字,limit是返回结果数
为什么会出现其他的值不同的d?因为在网页加载的过程中,并不之加载了歌曲的相关信息,还有一些如评论之类的其他信息,此时传入的d值是不一样的。因此,当遇到多参数时,要根据实际,多次尝试找到真实的参数。
接下来处理一下加密的算法
首先查看变量i,是函数a的返回结果,看看a是什么东西
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的应该能看懂,不懂js的话,a函数是生成一个长度为a的随机字符串 其中,默认长度为16
通过python实现
1 def a(self,a=16): 2 """ 3 获取16位随机字符串 4 """ 5 words="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 6 result='' 7 for i in range(a): 8 result+=words[random.randint(0,len(words)-1)] 9 return result
运行结果

接下来查看b函数,encText的内容是b函数通过两次加密的结果。
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函数是一段aes加密的代码。
如何判断这是aes加密呢?很简单
其中
iv : 0102030405060708
关于这个aes加密的代码真的是搞得我很难受,用python怎么都写不出来,后来在网上翻了好久,总算翻到一个可行的,因此,我aes和rsa加密的代码都是直接拿的大佬的
通过python实现b函数
1 def b(self,data,key): 2 """ 3 AES 加密 4 """ 5 iv = b'0102030405060708' 6 pad = 16 - len(data) % 16 7 data = data + chr(2) * pad 8 9 aes = AES.new(key.encode(), AES.MODE_CBC, iv) 10 tmp = aes.encrypt(data.encode()) 11 result = base64.b64encode(tmp).decode() 12 return result
最后看到这一行代码
encSecKey的值是c函数的返回结果,看看C函数是个什么东西。
function c(a, b, c) { var d, e; return setMaxDigits(131), d = new RSAKeyPair(b,"",c), e = encryptedString(d, a) }
很明显,c函数是一个rsa加密的代码
通过python实现c函数
1 def c(self,a,b,c): 2 """ 3 RSA 加密 4 """ 5 a = a[::-1] 6 result = pow(int(hexlify(a.encode()), 16), int(b, 16), int(c, 16)) 7 return format(result, 'x').zfill(131)
3.1.3 搜索算法实现
由于我们获取搜索内容的主要目的是获取歌曲id,因此就创建一个名为GetId的类
1 class GetMusic: 2 def __init__(self,id): 3 self.id=id 4 self.url='https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=' 5 self.headers = { 6 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36', 7 'Referer': 'https://music.163.com/', 8 'Host': 'music.163.com', 9 } 10 def get(self): 11 try: 12 r = requests.post(self.url, headers=self.headers, data=self.getData(), timeout=5) 13 download_url=r.json()['data'][0]['url'] 14 return download_url 15 except: 16 return 17 def getData(self): 18 d={"ids":"[%s]"%self.id,"level":"standard","encodeType":"aac","csrf_token":""} 19 d=json.dumps(d) 20 e = '010001' 21 f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 22 g = '0CoJUm6Qyw8W8jud' 23 i = self.a() 24 params = self.b(d, g) 25 params = self.b(params, i) 26 encSecKey = self.c(i, e, f) 27 return {'params': params, 'encSecKey': encSecKey} 28 def a(self,a=16): 29 """ 30 获取16位随机字符串 31 """ 32 words="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 33 result='' 34 for i in range(a): 35 result+=words[random.randint(0,len(words)-1)] 36 return result 37 def b(self,data,key): 38 """ 39 AES 加密 40 """ 41 iv = b'0102030405060708' 42 pad = 16 - len(data) % 16 43 data = data + chr(2) * pad 44 45 aes = AES.new(key.encode(), AES.MODE_CBC, iv) 46 tmp = aes.encrypt(data.encode()) 47 result = base64.b64encode(tmp).decode() 48 return result 49 def c(self,a,b,c): 50 """ 51 RSA 加密 52 """ 53 a = a[::-1] 54 result = pow(int(hexlify(a.encode()), 16), int(b, 16), int(c, 16)) 55 return format(result, 'x').zfill(131)
3.2 下载链接
3.2.1 抓取下载链接
随便打开一首歌,点击播放按钮,就会开始播放音乐
按下F12抓包,按下播放键,查看请求
发现在url为https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=的请求中看到了歌曲的详细信息,而其中
m4a格式?那不就是歌曲链接吗!把链接放到浏览器里,果然没错
查看该请求
URL : https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=
Method : POST
再看参数
又是这两个东西,这玩意熟啊,先前已经分析过了。
3.2.2 获取加密参数
前文已经提到,获取的信息不同,传入的参数d也是不同的,因此,在这里只需要知道参数d的内容即可
找到先前的js代码,打上断点,刷新调试
随着函数的几次运行,看到了这样的内容

在这里,看到了先前获取的音乐id
接下来就很简单了
3.2.3 代码实现
1 # -*- encoding = utf-8 -*- 2 # author : manlu 3 import base64 4 import json 5 import random 6 from binascii import hexlify 7 8 import requests 9 from Crypto.Cipher import AES 10 11 12 13 class GetMusic: 14 def __init__(self,id): 15 self.id=id 16 self.url='https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=' 17 self.headers = { 18 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36', 19 'Referer': 'https://music.163.com/', 20 'Host': 'music.163.com', 21 } 22 def get(self): 23 try: 24 r = requests.post(self.url, headers=self.headers, data=self.getData(), timeout=5) 25 download_url=r.json()['data'][0]['url'] 26 return download_url 27 except: 28 return 29 def getData(self): 30 d={"ids":"[%s]"%self.id,"level":"standard","encodeType":"aac","csrf_token":""} 31 d=json.dumps(d) 32 e = '010001' 33 f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 34 g = '0CoJUm6Qyw8W8jud' 35 i = self.a() 36 params = self.b(d, g) 37 params = self.b(params, i) 38 encSecKey = self.c(i, e, f) 39 return {'params': params, 'encSecKey': encSecKey} 40 def a(self,a=16): 41 """ 42 获取16位随机字符串 43 """ 44 words="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 45 result='' 46 for i in range(a): 47 result+=words[random.randint(0,len(words)-1)] 48 return result 49 def b(self,data,key): 50 """ 51 AES 加密 52 """ 53 iv = b'0102030405060708' 54 pad = 16 - len(data) % 16 55 data = data + chr(2) * pad 56 57 aes = AES.new(key.encode(), AES.MODE_CBC, iv) 58 tmp = aes.encrypt(data.encode()) 59 result = base64.b64encode(tmp).decode() 60 return result 61 def c(self,a,b,c): 62 """ 63 RSA 加密 64 """ 65 a = a[::-1] 66 result = pow(int(hexlify(a.encode()), 16), int(b, 16), int(c, 16)) 67 return format(result, 'x').zfill(131) 68 if __name__ == '__main__': 69 get=GetMusic('22605222') 70 print(get.get())
`
4.总结
这次js逆向难度不算太大,但实在是花了我很久。
本人水平有限,不妥之处请指出

浙公网安备 33010602011771号