python爬蟲爬取網易雲音樂(超詳細教學,附原始碼)

2022-12-06 09:01:51

一、 前言

先說結論,目前無法下載無失真音樂,也無法下載vip音樂。
此程式碼模擬web網頁js加密的過程,向api介面傳送引數並獲取資料,僅供參考學習,如果需要下載網易雲音樂,不如直接在使用者端下載,使用者端還可以下載無失真音樂。
程式碼還是半成品,打算再做個音樂播放器,直接打包成exe,等有時間做好了再傳到github上去,現在先把解析過程記錄下來釋出。
至於音樂搜尋器,我所知道的有一個,地址:https://iw233.cn/music/

上面這個網頁直接返回所有的搜尋結果和音樂資訊,如要使用,請自行解析(很簡單的一個頁面)。
網上流傳的網易雲音樂外連地址:http://music.163.com/song/media/outer/url?id=534544522.mp3,我也不知道怎麼來的,輸入音樂id即可獲得下載連結,本著學習的態度,我認為還是從頭到尾解析下載連結才好,因此不考慮使用該外連。
介面檔案已釋出
連結: https://25ukpfkme3.apifox.cn 存取密碼: qtlXaZPH

二、解析過程

1、音樂搜尋

(1)獲取連結

來到網易雲音樂首頁,輸入音樂名稱,得到搜尋結果

按F12,開啟開發者工具,重新重新整理一下介面,點選網路,在篩選器裡只篩選XHR和Fetch資料,點選預覽,一個一個連結往下找,直到找到我們需要的資料為止。

得到音樂搜尋的api介面:https://music.163.com/weapi/cloudsearch/get/web?csrf_token=

csrf_token只有你登入時才有,有沒有這個值不影響返回的結果。
需要注意,這個api介面的POST請求,後面所有的api介面都是POST請求

(2)分析引數

點選負載,檢視我們需要傳入哪些資料

可以看到我們需要傳入params和encSecKey兩個引數,才能獲取資料,否則得到的資料為空。
那麼如何獲取這兩個引數呢?點選發起程式,隨便點選一個,進入js指令碼介面

點下面的{},將js程式碼格式化,這樣我們更好檢視程式碼

在js頁面裡按ctrl+f,直接搜尋encSecKey,總共有三個結果,

可以看到,在執行window.asrsea()函數後,生成了params和encSecKey,
這裡我們貼一下js原始碼,後面還會用到

var bMr5w = window.asrsea(JSON.stringify(i8a), bsg1x(["流淚", "強"]), bsg1x(TH5M.md), bsg1x(["愛心", "女孩", "驚恐", "大笑"]));
            e8e.data = j8b.cr9i({
                params: bMr5w.encText,
                encSecKey: bMr5w.encSecKey
            })

那麼window.asrsea()是什麼呢,搜尋asrsea,可以看到window.asrsea()=d

找到d函數,就在window.asrsea()上面,
把js程式碼複製出來

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
    }

其共涉及到三個函數a,b,c。我們先不急研究這三個函數的作用,先檢視d函數傳進去的四個引數d,e,f,g是什麼。
其實從前面的window.asrsea()函數裡我們就知道,這四個引數分別為JSON.stringify(i8a), bsg1x(["流淚", "強"]), bsg1x(TH5M.md), bsg1x(["愛心", "女孩", "驚恐", "大笑"]),
在這行打一個斷點,重新重新整理一下介面,進入偵錯模式。
在控制檯依次輸入這四個引數,獲得其值

點選執行,繼續執行下一步,再次檢視四個引數的值,可以在監視裡面輸入四個引數,每次執行後會顯示引數的值

經過多次偵錯後我們發現,除了第一個引數有變化之外,後面三個引數都是固定的,window.asrsea()函數會將這四個引數傳入到d函數中,也就是d,e,f,g

e = '010001'
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
g = '0CoJUm6Qyw8W8jud'

那麼d引數是什麼呢?在d函數裡打個斷點,偵錯頁面直至進入我們的api介面https://music.163.com/weapi/cloudsearch/get/web?csrf_token=為止

Y8Q = Y8Q.replace("api", "weapi");

仔細檢視此段程式碼,這段程式碼是將連結裡的api替換成weapi,因此可以根據Y8Q來定位到當前的連結地址,然後進行下一步偵錯。
在該行打一個斷點,開始偵錯。
這裡有個偵錯的小技巧,我們可以先在Y8Q那一行打一個斷點,先進行偵錯執行,當Y8Q變為搜尋的api介面後,再在window.asrsea()那裡打一個斷點,然後在d函數打一個斷點,進行偵錯。
記得在Y8Q那裡偵錯時先重新整理一下頁面,但是後面偵錯不要

這裡我們定位到了api介面,注意api還沒有換成weapi,點選執行下一步後,api被替換了

然後在window.asrsea打一個斷點,點選執行

然後在d函數打一個斷點,點選執行,得到d的值

直接將d複製過來,注意字串前要加個r,要不然字串裡的''會被當做跳脫字元處理。

d = r'{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"自由の翅","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}'
e = '010001'
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
g = '0CoJUm6Qyw8W8jud'

可以看到,s裡面的就是我們搜尋的音樂名稱了,後面可以更改s的值來改變搜尋的音樂。

(3)函數分析

好了,講了這麼多,結果只分析出四個引數的值是什麼,接下來我們發現a,b,c,d這四個函數的作用。

a函數

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
    }

根據我十分粗糙的js知識來看,這個函數返回的是一個b中的隨機字串,函數接收字串的長度,我們將它改寫成Python程式碼。

# 獲取一個隨意字串,length是字串長度
def generate_str(lenght):
    str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    res = ''
    for i in range(lenght):
        index = random.random() * len(str)  # 獲取一個字串長度的亂數
        index = math.floor(index)  # 向下取整
        res = res + str[index]  # 累加成一個隨機字串
    return res

其實這個隨機字串完全可以定死,但為了儘量還原js指令碼的執行過程,我們還是直接照搬過來吧。

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加密過程,a是加密內容,也就是encText,b是一個key,是一個固定值,也就是上面四個引數中的g

g = '0CoJUm6Qyw8W8jud'

加密的模式為CBC,參照Python AES的加密過程,將js程式碼改寫成了Python程式碼

# AES加密獲得params
def AES_encrypt(text, key):
    iv = '0102030405060708'.encode('utf-8')  # iv偏移量
    text = text.encode('utf-8')  # 將明文轉換為utf-8格式
    pad = 16 - len(text) % 16
    text = text + (pad * chr(pad)).encode('utf-8')  # 明文需要轉成二進位制,且可以被16整除
    key = key.encode('utf-8')  # 將金鑰轉換為utf-8格式
    encryptor = AES.new(key, AES.MODE_CBC, iv)  # 建立一個AES物件
    encrypt_text = encryptor.encrypt(text)  # 加密
    encrypt_text = base64.b64encode(encrypt_text)  # base4編碼轉換為byte字串
    return encrypt_text.decode('utf-8')

c函數

    function c(a, b, c) {
        var d, e;
        return setMaxDigits(131),
        d = new RSAKeyPair(b,"",c),
        e = encryptedString(d, a)
    }

c函數是RSA加密過程,其中a是隨機字串,b是一個key,也就是上面四個引數中的e,f也是四個引數中的f,返回的是encSeckey

e = '010001'
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'

由於本人學習的js知識十分粗淺,因此難以看懂這段程式碼,參考網上流傳的版本,改寫了程式碼

# RSA加密獲得encSeckey
def RSA_encrypt(str, key, f):
    str = str[::-1]  # 隨機字串逆序排列
    str = bytes(str, 'utf-8')  # 將隨機字串轉換為byte型別的資料
    sec_key = int(codecs.encode(str, encoding='hex'), 16) ** int(key, 16) % int(f, 16)  # RSA加密
    return format(sec_key, 'x').zfill(256)  # RSA加密後字串長度為256,不足的補x

RSA加密規則不是很熟悉,感興趣的自行百度

d函數

    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函數了,i是一個16位元的隨機字串,可以定死,使用b函數先對d函數進行了AES加密,由於d,g都是固定值,所以得到的encText也是固定值,可以通過偵錯來獲得第一次加密後的encText,然後在執行一下你的Python程式碼,檢視encText是否一致,用來驗證d是否正確。
第一次加密得到encText,再次對encText進行第二次加密,不過key換成了隨機字串i,兩次加密後得到encText。

# 獲取引數
def get_params(d, e, f, g):
    i = generate_str(16)    # 生成一個16位元的隨機字串
    # i = 'aO6mqZksdJbqUygP'
    encText = AES_encrypt(d, g)
    # print(encText)    # 列印第一次加密的params,用於測試d正確
    params = AES_encrypt(encText, i)  # AES加密兩次後獲得params
    encSecKey = RSA_encrypt(i, e, f)  # RSA加密後獲得encSecKey
    return params, encSecKey

至此,引數params和encSecKey都解析完畢,由於字串是隨機的,因此每次執行後得到的params和encSecKey都不一樣。

(4)分析返回結果

知道引數和介面後,就可以向伺服器傳送請求,獲取返回結果了。注意請求為post請求
由於後續api的解析過程基本一致,因此將程式碼封裝起來。

e = '010001'
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
g = '0CoJUm6Qyw8W8jud'

# 傳入msg和url,獲取返回的json資料
def get_data(msg, url):
    encText, encSecKey = get_params(msg, e, f, g)   # 獲取引數
    params = {
        "params": encText,
        "encSecKey": encSecKey
    }
    re = requests.post(url=url, params=params, verify=False)    # 向伺服器傳送請求
    return re.json()    #返回結果

# 搜尋返回的資料
serch_msg = r'{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"自由の翅","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}'
serch_url = 'https://music.163.com/weapi/cloudsearch/get/web?csrf_token='
print(get_data(serch_msg, serch_url))

三個引數e,f,g固定不變,只更改msg的值。
注意msg字串前要加個r,防止編譯器將字串裡的\當做跳脫字元處理。
返回結果如下

{
    "needLogin": true,
    "result": {
        "searchQcReminder": null,
        "songs": [
            {
                "name": "自由の翅",
                "id": 473403600,
                "pst": 0,
                "t": 0,
                "ar": [
                    {
                        "id": 17672,
                        "name": "佐藤ひろ美",
                        "tns": [
                            "佐藤裕美"
                        ],
                        "alias": [
                            "さとう ひろみ",
                            "Sato Hiromi"
                        ],
                        "alia": [
                            "さとう ひろみ",
                            "Sato Hiromi"
                        ]
                    }
                ],
                "alia": [
                    "PCゲーム『月影のシミュラクル -解放の羽-』OPテーマ"
                ],
                "pop": 85,
                "st": 0,
                "rt": null,
                "fee": 0,
                "v": 12,
                "crbt": null,
                "cf": "",
                "al": {
                    "id": 35377102,
                    "name": "月影のシミュラクル -解放の羽- オリジナルサウンドトラック",
                    "picUrl": "http://p3.music.126.net/jUm6aclu8k5fNUdwEODz0w==/18598239185710248.jpg",
                    "tns": [],
                    "pic_str": "18598239185710248",
                    "pic": 18598239185710250
                },
                "dt": 276866,
                "h": {
                    "br": 320000,
                    "fid": 0,
                    "size": 11077007,
                    "vd": -79678,
                    "sr": 44100
                },
                "m": {
                    "br": 192000,
                    "fid": 0,
                    "size": 6646222,
                    "vd": -77200,
                    "sr": 44100
                },
                "l": {
                    "br": 128000,
                    "fid": 0,
                    "size": 4430829,
                    "vd": -76001,
                    "sr": 44100
                },
                "sq": {
                    "br": 1052522,
                    "fid": 0,
                    "size": 36426061,
                    "vd": -79658,
                    "sr": 44100
                },
                "hr": null,
                "a": null,
                "cd": "1",
                "no": 1,
                "rtUrl": null,
                "ftype": 0,
                "rtUrls": [],
                "djId": 0,
                "copyright": 0,
                "s_id": 0,
                "mark": 262144,
                "originCoverType": 0,
                "originSongSimpleData": null,
                "tagPicList": null,
                "resourceState": true,
                "version": 12,
                "songJumpInfo": null,
                "entertainmentTags": null,
                "single": 0,
                "noCopyrightRcmd": null,
                "rtype": 0,
                "rurl": null,
                "mst": 9,
                "cp": 663018,
                "mv": 0,
                "publishTime": 1485446400000,
                "tns": [
                    "自由的翅膀"
                ],
                "privilege": {
                    "id": 473403600,
                    "fee": 0,
                    "payed": 0,
                    "st": 0,
                    "pl": 320000,
                    "dl": 999000,
                    "sp": 7,
                    "cp": 1,
                    "subp": 1,
                    "cs": false,
                    "maxbr": 999000,
                    "fl": 320000,
                    "toast": false,
                    "flag": 256,
                    "preSell": false,
                    "playMaxbr": 999000,
                    "downloadMaxbr": 999000,
                    "maxBrLevel": "lossless",
                    "playMaxBrLevel": "lossless",
                    "downloadMaxBrLevel": "lossless",
                    "plLevel": "exhigh",
                    "dlLevel": "lossless",
                    "flLevel": "exhigh",
                    "rscl": null,
                    "freeTrialPrivilege": {
                        "resConsumable": false,
                        "userConsumable": false,
                        "listenType": null
                    },
                    "chargeInfoList": [
                        {
                            "rate": 128000,
                            "chargeUrl": null,
                            "chargeMessage": null,
                            "chargeType": 0
                        },
                        {
                            "rate": 192000,
                            "chargeUrl": null,
                            "chargeMessage": null,
                            "chargeType": 0
                        },
                        {
                            "rate": 320000,
                            "chargeUrl": null,
                            "chargeMessage": null,
                            "chargeType": 0
                        },
                        {
                            "rate": 999000,
                            "chargeUrl": null,
                            "chargeMessage": null,
                            "chargeType": 1
                        }
                    ]
                }
            },
            {
                "name": "自由の翅 (BEAST-Ⅵ Bootleg)",
                "id": 1946185953,
                "pst": 0,
                "t": 0,
                "ar": [
                    {
                        "id": 49024337,
                        "name": "Nero",
                        "tns": [],
                        "alias": []
                    }
                ],
                "alia": [],
                "pop": 5,
                "st": 0,
                "rt": "",
                "fee": 0,
                "v": 3,
                "crbt": null,
                "cf": "",
                "al": {
                    "id": 144732637,
                    "name": "HyperRave01",
                    "picUrl": "http://p3.music.126.net/C3YZ8fAg8TJ1pgTdlAbxnA==/109951167398067153.jpg",
                    "tns": [],
                    "pic_str": "109951167398067153",
                    "pic": 109951167398067150
                },
                "dt": 182987,
                "h": {
                    "br": 320000,
                    "fid": 0,
                    "size": 7321644,
                    "vd": -85382,
                    "sr": 44100
                },
                "m": {
                    "br": 192000,
                    "fid": 0,
                    "size": 4393004,
                    "vd": -83101,
                    "sr": 44100
                },
                "l": {
                    "br": 128000,
                    "fid": 0,
                    "size": 2928684,
                    "vd": -81941,
                    "sr": 44100
                },
                "sq": null,
                "hr": null,
                "a": null,
                "cd": "01",
                "no": 10,
                "rtUrl": null,
                "ftype": 0,
                "rtUrls": [],
                "djId": 0,
                "copyright": 0,
                "s_id": 0,
                "mark": 262144,
                "originCoverType": 0,
                "originSongSimpleData": null,
                "tagPicList": null,
                "resourceState": true,
                "version": 3,
                "songJumpInfo": null,
                "entertainmentTags": null,
                "single": 0,
                "noCopyrightRcmd": null,
                "rtype": 0,
                "rurl": null,
                "mst": 9,
                "cp": 2707442,
                "mv": 0,
                "publishTime": 0,
                "privilege": {
                    "id": 1946185953,
                    "fee": 0,
                    "payed": 0,
                    "st": 0,
                    "pl": 320000,
                    "dl": 320000,
                    "sp": 7,
                    "cp": 1,
                    "subp": 1,
                    "cs": false,
                    "maxbr": 320000,
                    "fl": 320000,
                    "toast": false,
                    "flag": 128,
                    "preSell": false,
                    "playMaxbr": 320000,
                    "downloadMaxbr": 320000,
                    "maxBrLevel": "exhigh",
                    "playMaxBrLevel": "exhigh",
                    "downloadMaxBrLevel": "exhigh",
                    "plLevel": "exhigh",
                    "dlLevel": "exhigh",
                    "flLevel": "exhigh",
                    "rscl": null,
                    "freeTrialPrivilege": {
                        "resConsumable": false,
                        "userConsumable": false,
                        "listenType": null
                    },
                    "chargeInfoList": [
                        {
                            "rate": 128000,
                            "chargeUrl": null,
                            "chargeMessage": null,
                            "chargeType": 0
                        },
                        {
                            "rate": 192000,
                            "chargeUrl": null,
                            "chargeMessage": null,
                            "chargeType": 0
                        },
                        {
                            "rate": 320000,
                            "chargeUrl": null,
                            "chargeMessage": null,
                            "chargeType": 0
                        },
                        {
                            "rate": 999000,
                            "chargeUrl": null,
                            "chargeMessage": null,
                            "chargeType": 1
                        }
                    ]
                }
            }
        ],
        "songCount": 2
    },
    "code": 200
}

這裡簡單說明一下,code的響應狀態,可以根據這個來判斷請求是否成功,
result裡面,songCount代表搜尋結果有幾個,song裡面是音樂的一些資訊,
name,id,ar是藝術家artist的意思,也就是歌手,al是所屬專輯,包括名稱,封面之類的。
h, l,m,sq分別代表音質的等級,h是極高,l是較高,m的標準,sq是無失真。
privilege裡面是一些音樂的音質資訊,包括可下載的最大音質,會員下載資訊等,chargeInfoList列出了各個音質下載所需的許可權。

2、音樂資訊

(1)歌詞

點選一個音樂進入播放介面,開啟F12,篩選後一個一個連結尋找https://music.163.com/weapi/song/lyric?csrf_token=,此連結返回歌詞資訊

{
    "sgc": false,
    "sfy": false,
    "qfy": false,
    "transUser": {
        "id": 2204059,
        "status": 99,
        "demand": 1,
        "userid": 76837043,
        "nickname": "烈焰中舞動的火花",
        "uptime": 1493803368844
    },
    "lyricUser": {
        "id": 2204040,
        "status": 99,
        "demand": 0,
        "userid": 114415020,
        "nickname": "another_tonary",
        "uptime": 1493803368844
    },
    "lrc": {
        "version": 5,
        "lyric": "[00:00.000] 作詞 : 羽生みいな\n[00:00.515] 作曲 : Meis Clauson\n[00:01.30]自由の翅\n[00:02.78]月影のシミュラクル -解放の羽- 0P主題歌\n[00:10.45]\n[00:23.35]ここから見る景色は 何故どこか狹く悲しく ah\n[00:32.97]聲にならない聲で そう君を呼んでいたんだ ah\n[00:43.17]逃げられない蝶のように\n[00:48.00]最期を待つだけじゃないと\n[00:53.03]溫かい手 重ねた瞬間(とき)\n[00:58.09]差し込んだ光\n[01:02.03]絡みつくこの糸が 交わされた契約が\n[01:07.14]どれほど命 縛ろうとも\n[01:11.97]いつの日かこの翅(はね)を精一杯広げて\n[01:17.10]君が傍に居てくれるなら\n[01:22.24]きっと飛び立てるの あの空へと\n[01:48.09]仕方のないことだと 何故諦めようとしてた ah\n[01:57.94]紅く染まる暗闇から 抜け出せない気がして ah\n[02:08.16]それでもまだ君と生きたい\n[02:13.07]繫いだ手は震えるけど\n[02:18.05]熱い涙 溢れた瞬間(とき)\n[02:23.06]湧き上がる勇気\n[02:27.08]捕らわれた運命が 立ちはだかる試練が\n[02:32.11]どれほどこの身操ろうとも\n[02:37.00]立ち向かいたい 強く信じるの もっと強く\n[02:42.14]獨りじゃないと思えた 君となら\n[02:48.93]飛び立てるの あの空へと\n[02:53.53]忘れかけてた 遠い記憶 あの約束 思い出して\n[03:03.27]取り戻せるの 二人ならば 広い世界を\n[03:12.20]絡みつくこの糸が 交わされた契約が\n[03:17.08]どれほど命 縛ろうとも\n[03:22.01]いつの日かこの翅を 精一杯広げて\n[03:27.06]君が傍に居てくれるなら\n[03:32.00]きっと 光の向こう\n[03:37.11]捕らわれた運命が 立ちはだかる試練が\n[03:42.08]どれほどこの身操ろうとも\n[03:46.93]立ち向かいたい 強く信じるの もっと強く\n[03:52.09]獨りじゃないと思えた 君となら\n[03:58.82]飛び立てるの あの空へと\n"
    },
    "tlyric": {
        "version": 5,
        "lyric": "[by:所間]\n[ti:自由の翅]\n[ar:佐藤ひろ美]\n[al:月影のシミュラクル -解放の羽- 初回限定同梱 オリジナルサウンドトラック]\n[00:01.30]\n[00:02.78]\n[00:10.45]\n[00:23.35]這裡所看到的景色 為何會感到如此狹小又悲傷 ah\n[00:32.97]以泣不成聲的聲音 不斷呼喊著你 ah\n[00:43.17]如同無法掙脫的蝴蝶一般\n[00:48.00]只能默默等候終焉的到來\n[00:53.03]溫暖的雙手重合的瞬間\n[00:58.09]感受到了照射的光芒\n[01:02.03]不管這糾纏不清的絲線與這被迫簽下的契約\n[01:07.14]究竟束縛了多少的生命\n[01:11.97]總有一天要用這雙翅膀 用盡全力展翅翱翔\n[01:17.10]只要你能夠陪伴在我身邊\n[01:22.24]一定就能夠展翅高飛 向著那片天空\n[01:48.09]為何要說著「這是無可奈何的事情」而準備去放棄一切呢 ah\n[01:57.94]就算覺得無法從這漸漸染紅的黑暗中逃脫出去 ah\n[02:08.16]即便如此仍舊想要與你一同活下去\n[02:13.07]雖然緊牽著的手止不住顫抖\n[02:18.05]溫熱的淚水 滿溢的瞬間\n[02:23.06]心中所湧出的勇氣\n[02:27.08]不管這被囚禁的命運與這艱辛的試煉\n[02:32.11]會讓這幅身軀會承受多少傷害\n[02:37.00]就算如此也想要奮發向上 不斷堅信著 變得更加堅強\n[02:42.14]與你在一起的話 就不會感到孤獨\n[02:48.93]向著那片天空展翅高飛\n[02:53.53]從將要遺忘的記憶中找回了那個約定\n[03:03.27]我們一起的話 就能奪回那個寬廣的世界\n[03:12.20]不管這糾纏不清的絲線與這被迫簽下的契約\n[03:17.08]究竟束縛了多少的生命\n[03:22.01]總有一天要用這雙翅膀 用盡全力展翅翱翔\n[03:27.06]只要你能夠陪伴在我身邊\n[03:32.00]肯定就在那光芒的彼岸\n[03:37.11]不管這被囚禁的命運與這艱辛的試煉\n[03:42.08]會讓這幅身軀會承受多少傷害\n[03:46.93]就算如此也想要奮發向上 不斷堅信著 變得更加堅強\n[03:52.09]與你在一起的話 就不會感到孤獨\n[03:58.82]向著那片天空展翅高飛"
    },
    "code": 200
}

其中transUser為歌詞貢獻者,lyricUser為歌詞翻譯貢獻者,lrc裡有原版歌詞,tlyric裡有歌詞翻譯。
解析過程和上面一樣,偵錯頁面找到d的值即可。

d = '{"id":"473403600","lv":-1,"tv":-1,"csrf_token":""}'

id為音樂id,可更改。

# 歌詞檔案
lyric_msg = '{"id":"427419615","lv":-1,"tv":-1,"csrf_token":""}'
lyric_url = 'https://music.163.com/weapi/song/lyric?csrf_token='
print(get_data(lyric_msg, lyric_url))

(2)評論

https://music.163.com/weapi/comment/resource/comments/get?csrf_token=返回使用者評論資訊,目前還不需要,不使用。

(3)音樂資訊

點選藍色的播放按鈕,發現由多出了一些連結,一個一個找下來。

https://music.163.com/weapi/v3/song/detail?csrf_token=返回音樂的詳細資訊。
注意,這裡偵錯時需要一點技巧,重新整理頁面,首先在原始碼裡打個斷點

點選播放,注意必須是藍色的那個播放按鈕

然後偵錯獲得api地址https://music.163.com/weapi/v3/song/detail和d
繼續偵錯可獲得音樂的下載地址。

# 音樂詳細資訊,包含了音質等級和可下載許可權
detail_msg = r'{"id":"473403600","c":"[{\"id\":\"473403600\"}]","csrf_token":""}'
detail_url = 'https://music.163.com/weapi/v3/song/detail?csrf_token='
print(get_data(detail_msg, detail_url))

注意msg要加上r,id為音樂id,可更改
其實沒有這個也行,在搜尋時,返回的資料裡就有音樂的詳細資訊了。

(4)下載連結

https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=返回的是下載連結,

偵錯找到d,

# 音樂下載地址,level代表音質等級,encodeType代表編碼型別,flac可儲存無失真音質,目前無法下載無失真音樂
# 音質 standard標準 higher較高 exhigh極高 lossless無失真 hires
# 編碼型別 aac flac
song_msg = '{"ids":"[473403600]","level":"lossless","encodeType":"flac","csrf_token":""}'
song_url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
print(get_data(song_msg, song_url))

ids裡面是音樂id,既然是一個陣列,那想必可以多加幾個音樂id,返回多個下載地址。level為音質水平,分四個等級,standard代表標準,higher代表較高,exhigh代表極高,lossless代表無失真,還有hires,其中lossless和hires都無法下載,不知道加上有會員許可權的csrf_token能不能下載。

3、音樂播放器外連

在網頁版有一個功能叫生成外連播放器,點選一下

它會叫你嵌入一段程式碼

我們將src裡面的內容複製出來,新增上頭部組成https://music.163.com/outchain/player?type=2&id=473403600&auto=1&height=66
你可以嵌入自己的網站中去(如果你的位置支援嵌入ifram的話),也可以自己寫一個前端播放器,然後爬音樂資訊,將資料放進去。
開啟開發者工具,這裡也有兩個可用的api介面
一個是音樂詳細資訊https://music.163.com/weapi/song/detail

另外一個是音樂下載地址https://music.163.com/weapi/song/enhance/player/url

偵錯頁面獲取d

# # 通過外連播放器獲取解析的連結
# # 音樂下載地址,br代表音質,依舊無法下載無失真音樂
# br四個等級 標準128000 較高192000 極高320000 無失真999000
song_msg = '{"ids":"[473403600]","br":1052522,"csrf_token":""}'
song_url = 'https://music.163.com/weapi/song/enhance/player/url'
print(get_data(song_msg, song_url))
# 
# # 音樂詳情,有更加詳細的資訊
detail_msg = r'{"id":"473403600","ids":"[\"473403600\"]","limit":10000,"offset":0,"csrf_token":""}'
detail_url = 'https://music.163.com/weapi/song/detail'
print(get_data(detail_msg, detail_url))

返回的資料和前面的差不多,稍微有點出入。

三、其他api

還記得上面提到過的一段程式碼嗎?

Y8Q = Y8Q.replace("api", "weapi");

這段程式碼將連結裡的api換成了weapi,如果我們用原來的連結會怎麼樣?
以歌詞檔案的api為例

補上字首得到https://music.163.com/api/song/lyric,當然,現在這個連結還用不了,需要傳遞引數。
展開query,發現裡面一些關於音樂的引數,帶入到連結裡去,https://music.163.com/api/song/lyric?id=473403600&lv=-1&tv=-1
此請求為get請求,自己獲取返回資料。

有沒有發現這個lv=-1,tv=-1這麼像d裡面的引數?

d = '{"id":"427419615","lv":-1,"tv":-1,"csrf_token":""}'

對比一下就知道了。

幾乎一致,通過更改引數的值,發現返回的結果,可以知道各個引數的含義。
其他api類似,如果你不想模擬js的加密過程,可以使用這些api直接獲取到資料。
更多api介面請檢視介面檔案。注意如果要下載會員音樂和無失真音樂(如果有的話)的話,要使用具有會員許可權的賬號cookie,post請求放cookies裡,get請求放headers裡。

四、最終實現程式碼

weapi介面的程式碼實現

"""
webapi介面
搜尋結果:https://music.163.com/weapi/cloudsearch/get/web?csrf_token=(post)
評論:https://music.163.com/weapi/comment/resource/comments/get?csrf_token=
歌詞:https://music.163.com/weapi/song/lyric?csrf_token=
詳情(包括音質):https://music.163.com/weapi/v3/song/detail?csrf_token=
歌曲下載:https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=

iw233網站解析連結
https://iw233.cn/music/?name=コトダマ紬ぐ未來&type=netease

外連:http://music.163.com/song/media/outer/url?id=534544522.mp3

音樂外連播放器:https://music.163.com/outchain/player?type=2&id=473403600&auto=1&height=66
"""
import base64
import codecs
import json
import math
import random

import requests
from Crypto.Cipher import AES
from urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

'''

var bKB3x = window.asrsea(JSON.stringify(i3x), buU1x(["流淚", "強"]), buU1x(Rg7Z.md), buU1x(["愛心", "女孩", "驚恐", "大笑"]));
            e3x.data = j3x.cr3x({
                params: bKB3x.encText,
                encSecKey: bKB3x.encSecKey
            })


    window.asrsea = d,

    d: {"hlpretag":"<span class="s-fc7">","hlposttag":"</span>","s":"コトダマ紬ぐ未來","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}
    e:010001
    f:00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
    g:0CoJUm6Qyw8W8jud
        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 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)
    }
'''

class wangyiyun:
    def __init__(self):
        self.e = '010001'
        self.f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
        self.g = '0CoJUm6Qyw8W8jud'

    # 獲取一個隨意字串,length是字串長度
    def generate_str(self, lenght):
        str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
        res = ''
        for i in range(lenght):
            index = random.random() * len(str)  # 獲取一個字串長度的亂數
            index = math.floor(index)  # 向下取整
            res = res + str[index]  # 累加成一個隨機字串
        return res

    # AES加密獲得params
    def AES_encrypt(self, text, key):
        iv = '0102030405060708'.encode('utf-8')  # iv偏移量
        text = text.encode('utf-8')  # 將明文轉換為utf-8格式
        pad = 16 - len(text) % 16
        text = text + (pad * chr(pad)).encode('utf-8')  # 明文需要轉成二進位制,且可以被16整除
        key = key.encode('utf-8')  # 將金鑰轉換為utf-8格式
        encryptor = AES.new(key, AES.MODE_CBC, iv)  # 建立一個AES物件
        encrypt_text = encryptor.encrypt(text)  # 加密
        encrypt_text = base64.b64encode(encrypt_text)  # base4編碼轉換為byte字串
        return encrypt_text.decode('utf-8')

    # RSA加密獲得encSeckey
    def RSA_encrypt(self, str, key, f):
        str = str[::-1]  # 隨機字串逆序排列
        str = bytes(str, 'utf-8')  # 將隨機字串轉換為byte型別的資料
        sec_key = int(codecs.encode(str, encoding='hex'), 16) ** int(key, 16) % int(f, 16)  # RSA加密
        return format(sec_key, 'x').zfill(256)  # RSA加密後字串長度為256,不足的補x

    # 獲取引數
    def get_params(self, d, e, f, g):
        i = self.generate_str(16)    # 生成一個16位元的隨機字串
        # i = 'aO6mqZksdJbqUygP'
        encText = self.AES_encrypt(d, g)
        # print(encText)    # 列印第一次加密的params,用於測試d正確
        params = self.AES_encrypt(encText, i)  # AES加密兩次後獲得params
        encSecKey = self.RSA_encrypt(i, e, f)  # RSA加密後獲得encSecKey
        return params, encSecKey

    # 傳入msg和url,獲取返回的json資料
    def get_data(self, msg, url):
        encText, encSecKey = self.get_params(msg, self.e, self.f, self.g)   # 獲取引數
        params = {
            "params": encText,
            "encSecKey": encSecKey
        }
        re = requests.post(url=url, params=params, verify=False)    # 向伺服器傳送請求
        return re.json()    #返回結果

    # 返回搜尋資料
    def get_search_data(self, s='', type=1, offset=0, total='true', limit=30, csrf_token=''):
        msg = r'{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>",' + f'"s":"{s}","type":"{type}","offset":"{offset}","total":"{total}","limit":"{limit}","csrf_token":"{csrf_token}"' + '}'
        url = f'https://music.163.com/weapi/cloudsearch/get/web?csrf_token={csrf_token}'
        return self.get_data(msg, url)

    # 返回歌詞資料
    def get_lyric_data(self, id, lv=-1, tv=-1, csrf_token=''):
        msg = '{' + f'"id":"{id}","lv":"{lv}","tv":"{tv}","csrf_token":"{csrf_token}"' + '}'
        url = f'https://music.163.com/weapi/song/lyric?csrf_token={csrf_token}'
        return self.get_data(msg, url)

    # 返回音樂詳情,包含了音質等級和可下載許可權
    def get_detail_data(self, id, csrf_token=''):
        msg = '{' + f'"id":"{id}",' + r'"c":"[{\"id\":\"' + str(id) + r'\"}]",' + f'"csrf_token":"{csrf_token}"' + '}'
        url = f'https://music.163.com/weapi/v3/song/detail?csrf_token={csrf_token}'
        return  self.get_data(msg, url)

    # 返回下載資料,level代表音質等級,encodeType代表編碼型別,flac可儲存無失真音質,目前無法下載無失真音樂
    # # 音質 standard標準 higher較高 exhigh極高 lossless無失真 hires
    # # 編碼型別 aac flac
    def get_download_data(self, id, level='exhigh', encodeType='flac', csrf_token=''):
        msg = '{' + f'"ids":"{id}","level":"{level}","encodeType":"{encodeType}","csrf_token":"{csrf_token}"' + '}'
        url = f'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token={csrf_token}'
        return self.get_data(msg, url)

    # 通過播放器外連的方式返回的音樂詳情資料
    def get_detail_outdata(self, id, limit=10000, offset=0, csrf_token=''):
        msg = '{' + f'"id":"{id}",' + r'"ids":"[\"' + str(id) + r'\"]",' + f'"limit":{limit},"offset":{offset},"csrf_token":"{csrf_token}"' + '}'
        url = 'https://music.163.com/weapi/song/detail'
        return self.get_data(msg, url)

    # 通過播放器外連的方式返回的音樂下載資料
    # br代表音質,四個等級 標準128000 較高192000 極高320000 無失真999000
    def get_download_outdata(self, id, br=320000, csrf_token=''):
        msg = '{' + f'"ids":"{id}","br":{br},"csrf_token":"{csrf_token}"' + '}'
        url = 'https://music.163.com/weapi/song/enhance/player/url'
        return self.get_data(msg, url)

api介面的程式碼實現

'''
api介面
'''

import json
import requests
from urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)


class wangyiyun:
    # 獲取資料
    def get_data(self, url, data):
        re = requests.post(url=url, data=data, verify=False)
        return re.json()
    # 返回搜尋資料
    def get_search_data(self, s='', type=1, offset=0, total='true', limit=30, csrf_token=''):
        url = 'https://music.163.com/api/cloudsearch/get/web'
        data = {
            'hlpretag': '<span class="s-fc7">',
            'hlposttag': '</span>',
            's': s,
            'type': type,
            'offset': offset,
            'total': total,
            'limit': limit,
            'csrf_token': csrf_token
        }
        return self.get_data(url, data)

    # 返回歌詞資料
    def get_lyric_data(self, id, lv=-1, tv=-1, csrf_token=''):
        url = 'https://music.163.com/api/song/lyric'
        data = {
            'id': id,
            'lv': lv,
            'tv': tv,
            'csrf_token': csrf_token
        }
        return self.get_data(url, data)

    # 返回音樂詳情,包含了音質等級和可下載許可權
    def get_detail_data(self, id, csrf_token=''):
        url = 'https://music.163.com/api/v3/song/detail'
        c = '[{' + f'"id":"{id}"' + '}]'
        data = {
            'id': id,
            'c': c,
            'csrf_token': csrf_token
        }
        return  self.get_data(url, data)

    # 返回下載資料,level代表音質等級,encodeType代表編碼型別,flac可儲存無失真音質,目前無法下載無失真音樂
    # # 音質 standard標準 higher較高 exhigh極高 lossless無失真 hires
    # # 編碼型別 aac flac
    def get_download_data(self, id, level='exhigh', encodeType='flac', csrf_token=''):
        url = 'https://music.163.com//api/song/enhance/player/url/v1'
        data = {
            'encodeType': encodeType,
            'ids': str(id),
            'level': level,
            'csrf_token': csrf_token
        }
        return self.get_data(url, data)

    # 通過播放器外連的方式返回的音樂詳情資料
    def get_detail_outdata(self, id, limit=10000, offset=0, csrf_token=''):
        url = 'https://music.163.com/api/song/detail'
        data = {
            'id': id,
            'ids': f'[{str(id)}]',
            'limit': limit,
            'offset': offset,
            'csrf_token': csrf_token
        }
        return self.get_data(url, data)

    # 通過播放器外連的方式返回的音樂下載資料
    # br代表音質,四個等級 標準128000 較高192000 極高320000 無失真999000
    def get_download_outdata(self, id, br=320000, csrf_token=''):
        url = 'https://music.163.com/api/song/enhance/player/url'
        data = {
            'br': br,
            'ids': str(id),
            'csrf_token': csrf_token
        }
        return self.get_data(url, data)

五、總結

全部過程爬取下來,發現網易雲對資料的加密方式還是挺單調的,只要弄懂了原理就好辦了,基本上都一致。
後續考慮做個音樂播放器,新增網易雲,QQ,酷狗,百度等源,等做好了再發個教學和專案地址,不過感覺用處也不大,畢竟不能下載vip音樂和無失真音樂,就當做是學習了。
小夥伴們有什麼不懂的地方可以私信我,也可以在評論區留言。