**1、**首先我们打开歌单内的任意一首歌曲,在该页面下打开chrome的开发者工具后切换到Network后重新刷新页面,找到请求到该歌曲播放源的URL,如下图:
找到后切换到Headers,可以看到其为一个post请求,URL为:‘https://music.163.com/weapi/song/enhance/player/url?csrf_token=’ ,从上图可知这个URL返回的是json,里面包含的是歌曲的信息及其MP3播放链接,请求时携带的参数为params及encSecKey,为了找到该歌曲播放源的URL,我们需解密params及encSecKey。
**2、**以encSecKey为关键词进行全局搜索,找到加密该参数的文件,如下图:
接着找到该参数是如何生成的,从下图可以看到其与bYd2x有关,为了得到该encSecKey我们需知道bYd2x这个值是如何生成的,所以我们按下图的操作设置断点,然后重新刷新页面进行调试。
按上图操作后,我们可以看到bYd2x这个函数中有四个参数,其中i8a与我们的歌曲id有关,另外三个参数我们也需要找出来,通过多次调试后可以看到其返回的值其实都是固定值,调试结果如下图:
通过上面分析得知其需要的参数值后,我们需找到window.asrsea这个函数是如何执行的,这样才能从其返回值里得到encText(即params)和encSecKey。在调试模式下我们按下图将鼠标悬浮到这个函数上可知它其实就是d函数,因此我们需找到d函数的定义。
**3、**按上图将鼠标悬浮到这个函数上后我们点击悬浮框上的蓝色字体可以到达定义d函数的位置,如下图所示:
从上图可知d函数与a函数,b函数及c函数有关,且excText的值由b函数加密了两次,因此我们要分别找到这些函数的定义并写成python语言,同样的我们在var h={}这行打上断点后重新刷新页面。首先我们找到a函数,其定义如下图:
可以看到它其实是在一堆字符串中随机找出16个字符串,将其写成python为:
get_i=execjs.compile(r"""
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
}
""")
i=get_i.call('a',16)
接着调试到b函数,其定义如下图:
从上图b 函数中的f可知其采用了 AES 加密,加密模式 为CBC ,加密的密文是 e 【其实就是我们上面说的i8a】,c是密钥【其实就是buU9L([“爱心”, “女孩”, “惊恐”, “大笑”])的返回值,是一个常量】,偏移量 d是一个常量为 ‘0102030405060708’,将其写成python语言为:
def to_16(key):
while len(key) % 16 != 0:
key += '\0'
return str.encode(key)
def AES_encrypt(text, key, iv):#text为密文,key为公钥,iv为偏移量
bs = AES.block_size
pad2 = lambda s: s + (bs - len(s) % bs) * chr(bs - len(s) % bs)
encryptor = AES.new(to_16(key), AES.MODE_CBC,to_16(iv))
encrypt_aes = encryptor.encrypt(str.encode(pad2(text)))
encrypt_text = str(base64.encodebytes(encrypt_aes), encoding='utf-8')
return encrypt_text
而excText的值(即params)是由b函数加密了两次的,传入的参数为i8a(即密文)及【buU9L([“爱心”, “女孩”, “惊恐”, “大笑”])】的返回值(即公钥),因此定义得到params的函数:
g='0CoJUm6Qyw8W8jud'
def get_params(id,g):#id为歌曲id
iv="0102030405060708"
i=get_i.call('a',16)
encText=str({'ids': "[" + str(id) + "]", 'br': 128000, 'csrf_token': ""})#i8a
return AES_encrypt(AES_encrypt(encText,g, iv), i, iv)
最后调试到c函数,其定义如下图:
由上图知c 函数是采用 RSA 加密,传入参数a是a函数生成的16位字符串,b 为【buU9L([“流泪”, “强”])的值】, c 为【buU9L(Rg4k.md)的值】,将其写成python语言为:
def RSA_encrypt(text, pubKey, modulus):
text=text[::-1]
rs=int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16)
return format(rs, 'x').zfill(256)
而encSecKey的值就是c函数的返回值,因此定义得到encSecKey的函数为:
i=get_i.call('a',16)
b="010001"
c='00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
def get_encSecKey(i, b, c):
return RSA_encrypt(i, b, c)
至此,我们就全部解析完d函数,并且也得到了请求’https://music.163.com/weapi/song/enhance/player/url?csrf_token=’ 这个URL时携带的参数为params及encSecKey,而这个post请求返回的json数据里包含了歌曲的信息及其MP3播放源链接,有了播放源链接我们就可以用urllib.request.retrieve这个请求方式直接将远程数据下载到本地。
**4、**综上分析可知我们只需有歌曲的id就能下载了,为此我们要提取所创建的歌单中所有歌曲的id。此处以歌单:https://music.163.com/playlist?id=2624438246 为例,点开该歌单后打开chrome的开发者工具,刷新后找到下面这个请求,看到这个请求的Response中并没有正常地显示数据,因此可知歌单内容是通过js动态渲染出来的。
对于这种情况,我们一般也会想到2种解决方案,使用selenium或者继续破解js,但还有一个更好的解决方法,就是使用requests-html这个库,之所以选这个库时因为其内置了pyppeteer这个库支持js渲染,且相对与selenium来说高效许多,我们通过下面这个代码先来看下请求后返回的页面源代码,然后再用xpath编写提取规则就能得到每首歌的id及歌名。
到此所有问题都以解决,最后奉上全部源码及运行结果:
import urllib.request,os,json
from lxml import etree
from requests_html import HTMLSession
import execjs,requests,random
import base64,codecs
from Crypto.Cipher import AES
def to_16(key):
while len(key) % 16 != 0:
key += '\0'
return str.encode(key)
def AES_encrypt(text, key, iv):
bs = AES.block_size
pad2 = lambda s: s + (bs - len(s) % bs) * chr(bs - len(s) % bs)
encryptor = AES.new(to_16(key), AES.MODE_CBC,to_16(iv))
encrypt_aes = encryptor.encrypt(str.encode(pad2(text)))
encrypt_text = str(base64.encodebytes(encrypt_aes), encoding='utf-8')
return encrypt_text
def RSA_encrypt(text, pubKey, modulus):
text=text[::-1]
rs=int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16)
return format(rs, 'x').zfill(256)
def set_user_agent():
USER_AGENTS = [
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
"Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
"Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
"Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
"Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5"
]
user_agent = random.choice(USER_AGENTS)
return user_agent
#获取i值的函数,即随机生成长度为16的字符串
get_i=execjs.compile(r"""
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
}
""")
class WanYiYun():
def __init__(self):
self.playlist_url='https://music.163.com/playlist?id=2624438246'#歌单地址
self.song_url='https://music.163.com/weapi/song/enhance/player/url?csrf_token='
self.g = '0CoJUm6Qyw8W8jud'#buU9L(["爱心", "女孩", "惊恐", "大笑"])的值
self.b = "010001"#buU9L(["流泪", "强"])的值
# buU9L(Rg4k.md)的值
self.c = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
self.i=get_i.call('a',16)#随机生成长度为16的字符串
self.iv = "0102030405060708" # 偏移量
if not os.path.exists("d:/music"):
os.mkdir('d:/music')
self.headers={ 'User-Agent':set_user_agent(),
'Referer':'https://music.163.com/',
'Content-Type':'application/x-www-form-urlencoded'
}
#由于歌单内容是通过JS生成的,所以此处运用requests_html这个库来实现JS渲染从而获得歌单
def get_music_list(self):
session = HTMLSession()
response = session.get(self.playlist_url)
html = etree.HTML(response.content.decode())
song_list = html.xpath("//ul[@class='f-hide']/li/a")
music_list = []
for song in song_list:
music_id = song.xpath('.//@href')
music_name = song.xpath('.//text()')
music_list.append({'id': music_id, 'name': music_name})
return music_list
def get_params(self,id):
encText=str({'ids': "[" + str(id) + "]", 'br': 128000, 'csrf_token': ""})
return AES_encrypt(AES_encrypt(encText,self.g, self.iv), self.i, self.iv)
def get_encSecKey(self):
return RSA_encrypt(self.i, self.b, self.c)
def download(self):
music_list=self.get_music_list()
for music in music_list:
music_id=music['id'][0].split('=')[1]
music_name=music['name'][0]
formdata={'params':self.get_params(music_id),
'encSecKey':self.get_encSecKey()}
response=requests.post(self.song_url, headers=self.headers, data=formdata)
download_url=json.loads(response.content)["data"][0]["url"]
if download_url:
try:
# 根据音乐url地址,用urllib.request.retrieve直接将远程数据下载到本地
urllib.request.urlretrieve(download_url, 'd:/music/' + music_name+ '.mp3')
print('Successfully Download:'+music_name+ '.mp3')
except:
print('Download wrong~')
if __name__ == '__main__':
wanyiyun=WanYiYun()
wanyiyun.download()