注意:本次爬取的音樂僅有1分鐘試聽,僅作學習爬蟲的原理,完整音樂需要自行下載客戶端。
一、 初步分析
登陸酷狗音樂后隨機選取一首歌,在請求里發現一段mp3文件,復制網址,確實是我們需要的url。
復制音頻的名字,搜索找到發起請求的網址,發現是在songinfo里
查看參數和請求頭,刷新一次,查看是否有哪些參數是變化的。可以發現圖中兩次請求的這些參數都不同,接下來就尋找這些參數的生成方式,參數clienttime
為時間戳,那么就只需要找到signature
的生成方式就可以了:
二、分析參數signature
1. 分析過程
搜索參數signature
,并在可能生成的位置打上斷點,然后刷新網頁
網頁斷在了此處,可以看見參數signature
跟函數d
與數組s
有關。
補充:如果s的長度不為13,需要放行一下,點擊這個按鈕
查看函數d
的定義
發現函數內部沒有wordsToBytes
函數和函數i
的相關定義,那么打下斷點,查看具體函數的位置
獲得wordsToBytes
函數的具體定義
獲得i
函數的具體定義
i
函數里沒有r.stringToBytes(t)
的相關定義,繼續打下斷點
找到r.stringToBytes(t)
的相關定義
接著查看s
的內容:在console里查看s
的內容,發現s
的值跟之前請求的參數類似。而且下標為0和下標為12的值跟u
的值相同
往上查找u
的定義,發現u
的值是固定的
2. 代碼實現
那么就開始實現生成signature的代碼(以下為JavaScript代碼)
function bytesToWords(t) {for (var n = [], r = 0, e = 0; r < t.length; r++,e += 8)n[e >>> 5] |= t[r] << 24 - e % 32;return n
}function rotl(t, n) {return t << n | t >>> 32 - n
}function endian(t) {if (t.constructor == Number)return 16711935 & rotl(t, 8) | 4278255360 & rotl(t, 24);for (var n = 0; n < t.length; n++)t[n] = endian(t[n]);return t
}function i(t, c) {var l = {utf8: {stringToBytes: function (t) {return l.bin.stringToBytes(unescape(encodeURIComponent(t)))},bytesToString: function (t) {return decodeURIComponent(escape(l.bin.bytesToString(t)))}},bin: {stringToBytes: function (t) {for (var n = [], r = 0; r < t.length; r++)n.push(255 & t.charCodeAt(r));return n},bytesToString: function (t) {for (var n = [], r = 0; r < t.length; r++)n.push(String.fromCharCode(t[r]));return n.join("")}}};i._ff = function (t, n, r, e, o, i, c) {var s = t + (n & r | ~n & e) + (o >>> 0) + c;return (s << i | s >>> 32 - i) + n},i._gg = function (t, n, r, e, o, i, c) {var s = t + (n & e | r & ~e) + (o >>> 0) + c;return (s << i | s >>> 32 - i) + n},i._hh = function (t, n, r, e, o, i, c) {var s = t + (n ^ r ^ e) + (o >>> 0) + c;return (s << i | s >>> 32 - i) + n},i._ii = function (t, n, r, e, o, i, c) {var s = t + (r ^ (n | ~e)) + (o >>> 0) + c;return (s << i | s >>> 32 - i) + n};t.constructor == String ? t = c && "binary" === c.encoding ? o.stringToBytes(t) : l.utf8.stringToBytes(t) : e(t) ? t = Array.prototype.slice.call(t, 0) : Array.isArray(t) || (t = t.toString());for (var s = bytesToWords(t), a = 8 * t.length, l = 1732584193, u = -271733879, f = -1732584194, d = 271733878, g = 0; g < s.length; g++)s[g] = 16711935 & (s[g] << 8 | s[g] >>> 24) | 4278255360 & (s[g] << 24 | s[g] >>> 8);s[a >>> 5] |= 128 << a % 32,s[14 + (a + 64 >>> 9 << 4)] = a;for (var b = i._ff, p = i._gg, h = i._hh, m = i._ii, g = 0; g < s.length; g += 16) {var y = l, j = u, S = f, v = d;u = m(u = m(u = m(u = m(u = h(u = h(u = h(u = h(u = p(u = p(u = p(u = p(u = b(u = b(u = b(u = b(u, f = b(f, d = b(d, l = b(l, u, f, d, s[g + 0], 7, -680876936), u, f, s[g + 1], 12, -389564586), l, u, s[g + 2], 17, 606105819), d, l, s[g + 3], 22, -1044525330), f = b(f, d = b(d, l = b(l, u, f, d, s[g + 4], 7, -176418897), u, f, s[g + 5], 12, 1200080426), l, u, s[g + 6], 17, -1473231341), d, l, s[g + 7], 22, -45705983), f = b(f, d = b(d, l = b(l, u, f, d, s[g + 8], 7, 1770035416), u, f, s[g + 9], 12, -1958414417), l, u, s[g + 10], 17, -42063), d, l, s[g + 11], 22, -1990404162), f = b(f, d = b(d, l = b(l, u, f, d, s[g + 12], 7, 1804603682), u, f, s[g + 13], 12, -40341101), l, u, s[g + 14], 17, -1502002290), d, l, s[g + 15], 22, 1236535329), f = p(f, d = p(d, l = p(l, u, f, d, s[g + 1], 5, -165796510), u, f, s[g + 6], 9, -1069501632), l, u, s[g + 11], 14, 643717713), d, l, s[g + 0], 20, -373897302), f = p(f, d = p(d, l = p(l, u, f, d, s[g + 5], 5, -701558691), u, f, s[g + 10], 9, 38016083), l, u, s[g + 15], 14, -660478335), d, l, s[g + 4], 20, -405537848), f = p(f, d = p(d, l = p(l, u, f, d, s[g + 9], 5, 568446438), u, f, s[g + 14], 9, -1019803690), l, u, s[g + 3], 14, -187363961), d, l, s[g + 8], 20, 1163531501), f = p(f, d = p(d, l = p(l, u, f, d, s[g + 13], 5, -1444681467), u, f, s[g + 2], 9, -51403784), l, u, s[g + 7], 14, 1735328473), d, l, s[g + 12], 20, -1926607734), f = h(f, d = h(d, l = h(l, u, f, d, s[g + 5], 4, -378558), u, f, s[g + 8], 11, -2022574463), l, u, s[g + 11], 16, 1839030562), d, l, s[g + 14], 23, -35309556), f = h(f, d = h(d, l = h(l, u, f, d, s[g + 1], 4, -1530992060), u, f, s[g + 4], 11, 1272893353), l, u, s[g + 7], 16, -155497632), d, l, s[g + 10], 23, -1094730640), f = h(f, d = h(d, l = h(l, u, f, d, s[g + 13], 4, 681279174), u, f, s[g + 0], 11, -358537222), l, u, s[g + 3], 16, -722521979), d, l, s[g + 6], 23, 76029189), f = h(f, d = h(d, l = h(l, u, f, d, s[g + 9], 4, -640364487), u, f, s[g + 12], 11, -421815835), l, u, s[g + 15], 16, 530742520), d, l, s[g + 2], 23, -995338651), f = m(f, d = m(d, l = m(l, u, f, d, s[g + 0], 6, -198630844), u, f, s[g + 7], 10, 1126891415), l, u, s[g + 14], 15, -1416354905), d, l, s[g + 5], 21, -57434055), f = m(f, d = m(d, l = m(l, u, f, d, s[g + 12], 6, 1700485571), u, f, s[g + 3], 10, -1894986606), l, u, s[g + 10], 15, -1051523), d, l, s[g + 1], 21, -2054922799), f = m(f, d = m(d, l = m(l, u, f, d, s[g + 8], 6, 1873313359), u, f, s[g + 15], 10, -30611744), l, u, s[g + 6], 15, -1560198380), d, l, s[g + 13], 21, 1309151649), f = m(f, d = m(d, l = m(l, u, f, d, s[g + 4], 6, -145523070), u, f, s[g + 11], 10, -1120210379), l, u, s[g + 2], 15, 718787259), d, l, s[g + 9], 21, -343485551),l = l + y >>> 0,u = u + j >>> 0,f = f + S >>> 0,d = d + v >>> 0}return endian([l, u, f, d])
}function wordsToBytes(t) {for (var n = [], r = 0; r < 32 * t.length; r += 8)n.push(t[r >>> 5] >>> 24 - r % 32 & 255);return n
}function bytesToHex(t) {for (var n = [], r = 0; r < t.length; r++)n.push((t[r] >>> 4).toString(16)),n.push((15 & t[r]).toString(16));return n.join("")
}function d(t, r) {if (void 0 === t || null === t)throw new Error("Illegal argument " + t);var e = wordsToBytes(i(t, r));return r && r.asBytes ? e : r && r.asString ? o.bytesToString(e) : bytesToHex(e)
}function getsianature() {var s = ["NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt","appid=1014","clienttime=1741411989613","clientver=20000","dfid=3Mm61k0WDxvm033Epz2worRG","encode_album_audio_id=j410q60","mid=70789bebe63fb74c52e4a911853f5450","platid=4","srcappid=2919","token=cbfe2e174e4b97fd6aca35682cdba3d2b431c4ed95e2dbd1779e37a7975b672c","userid=2307902397","uuid=70789bebe63fb74c52e4a911853f5450","NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt"];return d(s.join(""))
}console.log(getsianature());
經過一系列調試,可以發現生成的結果與瀏覽器生成的值一樣,那么生成signature的代碼就沒問題了。
將一些會變的值改為變量,可以看到參數s
的值里只有clienttime
的值是會變的,因此修改上述代碼中getsianature
函數,將參數s
的值放在python代碼中,getsianature
函數改完如下圖所示:
function getsianature(s) {return d(s.join(""))
}
三、獲取多首歌
1. 分析過程
點擊不同的歌,可以發現每首歌的參數encode_album_audio_id
都不同,因此需要獲取encode_album_audio_id
搜索歌名,找到歌曲的id
接著查看請求參數,同樣有一個signature
參數,刷新多次網頁發現signature
參數會變化,那么重復之前分析signature
的步驟
打下斷點,查看中斷的位置,參數s
有所變化
打印s
,查看s
的內容,除此之外沒有變化,那么就沿用先前的代碼
2. 代碼實現
完整代碼如下,注意這里調用了JavaScript代碼,需要安裝PyExecJS模塊:pip install PyExecJS -i https://pypi.tuna.tsinghua.edu.cn/simple。
本代碼中JavaScript文件名為kugou.js
,JavaScript代碼在參數signature
的分析中有寫到,以下為python代碼:
import json
import re
import time
import requests
import execjsclass kugou_music:def __init__(self):self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36','Referer': 'https://www.kugou.com/',}def get_signature(self, s):with open("kugou.js", "r", encoding="utf-8") as f:js = f.read()ctx = execjs.compile(js)signature = ctx.call("getsianature", s)return signaturedef get_one_song_url(self, audio_id):timestamp = str(int(time.time() * 1000))s = ["NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt","appid=1014",f"clienttime={timestamp}","clientver=20000","dfid=3Mm61k0WDxvm033Epz2worRG",f"encode_album_audio_id={audio_id}","mid=70789bebe63fb74c52e4a911853f5450","platid=4","srcappid=2919","token=cbfe2e174e4b97fd6aca35682cdba3d2b431c4ed95e2dbd1779e37a7975b672c","userid=2307902397","uuid=70789bebe63fb74c52e4a911853f5450","NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt"]signature = self.get_signature(s)params = {'srcappid': '2919','clientver': '20000','clienttime': timestamp,'mid': '70789bebe63fb74c52e4a911853f5450','uuid': '70789bebe63fb74c52e4a911853f5450','dfid': '3Mm61k0WDxvm033Epz2worRG','appid': '1014','platid': '4','encode_album_audio_id': audio_id,'token': 'cbfe2e174e4b97fd6aca35682cdba3d2b431c4ed95e2dbd1779e37a7975b672c','userid': '2307902397','signature': signature}one_song_url = 'https://wwwapi.kugou.com/play/songinfo'response = requests.get(one_song_url, headers=self.headers, params=params)song_url = response.json()['data']['play_url']return song_urldef get_signal_music(self, audio_id, audio_name):song_url = self.get_one_song_url(audio_id)response = requests.get(song_url, headers=self.headers)with open(f'{audio_name}.mp3', 'wb') as f:f.write(response.content)print(f'{audio_name}.mp3下載完成')def get_song_id(self,keyword):timestamp = str(int(time.time() * 1000))s = ["NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt","appid=1014","bitrate=0","callback=callback123",f"clienttime={timestamp}","clientver=1000","dfid=3Mm61k0WDxvm033Epz2worRG","filter=10","inputtype=0","iscorrection=1","isfuzzy=0",f"keyword={keyword}","mid=70789bebe63fb74c52e4a911853f5450","page=1","pagesize=30","platform=WebFilter","privilege_filter=0","srcappid=2919","token=cbfe2e174e4b97fd6aca35682cdba3d2b431c4ed95e2dbd1779e37a7975b672c","userid=2307902397","uuid=70789bebe63fb74c52e4a911853f5450","NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt"]signature = self.get_signature(s)params = {'callback': 'callback123','srcappid': '2919','clientver': '1000','clienttime': timestamp,'mid': '70789bebe63fb74c52e4a911853f5450','uuid': '70789bebe63fb74c52e4a911853f5450','dfid': '3Mm61k0WDxvm033Epz2worRG','keyword': keyword,'page': '1','pagesize': '30','bitrate': '0','isfuzzy': '0','inputtype': '0','platform': 'WebFilter','userid': '2307902397','iscorrection': '1','privilege_filter': '0','filter': '10','token': 'cbfe2e174e4b97fd6aca35682cdba3d2b431c4ed95e2dbd1779e37a7975b672c','appid': '1014','signature': signature}song_id_url = 'https://complexsearchretry.kugou.com/v2/search/song'response = requests.get(song_id_url, headers=self.headers, params=params, verify=False)temp = re.findall(r'callback123(.*)', response.text)[0][1:-1]temp = json.loads(temp)song = temp['data']['lists']return songdef get_all_song(self,keyword):song = self.get_song_id(keyword)for i in song:song_id = i.get('EMixSongID')song_name = i.get('FileName')# print(song_name, song_id)try:self.get_signal_music(song_id, song_name)except Exception as e:print(f'{song_name}下載失敗:', e)if __name__ == '__main__':kugou = kugou_music()# kugou.get_signal_music('j410q60')keyword='周杰倫'kugou.get_all_song(keyword)