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是个什么东西?

很明显,就在上一行,bUM2xwindow.asrsea函数的一个返回值

 

在这几行打上断点,刷新一下网页

 

在这里插入图片描述

在这里插入图片描述

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

 

在这里插入图片描述 在这里插入图片描述

 

3.1.2 js逆向分析

看到d函数中返回了encText和encSecKey。这两个东西就是我们所需的参数。

其中,ia函数的返回结果,接下来,用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 }();
View Code

 

先查看参数的内容。在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:https://www.cnblogs.com/luop/p/4334160.html

如何判断这是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逆向难度不算太大,但实在是花了我很久。

本人水平有限,不妥之处请指出

posted @ 2021-12-16 23:48  Manlu  阅读(360)  评论(1)    收藏  举报