文章目录
- 介绍
- 下载链接
- Robots文件
- 搜索功能
- JS逆向
-
-
- **函数a:生成随机字符串**
- **函数b:AES-CBC加密**
- **函数c:RSA公钥加密**
-
- 歌曲下载
- 总结
介绍
在某易云音乐中,很多歌曲听是免费的,但下载需要VIP,此程序旨在"可听"则可下,只要能够听的歌,都可以下载下来。
而此篇博客则着重讲此代码是如何编写的,如果只想要下载工具,请自行查看下载链接。
注意,此程序仅做学习参考,任何违法行为与作者无关。
下载链接
GitHub:https://github.com/13337356453/163Music
CSDN:https://download.csdn.net/download/realmels/90855703
Robots文件
养成好习惯,爬虫之前,先看robots文件。
禁止爬取
/prime/m/gift-receive
文件,刚好,我们这次不爬取此文件。
搜索功能
在某易云音乐中提供了搜索功能,可以通过关键词搜索歌曲。
而我们的程序需要通过用户关键词来搜索内容,因此需要首先从搜索功能开始爬取。
在这里通过歌曲abc为例,进行搜索功能的分析与实现。
在搜索栏输入关键字abc ,按下回车键得到搜索结果。
按下F12 进入开发者工具 ,选中Network ,点击XHR查看Ajax的请求包
刷新界面,出现了很多请求包
逐个查看请求包的Response,寻找搜索歌曲的请求包。
在路径为
/weapi/cloudsearch/get/web
的请求包中,我们可以看到歌曲的搜索信息。

可以确定这个就是搜索歌曲的请求包。
接下来分析请求参数。

这是一个POST 请求,请求的URL为:https://music.163.com/weapi/cloudsearch/get/web
,POST的参数如下:
bash
params=NM406wjNCdicjbT3ZSuA7X6ICkCBzfN6rA1KAeZR4Pmpv6VhTT8jKqR/ZRpGitFCp77TipjrdwPahGWiMjGf2cLrREp5Gadgeseo9l9+IMfrwG/JmgDfW9pLZaRIagi+MSESFlTJnlH3vJI7YbqwWPjgfbyzBX0sgw4l3IXxfuamFggnztM3DlEhB1uCBUPqEpDFlsMW8pPdEPtaJS47Y2DMXBY/SJVOa8Y3nD/rjP2lhw5sXaZ3qs5LEwmYKiriEAaZ6fRmKJb4Vn6Ay6ELCBWL74DqjF4BPh8GsEPicXdah/nR0BPoM+suZbKCICMK
encSecKey=8b987300e7f1a5e657c7a69d3c9ff9a4d9ab1c3dc1b2328df473600103b1fe18aa4898008ac9d03074c2cb560543ac22740c4c0c1f7d2c14535f938344a999224733d97e75ffdc26dc2a7ac9afff302f127b29ee762ce02061b9ce89ad2b9006938ed0e62667bfbac656b12adbe951ccff0d02dacd27fb94de7473fa933043ec
别的都容易理解,这两个参数是什么呢?
我们从JS文件中找答案。
JS逆向
在这里先科普基本知识。一个网站,分为前端和后端,前端负责展现好看的页面,后端负责处理繁杂的数据。用户通过前端界面,将数据发送到后端,后端代码处理数据,将相应的结果发送到前端,呈现在用户眼前。这个过程通过request /response,实现。因此,前端所有奇形怪状的参数,都可以在前端代码中找到蛛丝马迹。毕竟,后端代码是不可见的,而前端代码是公开的。
首先需要明白params 和encSecKey 这两个参数是如何产生的,这里我们复制encSecKey ,在开发者工具中选中Sources ,按下Ctrl+Shift+F 进行全局搜索。
在这里我们逐个进行分析,首先打开这个core 开头的JS文件,顾名思义,core 就是核心,因此最可能有我们想要的数据。
打开文件后按下Ctrl+F 进行文件内搜索,搜索encSecKey ,查询到三个结果。
我们分别进行分析。
在第二次出现的地方,我们需要的两个参数:params 和encsecKey 同时出现
怀疑这里有参数的生成逻辑,打一个断点。
刷新页面,程序运行到断点处停止。
可以看到,params 和encsecKey 这两个参数分别是bYE4I 这个对象的两个属性。而bYE4I 这个对象是由window.asrsea这个函数生成的。
我们把光标放到window.asrsea 这个函数上,查看这个函数的真实面目。
跳转到函数d
我们可以看到,函数d 中调用了函数a ,b ,c 。正好这三个参数都在d函数的附近,我们一起复制过来查看。
javascript
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
}
我们在d函数中打上断点,分析d 函数的参数
一直点击上方的下一步,同时眼睛观察着右边的实时参数值。
多走几步,我们会发现函数d 接受的四个参数中,e ,f ,g都是固定值,值如下:
javascript
e=010001
f=00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
g=0CoJUm6Qyw8W8jud
而参数d的值却一直在变化。
我们一直点下一步 的按钮,同时盯着参数,点着点着,会发现右侧的d参数 发生变化,大多数时候都是csrf_token的值,我们不用管,但偶尔会出现别的内容,而大概点了几十次后,参数会变成:
javascript
d="{\"hlpretag\":\"<span class=\\\"s-fc7\\\">\",\"hlposttag\":\"</span>\",\"s\":\"abc\",\"type\":\"1\",\"offset\":\"0\",\"total\":\"true\",\"limit\":\"30\",\"csrf_token\":\"\"}"
在其中,出现了我们的关键词abc ,以及出现歌曲的数量30
确定d函数就是生成参数的逻辑。
接下来仔细分析d函数。
javascript
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
}
首先定义了一个空对象h ,然后通过a 函数生成内容,赋值给i ,接下来通过b 函数生成第一个encText ,赋给h 的属性,接受参数d 和g ,再用b 函数二次处理encText ,参数为第一次处理的encText 和i ,这就是提交时所需的params ;同时用c 函数生成encsecKey ,接受参数i ,e ,f
接下来我们逐个分析a ,b ,c函数
javascript
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)
}
人与动物的区别就是人会使用工具,我们直接问deepseek
这是deepseek的回答。
函数a:生成随机字符串
javascript
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
控制字符串长度。 - 实现细节 :
- 循环
a
次,每次从字符池b
中随机选取一个字符。 - 使用
Math.random()
生成随机索引,可能不够安全(非密码学安全随机数)。
- 循环
- 安全问题 :
Math.random()
在浏览器中可能被预测,建议改用crypto.getRandomValues()
。
- 用途:生成临时密钥、Nonce 或盐值。
函数b:AES-CBC加密
javascript
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()
}
- 功能:使用 AES-CBC 模式加密数据,密钥由参数提供。
- 参数 :
a
:明文数据(UTF-8 字符串)。b
:加密密钥(UTF-8 字符串)。
- 实现细节 :
- 固定初始化向量(IV)
0102030405060708
。 - 使用
CryptoJS
库进行加密,返回 Base64 编码的密文。
- 固定初始化向量(IV)
- 安全问题 :
- 固定IV:CBC 模式要求每次加密使用随机 IV,否则可能导致明文模式泄露。
- 密钥建议使用二进制格式(如 PBKDF2 派生),而非直接字符串。
- 用途:加密传输敏感数据。
函数c:RSA公钥加密
javascript
function c(a, b, c) {
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}
- 功能:使用 RSA 公钥加密数据。
- 参数 :
a
:明文数据(字符串或数值)。b
:公钥指数(通常为"10001"
十六进制)。c
:RSA 模数(n)。
- 实现细节 :
setMaxDigits(131)
设置大整数位数(支持最大 131 位,对应 1024 位 RSA)。RSAKeyPair
仅使用公钥指数和模数,私钥留空,表明这是公钥加密。encryptedString
可能是自定义函数,实现 RSA 加密逻辑。
- 安全问题 :
- 需确认是否使用正确填充(如 PKCS#1 v1.5 或 OAEP),默认可能不安全。
- 用途:加密对称密钥(如 AES 密钥)以便安全传输。
知道了函数的用途,我们开始编写Python代码。
写一个工具类,分别实现函数abcd的功能,名字为JS,定义四个静态方法:
python
import base64
import random
from binascii import hexlify
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
class JS:
@staticmethod
def d(d,e="010001",f="00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7",g="0CoJUm6Qyw8W8jud"):
'''生成数据'''
i=JS.a(16)
h_encText=JS.b(d,g)
encText=JS.b(h_encText,i)
encSecKey=JS.c(i,e,f)
return encText,encSecKey
@staticmethod
def a(a):
'''生成16位随机字符'''
b="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
c=""
for i in range(a):
e = random.randint(0, len(b) - 1)
c+=b[e]
return c
@staticmethod
def b(a,b):
'''AES加密'''
key = b.encode('utf-8')
iv = "0102030405060708".encode('utf-8')
data = a.encode('utf-8')
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(pad(data, AES.block_size))
return base64.b64encode(encrypted).decode('utf-8')
@staticmethod
def c(a,b,c):
'''RSA加密'''
a = a[::-1]
result = pow(int(hexlify(a.encode()), 16), int(b, 16), int(c, 16))
return format(result, 'x').zfill(131)
至于这几个函数是如何改写的。。其实很简单。
接下来的步骤就很简单了,编写搜索歌曲功能代码:
python
import requests
from urllib.parse import quote
from json import loads
from JS import JS
requests.packages.urllib3.disable_warnings()
class Searcher:
url="https://music.163.com/weapi/cloudsearch/get/web?csrf_token="
headers={
'Host': 'music.163.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
'Referer': 'https://music.163.com/search/',
'Content-Type': 'application/x-www-form-urlencoded',
}
jsonStr="{\"hlpretag\":\"<span class=\\\"s-fc7\\\">\",\"hlposttag\":\"</span>\",\"s\":\"%s\",\"type\":\"1\",\"offset\":\"0\",\"total\":\"true\",\"limit\":\"%d\",\"csrf_token\":\"\"}"
def __init__(self,keyword,proxies={},number=1,cookie=""):
self.keyword=keyword
self.proxies=proxies
self.number=number
self.headers['Cookie']=cookie
def getData(self):
s=self.jsonStr%(self.keyword,self.number)
params,encSecKey=JS.d(s)
data=f"""params={quote(params)}&encSecKey={quote(encSecKey)}"""
try:
r=requests.post(self.url,headers=self.headers,data=data,verify=False,timeout=10,proxies=self.proxies)
if r.status_code==200:
result=loads(r.content.decode())
return result['result']['songs']
return None
except Exception as e:
print(f'[!] {e}')
return None
注意注意,这里有个非常坑的点,就是在传输data中,数据还要进行URL加密,否则就会报400,参数错误。这个点害了我一晚上。
我们成功实现了通过关键字搜索功能,获取到歌曲的id ,name,这两个参数至关重要
歌曲下载
选中一首歌,我们打开详细页。
打开开发者工具抓包,同时点击播放 按钮
抓取到数据包后,我们逐个分析。
锁定到
/weapi/song/enhance/player/url/v1
这个请求包,在请求包中,我们可以看到歌曲的下载链接。
我们把这个链接复制到浏览器打开,成功播放了歌曲,确定就是下载链接。

接下来我们分析这个请求包的参数。
熟悉的params 和encsecKey,我们之前已经分析过了
使用同样方法打好断点,再次点击播放按钮,查看传入的参数。

这里出现了歌曲id,原始的参数为:
python
d="{\"ids\":\"[33004804]\",\"level\":\"exhigh\",\"encodeType\":\"aac\",\"csrf_token\":\"\"}"
于是我们可以编写相关Python代码:
python
from json import loads
from urllib.parse import quote
import requests
from JS import JS
requests.packages.urllib3.disable_warnings()
class Musicer:
url='https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
headers={
'Host': 'music.163.com',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36',
'Referer': 'https://music.163.com/search/',
'Content-Type': 'application/x-www-form-urlencoded',
}
jsonStr='{"ids":"[%s]","level":"exhigh","encodeType":"aac","csrf_token":""}'
def __init__(self,musicid,cookie,name,proxies={}):
self.musicid=str(musicid)
self.headers['Cookie']=cookie
self.proxies=proxies
self.name=name
def getDownloadUrl(self):
s=self.jsonStr%self.musicid
params,encSecKey=JS.d(s)
data=f"""params={quote(params)}&encSecKey={quote(encSecKey)}"""
try:
r=requests.post(self.url,headers=self.headers,data=data,verify=False,timeout=10,proxies=self.proxies)
if r.status_code==200:
return loads(r.content.decode())['data'][0]['url']
return None
except Exception as e:
print(e)
return None
def download(self):
url=self.getDownloadUrl()
try:
r=requests.get(url,proxies=self.proxies,verify=False,timeout=10)
if r.status_code==200:
with open(f'{self.name}.mp3','wb') as f:
f.write(r.content)
print(f"[+] {self.name}.mp3 已保存")
except Exception as e:
print(f'[!] {e}')
至此,本项目全部完成。
总结
本博客仅做学习参考,任何违法犯罪行为与本文作者无关。