【7】实战:爬取网易云音乐歌曲对应id并剔除无版权歌曲

目录

1. 在https://music.163.com/search中搜索林俊杰可得到下图结果:

2. 我们查看源代码,并对照浏览器渲染后的代码,可以发现歌曲是后续加载的:

3. 查看XHR与JS文件,确定数据包来源:

4. 找到构造请求的方法:

5. 找到d函数的参数:

6. 构造请求:

7. 剔除无版权歌曲

8. 运行测试


我们的如果想要播放网易云音乐上的音频,只需拿到其mp3的url即可,对应外链的url格式如下:(完整格式可从网上搜索,后缀+ ‘.mp3‘)

*/song/media/outer/url?id=

其中,id为音乐列表对应编号。

故若要根据用户的要求播放指定的音频,就要找到指定音频的对应id

由此,我们的分析步骤如下:

1. 在https://music.163.com/search中搜索林俊杰可得到下图结果:

2. 我们查看源代码,并对照浏览器渲染后的代码,可以发现歌曲是后续加载的:

浏览器渲染代码:

网页源代码:

3. 查看XHR与JS文件,确定数据包来源:

最终来源为https://music.163.com/weapi/cloudsearch/get/web?csrf_token=,该请求的返回信息包含搜索到的歌曲与id的对应关系:

通过查看该url的请求头部,可知其请求方式为POST,且有特定的请求参数params与encSecKey:

故而若我们要请求到目标数据,需求构造特定的请求头与请求参数

4. 找到构造请求的方法:

由于该请求是通过AJAX技术发送的,即是由客户机发送的,故而构造的方法一定在服务器发送回的某个脚本的函数中,通过查找关键字SecKey,我们找到了位于https://s3.music.126.net/web/s/core_c24658b266780ad771d7dff4e097e475.js?c24658b266780ad771d7dff4e097e475中(返回的js文件中的第一个或其他core开头的js文件)的一段生成h.encSecKey的方法:

首先可知该段是一段立即执行函数,格式化该段代码后可知该段代码主要有四个函数(函数中调用的有关密码学的函数均在同一js文件中有所定义):

a. 函数a的作用即生成指定字节的随机数:

b. 函数b的作用为AES加密:

c. 函数c的作用为RSA加密:

d. 函数d为主要逻辑控制函数,生成params与encSecKey:

除以上函数之外,该立即执行函数中还设置了两个参数分别指向函数d与函数e:

总的而言,由以上四个函数,尤其是函数d,我们可以总结出网易云音乐生成params与encSecKey的主要逻辑以及服务器解析的主要逻辑如下图所示(d为要加密的文本,AES的初始密钥为g,f为RSA的大模数,e为RSA的公钥,i为生成的随机数):

故而如果我们需要知道上述d函数的参数,才能得到特定的请求参数。

5. 找到d函数的参数:

由于该段是一个立即执行的函数,那么我们可以通过调试该js文件即可找到d函数的参数,故而我们直接监控该文件,在第88行下断点,查看其调用栈:

第一次直接运行,可以看到运行到88行时该d函数已经调用了a函数,而d函数是由v8n.bl9c调用,我们点击d函数,查看下面的调用信息:

由此,我们便拿到了第一次运行到d函数的4个调用参数。

我们点击继续运行,发现网页还没有显示歌单,且会第二次运行到88行,此时d函数被第二次调用:

可以看到e、f、g三个参数均没有发生改变,而d发生了改变;

我们点击继续运行,发现网页依然没有显示歌单,且会第三次运行到88行,此时d函数被第三次调用:

嗯,歌单依然没有出来,但我们现在基本可以确认e、f、g三个参数是固定的,我们继续运行,直到第七次调用

可以看到,d参数中出现了不一样的数据,我们查找的内容即林俊杰,对应参数中的keyword,我们继续运行,但歌单依然没有刷新……所以继续运行,到了第八次调用

好,这一次依然是有关键信息的,如id、s与type,均与我们的查找参数对应:

故而我们单步调试该次调用,记录该次生成的params与encSecKey:

点击继续运行后,我们可以在Network中可以看到其对目标发出了请求:

但是歌单信息还是没有请求到,继续运行:

再次继续运行,发现歌单终于出来了,且请求参数与先前不同:

故我们可以确定的是整个调用过程共请求了目标

https://music.163.com/weapi/cloudsearch/get/web?csrf_token=

两次,且两次参数并不相同,故而后续我们以上述两次请求的参数来构造请求,以验证能否请求得到信息。

但是,在实际测试中,只有第一次构造的参数才能够请求到对应的数据。

6. 构造请求:

如以上分析,我们只需构造类似于a,b,c,d四个函数的相应操作,即按照此前分析的基本请求头构造流程来构造请求即可:

a. 生成随机数,即相当于a函数,由于我们的调试中可以知道a函数的调用参数为16,故而我们直接默认生成16字节的随机数即可:

b. AES加密,相当于b函数,其中需要对text与key进行padding,padding的规则在原JS代码中的parse代码中定义:

c. RSA加密,相当于c函数:

d. 主逻辑控制函数,相当于d函数:

              在这些函数的基础上,我们还需要构造请求头,初始化参数信息等,在此不在赘述。

7. 剔除无版权歌曲

此外,我们还需要判断网易云音乐中的版权问题,如一些歌曲需要vip权限或者网易云音乐没有响应的版权,整个过程相对比较复杂枯燥,不再赘述,基本思路就是单步调试找到js渲染的方法(如无版权是灰色而不是黑色):

       我们也并没有完全将其解析方式进行完全剖析,由于原js代码经过了混淆,可读性较差,我们粗略分析判断如下:

       随后在收到的数据包进行一层过滤即可:

感兴趣的朋友可以自行检测,可以在原js代码中(与上述同一个)以fee为关键词找到相应的处理过程,或以compareFee为关键字搜索也可以,处理的基本过程就在该函数的后面的一段代码中。

8. 运行测试

python版本:3.6.4

平台:windows 10

依赖库:pycrptodome

具体代码如下:

import requests
import random
import base64
from Crypto.Cipher import AES
import json
import binascii

class Music_api():
    # 设置从JS文件提取的RSA的模数、协商的AES对称密钥、RSA的公钥等重要信息
    def __init__(self):
        self.modulus = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
        self.nonce = '0CoJUm6Qyw8W8jud'
        self.pubKey = '010001'
        self.url = "https://music.163.com/weapi/cloudsearch/get/web?csrf_token="
        self.HEADER = {}
        self.setHeader()
        self.secKey = self.getRandom()

    # 生成16字节即256位的随机数
    def getRandom(self):
        string = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        res = ""
        for i in range(16):
            res += string[int(random.random()*62)]
        return res

    # AES加密,用seckey对text加密
    def aesEncrypt(self, text, secKey):
        pad = 16 - len(text) % 16
        text = text + pad * chr(pad)
        encryptor = AES.new(secKey.encode('utf-8'), 2, '0102030405060708'.encode('utf-8'))
        ciphertext = encryptor.encrypt(text.encode('utf-8'))
        ciphertext = base64.b64encode(ciphertext).decode("utf-8")
        return ciphertext

    # 快速模幂运算,求 x^y mod mo 
    def quickpow(self, x, y, mo):
        res = 1
        while y:
            if y & 1:
                res = res * x % mo
            y = y // 2
            x = x * x % mo
        return res 

    # rsa加密
    def rsaEncrypt(self, text, pubKey, modulus):
        text = text[::-1]
        a = int(binascii.hexlify(str.encode(text)), 16)
        b = int(pubKey, 16)
        c = int(modulus, 16)
        rs = self.quickpow(a, b, c)
        return format(rs, 'x').zfill(256)

    # 设置请求头
    def setHeader(self):
        self.HEADER = {
            'Accept': '*/*',
            'Accept-Encoding': 'gzip,deflate,sdch',
            'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
            'Connection': 'keep-alive',
            'Content-Type': 'application/x-www-form-urlencoded',
            'Host': 'music.163.com',
            'Referer': 'https://music.163.com/search/',
            'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36'
        }

    # 设置相应的请求参数,从而搜索列表
    # 总体的密码加密步骤为:
    # 首先用nonce对text加密生成密文1
    # 然后用随机数seckey加密密文1生成密文2
    # 随后,用公钥加密seckey生成密文3
    # 其中,密文2作为请求参数中的params,密文3作为encSeckey字段
    # 这样,接收方可以通过私钥解密密文3获得seckey(随机数)
    # 然后用seckey解密密文2获得密文1
    # 最终用统一协商的密钥nonce解密密文1最终获得text
    def search(self, s,offset,type="1"):
        text = {"hlpretag": "<span class=\"s-fc7\">",
            "hlposttag": "</span>",
            "#/discover": "",
            "s": s,
            "type": type,
            "offset": offset,
            "total": "true",
            "limit": "30",
            "csrf_token": ""}
        text = json.dumps(text)
        params = self.aesEncrypt(self.aesEncrypt(text,self.nonce),self.secKey)
        encSecKey = self.rsaEncrypt(self.secKey,self.pubKey,self.modulus)
        data = {
            'params': params,
            'encSecKey': encSecKey
        }
        result = requests.post(url=self.url,
                                data=data,
                                headers = self.HEADER).json()
        return result

    # 获取指定音乐列表(相当于主函数)
    def get_music_list(self, keywords):
        music_list = []
        for offset in range(1):
            result = Music_api().search(keywords, str(offset))
            result = result['result']['songs']
            for music in result:
                # if music['copyright'] == 1 and music['fee'] == 8:
               if (music['privilege']['fee'] == 0 or music['privilege']['payed']) and music['privilege']['pl'] > 0 and music['privilege']['dl'] == 0:
                    continue
                if music['privilege']['dl'] == 0 and music['privilege']['pl'] == 0:
                    continue
                # if music['fee'] == 8:
                music_list.append(music)
        return music_list
print(Music_api().get_music_list("像我这样的人"))#测试

测试结果:

[{'name': '像我这样的人', 'id': 569213220, 'pst': 0, 't': 0, 'ar': [{'id': 12138269, 'name': '毛不易', 'tns': [], 'alias': []}], 'alia': [], 'pop': 100.0, 'st': 0, 'rt': None, 'fee': 8, 'v': 97, 'crbt': None, 'cf': '', 'al': {'id': 39483040, 'name': '平凡的一天', 'picUrl': 'http://p2.music.126.net/vmCcDvD1H04e9gm97xsCqg==/109951163350929740.jpg', 'tns': [], 'pic_str': '109951163350929740', 'pic': 109951163350929740}, 'dt': 207466, 'h': {'br': 320000, 'fid': 0, 'size': 8301758, 'vd': -2.0}, 'm': {'br': 192000, 'fid': 0, 'size': 4981072, 'vd': -2.0}, 'l': {'br': 128000, 'fid': 0, 'size': 3320729, 'vd': -2.0}, 'a': None, 'cd': '2', 'no': 1, 'rtUrl': None, 'ftype': 0, 'rtUrls': [], 'djId': 0, 'copyright': 2, 's_id': 0, 'rtype': 0, 'rurl': None, 'mst': 9, 'cp': 755014, 'mv': 5959041, 'publishTime': 1530547200007, 'privilege': {'id': 569213220, 'fee': 8, 'payed': 0, 'st': 0, 'pl': 128000, 'dl': 0, 'sp': 7, 'cp': 1, 'subp': 1, 'cs': False, 'maxbr': 999000, 'fl': 128000, 'toast': False, 'flag': 68}}, {'name': '像我这样的人 (伴奏)', 'id': 569212215, 'pst': 0, 't': 0, 'ar': [{'id': 12138269, 'name': '毛不易', 'tns': [], 'alias': []}], 'alia': [], 'pop': 90.0, 'st': 0, 'rt': None, 'fee': 8, 'v': 93, 'crbt': None, 'cf': '', 'al': {'id': 39483040, 'name': '平凡的一天', 'picUrl': 'http://p2.music.126.net/vmCcDvD1H04e9gm97xsCqg==/109951163350929740.jpg', 'tns': [], 'pic_str': '109951163350929740', 'pic': 109951163350929740}, 'dt': 211818, 'h': {'br': 320000, 'fid': 0, 'size': 8475211, 'vd': 329.0}, 'm': {'br': 192000, 'fid': 0, 'size': 5085144, 'vd': 1069.0}, 'l': {'br': 128000, 'fid': 0, 'size': 3390111, 'vd': -2.0}, 'a': None, 'cd': '3', 'no': 9, 'rtUrl': None, 'ftype': 0, 'rtUrls': [], 'djId': 0, 'copyright': 2, 's_id': 0, 'rtype': 0, 'rurl': None, 'mst': 9, 'cp': 755014, 'mv': 0, 'publishTime': 1530547200007, 'privilege': {'id': 569212215, 'fee': 8, 'payed': 0, 'st': 0, 'pl': 128000, 'dl': 0, 'sp': 7, 'cp': 1, 'subp': 1, 'cs': False, 'maxbr': 999000, 'fl': 128000, 'toast': False, 'flag': 68}}, {'name': '나 같은 놈', 'id': 26087209, 'pst': 0, 't': 0, 'ar': [{'id': 236161, 'name': '100%', 'tns': [], 'alias': []}], 'alia': [], 'pop': 55.0, 'st': 0, 'rt': '', 'fee': 8, 'v': 28, 'crbt': None, 'cf': '', 'al': {'id': 2382015, 'name': '나 같은 놈', 'picUrl': 'http://p2.music.126.net/8qmCC45WMwx7McnWpcE6KQ==/6667438510932879.jpg', 'tns': [], 'pic': 6667438510932879}, 'dt': 205113, 'h': {'br': 320000, 'fid': 0, 'size': 8233530, 'vd': -3.23}, 'm': {'br': 160000, 'fid': 0, 'size': 4134874, 'vd': -2.81}, 'l': {'br': 96000, 'fid': 0, 'size': 2495011, 'vd': -2.87}, 'a': None, 'cd': '1', 'no': 1, 'rtUrl': None, 'ftype': 0, 'rtUrls': [], 'djId': 0, 'copyright': 1, 's_id': 0, 'rtype': 0, 'rurl': None, 'mst': 9, 'cp': 1410822, 'mv': 10800452, 'publishTime': 1354809600007, 'tns': ['像我这样的人'], 'privilege': {'id': 26087209, 'fee': 8, 'payed': 0, 'st': 0, 'pl': 128000, 'dl': 0, 'sp': 7, 'cp': 1, 'subp': 1, 'cs': False, 'maxbr': 320000, 'fl': 128000, 'toast': False, 'flag': 68}}, {'name': '像我这样的人 (伴奏) ', 'id': 1302084496, 'pst': 0, 't': 0, 'ar': [{'id': 12228050, 'name': 'Mc山迪', 'tns': [], 'alias': []}], 'alia': [], 'pop': 25.0, 'st': 0, 'rt': None, 'fee': 8, 'v': 5, 'crbt': None, 'cf': '', 'al': {'id': 72302966, 'name': '像我这样的人', 'picUrl': 'http://p2.music.126.net/dfMB1bIaESNgUck6effkMg==/109951163469679983.jpg', 'tns': [], 'pic_str': '109951163469679983', 'pic': 109951163469679983}, 'dt': 170631, 'h': {'br': 320000, 'fid': 0, 'size': 6827407, 'vd': 10643.0}, 'm': {'br': 192000, 'fid': 0, 'size': 4096462, 'vd': 11856.0}, 'l': {'br': 128000, 'fid': 0, 'size': 2730989, 'vd': 13092.0}, 'a': None, 'cd': '01', 'no': 0, 'rtUrl': None, 'ftype': 0, 'rtUrls': [], 'djId': 0, 'copyright': 0, 's_id': 0, 'rtype': 0, 'rurl': None, 'mst': 9, 'cp': 700012, 'mv': 0, 'publishTime': 1534694400007, 'privilege': {'id': 1302084496, 'fee': 8, 'payed': 0, 'st': 0, 'pl': 128000, 'dl': 0, 'sp': 7, 'cp': 1, 'subp': 1, 'cs': False, 'maxbr': 999000, 'fl': 128000, 'toast': False, 'flag': 256}}, {'name': '再见你个贱人(改编自《像我这样的人》)(Cover 毛不易)', 'id': 521753439, 'pst': 0, 't': 0, 'ar': [{'id': 4513, 'name': '馒头', 'tns': [], 'alias': []}], 'alia': [], 'pop': 25.0, 'st': 0, 'rt': None, 'fee': 8, 'v': 20, 'crbt': None, 'cf': '', 'al': {'id': 36867953, 'name': '再见你个贱人', 'picUrl': 'http://p2.music.126.net/nhtD8Wnke2AhR2EOuTmJKw==/109951163073850161.jpg', 'tns': [], 'pic_str': '109951163073850161', 'pic': 109951163073850161}, 'dt': 84071, 'h': {'br': 320000, 'fid': 0, 'size': 3367750, 'vd': -31500.0}, 'm': {'br': 192000, 'fid': 0, 'size': 2020667, 'vd': -28900.0}, 'l': {'br': 128000, 'fid': 0, 'size': 1347126, 'vd': -27200.0}, 'a': None, 'cd': '01', 'no': 1, 'rtUrl': None, 'ftype': 0, 'rtUrls': [], 'djId': 0, 'copyright': 0, 's_id': 0, 'rtype': 0, 'rurl': None, 'mst': 9, 'cp': 1415877, 'mv': 0, 'publishTime': 1512102811362, 'privilege': {'id': 521753439, 'fee': 8, 'payed': 0, 'st': 0, 'pl': 128000, 'dl': 0, 'sp': 7, 'cp': 1, 'subp': 1, 'cs': False, 'maxbr': 320000, 'fl': 128000, 'toast': False, 'flag': 2}}, {'name': '像我这样的人', 'id': 1300452511, 'pst': 0, 't': 0, 'ar': [{'id': 28390581, 'name': '帅帅宏', 'tns': [], 'alias': []}], 'alia': [], 'pop': 25.0, 'st': 0, 'rt': None, 'fee': 8, 'v': 3, 'crbt': None, 'cf': '', 'al': {'id': 72071459, 'name': '像我这样的人', 'picUrl': 'http://p2.music.126.net/3AZyp5Njcv3wjGBvULQFWQ==/109951163456667681.jpg', 'tns': [], 'pic_str': '109951163456667681', 'pic': 109951163456667681}, 'dt': 173871, 'h': {'br': 320000, 'fid': 0, 'size': 6956974, 'vd': 0.0}, 'm': {'br': 192000, 'fid': 0, 'size': 4174202, 'vd': 0.0}, 'l': {'br': 128000, 'fid': 0, 'size': 2782816, 'vd': 0.0}, 'a': None, 'cd': '', 'no': 0, 'rtUrl': None, 'ftype': 0, 'rtUrls': [], 'djId': 0, 'copyright': 0, 's_id': 0, 'rtype': 0, 'rurl': None, 'mst': 9, 'cp': 700012, 'mv': 0, 'publishTime': 1534089600007, 'privilege': {'id': 1300452511, 'fee': 8, 'payed': 0, 'st': 0, 'pl': 128000, 'dl': 0, 'sp': 7, 'cp': 1, 'subp': 1, 'cs': False, 'maxbr': 999000, 'fl': 128000, 'toast': False, 'flag': 256}}, {'name': '像我这样的人', 'id': 864255570, 'pst': 0, 't': 0, 'ar': [{'id': 27693855, 'name': '曾泽佑', 'tns': [], 'alias': []}], 'alia': [], 'pop': 25.0, 'st': 0, 'rt': None, 'fee': 8, 'v': 5, 'crbt': None, 'cf': '', 'al': {'id': 71720089, 'name': '像我这样的人', 'picUrl': 'http://p2.music.126.net/RA4S9Ry7sQ5b0jpLCoVWSg==/109951163416450553.jpg', 'tns': [], 'pic_str': '109951163416450553', 'pic': 109951163416450553}, 'dt': 170251, 'h': None, 'm': None, 'l': {'br': 128000, 'fid': 0, 'size': 2725137, 'vd': 1.0}, 'a': None, 'cd': '1', 'no': 1, 'rtUrl': None, 'ftype': 0, 'rtUrls': [], 'djId': 0, 'copyright': 0, 's_id': 0, 'rtype': 0, 'rurl': None, 'mst': 9, 'cp': 700012, 'mv': 0, 'publishTime': 1532016000007, 'privilege': {'id': 864255570, 'fee': 8, 'payed': 0, 'st': 0, 'pl': 128000, 'dl': 0, 'sp': 7, 'cp': 1, 'subp': 1, 'cs': False, 'maxbr': 128000, 'fl': 128000, 'toast': False, 'flag': 256}}, {'name': '像我这样的人', 'id': 863515299, 'pst': 0, 't': 0, 'ar': [{'id': 12321026, 'name': '余龙', 'tns': [], 'alias': []}], 'alia': [], 'pop': 5.0, 'st': 0, 'rt': None, 'fee': 8, 'v': 6, 'crbt': None, 'cf': '', 'al': {'id': 71723689, 'name': '像我这样的人', 'picUrl': 'http://p2.music.126.net/3Z_tvBVBq-BLErYkS73wwg==/109951163446696436.jpg', 'tns': [], 'pic_str': '109951163446696436', 'pic': 109951163446696436}, 'dt': 171781, 'h': {'br': 320000, 'fid': 0, 'size': 6873382, 'vd': -66400.0}, 'm': {'br': 192000, 'fid': 0, 'size': 4124047, 'vd': -63800.0}, 'l': {'br': 128000, 'fid': 0, 'size': 2749379, 'vd': -62100.0}, 'a': None, 'cd': '01', 'no': 0, 'rtUrl': None, 'ftype': 0, 'rtUrls': [], 'djId': 0, 'copyright': 0, 's_id': 0, 'rtype': 0, 'rurl': None, 'mst': 9, 'cp': 700012, 'mv': 0, 'publishTime': 1533571200007, 'privilege': {'id': 863515299, 'fee': 8, 'payed': 0, 'st': 0, 'pl': 128000, 'dl': 0, 'sp': 7, 'cp': 1, 'subp': 1, 'cs': False, 'maxbr': 999000, 'fl': 128000, 'toast': False, 'flag': 256}}]

 

  • 18
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值