用過網易雲音樂聽歌的朋友都知道,網易雲音樂每首歌曲後面都有很多評論,熱門歌曲的評論更是接近百萬或者是超過百萬條.現在我就來分享一下如何爬取網易雲音樂歌曲的全部評論,由於網易雲音樂的評論都做了混淆加密處理,因此我們需要深入瞭解它的加密過程之後才能爬取到網易雲音樂歌曲的全部評論.
一,首先分析資料的請求方式
網易雲音樂歌曲頁面的URL形式為https://music.163.com/#/song?id=歌曲id號,這裡我用Delacey的Dream it possible 為例進行講解,它的URL為https://music.163.com/#/song?id=38592976.接下來開始分析資料的請求方式.
由於網易雲音樂的評論是通過Ajax傳輸,我們開啟瀏覽器的開發者工具(檢查元素),選中控制皮膚中的Network,再點選XHR(捕獲ajax資料),然後點選左上角的重新載入,會看到下面圖片中的資料請求列表
點選R_SO_4_38592976?csrf_token=cdee144903c5a32e6752f50180329fc9這一行,再點選Preview
發現我們所需要的資料就在這json格式的資料中,其中comments中是第一頁的全部評論,一共20條,hotcomments是精彩評論一共有15條,每首歌曲只有第一頁評論才有精彩評論.接著看一下它的請求頭,點選Headers
我們發現的它是個post請求,向下滑你會發現這個post請求還帶有資料
這些資料都是經過加密處理的,因此我們需要分析它的加密過程來生成相應的引數,然後把加密後的引數加到post請求中才能獲取到我們需要的評論資料.
二,分析加密過程
通過斷點除錯發現params和encSecKey是由js指令碼中的window.asrsea()函式生成的.
我們發現window.asrsea()函式有4個引數,在瀏覽器的js控制檯分別對這四個引數進行除錯:
後面三個引數是定值,只有第一個引數是控制評論頁面偏移量的引數,它是一個變數.筆者經過分析發現第一個引數的形式是:
1 |
{"rid":"R_SO_4_38592976","offset":"0","total":"True","limit":"20","csrf_token":""} |
下面我來詳細講解這個變數的發現過程:
首先找到core_dfe56728795d119e4d476fd09ea2dc51.js這個js指令碼,然後將斷點打在第12973行,點選第一頁評論,頁面載入到斷點處便停止了
然後按下電腦的Esc鍵開啟js控制檯,輸入i1x,檢視第一個變數:
這是第一頁的i1x的值,接下來看第二頁的(需要點選第2頁,然後輸入i1x的值):
再看第3頁:
再看第4頁:
通過這幾頁的分析,我們可以得到i1x值的變化規律,且可以得到它的一般形式:
1 |
{"rid":"R_SO_4_38592976","offset":"0","total":"True","limit":"20","csrf_token":""} |
offset和limit是必選引數,其他引數是可選的,其他引數不影響data資料的生成,offset (頁面偏移量) = (頁數-1) * 20, 注意limit最大值為100,當設為100時,獲取第二頁時,預設前一頁是20個評論,也就是說第二頁最新評論有80個,有20個是第一頁顯示的.因此我們可以構造第一個引數為:
1 2 3 |
# 偏移量,page是頁數 offset = (page-1) * 20 msg = '{"offset":' + str(offset) + ',"total":"True","limit":"20","csrf_token":""}' |
接下來,我們來看一下window.asrsea()函式的整個加密過程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
!function() { // 函式a生成長度為16的隨機字串 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 } // 函式b實現AES加密 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() } // 函式c實現RSA加密 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 } function e(a, b, d, e) { var f = {}; return f.encText = c(a + e, b, d), f } window.asrsea = d, window.ecnonasr = e }(); |
window.asrsea()函式就是上面的d函式,現在我們來看函式d:
1 2 3 4 5 6 7 8 |
function d(d, e, f, g) { var h = {} , i = a(16); return h.encText = b(d, g), // 第一次AES加密 h.encText = b(h.encText, i), // 第二次AES加密 h.encSecKey = c(i, e, f), // RSA加密 h } |
引數h.encText是經過兩次AES加密得到的,h.encSecKey是經過一次RSA加密得到的,其中i是隨機生成的長度為16的隨機字串.
三,生成加密引數
首先我們需要生成長度為16的隨機字串,這裡我們仿照上面的javascript的實現,用Python生成16位長的隨機字串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 生成隨機字串 def generate_random_strs(length): string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" # 控制次數引數i i = 0 # 初始化隨機字串 random_strs = "" while i < length: e = random.random() * len(string) # 向下取整 e = math.floor(e) random_strs = random_strs + list(string)[e] i = i + 1 return random_strs |
接著用Python實現AES加密,這裡要用到pycrypto庫,先安裝好這個庫:
1 |
pip install pycrypto |
然後匯入加密模組:
1 |
from Crypto.Cipher import AES |
由於AES加密的明文長度必須是16的倍數,因此我們需要對明文進行必要的填充,以滿足它的長度是16的倍數:
1 2 3 4 |
# msg是需要加密的明文,如果不是16的倍數則進行填充(paddiing) padding = 16 - len(msg) % 16 # 這裡使用padding對應的單字元進行填充 msg = msg + padding * chr(padding) |
AES加密的模式是AES.MODE_CBC,初始化向量iv=’0102030405060708′,具體的AES加密:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# AES加密 def AESencrypt(msg, key): # 如果不是16的倍數則進行填充(paddiing) padding = 16 - len(msg) % 16 # 這裡使用padding對應的單字元進行填充 msg = msg + padding * chr(padding) # 用來加密或者解密的初始向量(必須是16位) iv = '0102030405060708' cipher = AES.new(key, AES.MODE_CBC, iv) # 加密後得到的是bytes型別的資料 encryptedbytes = cipher.encrypt(msg) # 使用Base64進行編碼,返回byte字串 encodestrs = base64.b64encode(encryptedbytes) # 對byte字串按utf-8進行解碼 enctext = encodestrs.decode('utf-8') return enctext |
然後是RSA加密.首先我簡單介紹一下RSA的加密過程.在RSA中,明文,金鑰和密文都是數字.RSA的加密過程可以用下列的公式來表達,這個公式非常的重要,你只有理解了這個公式,才能用Python實現RSA加密.
1 |
密文 = 明文E mod N (RSA加密) |
RSA的密文是對代表明文的數字的E次方求mod N 的結果, 通俗的講就是將明文和自己做E次乘法,然後將其結果除以N 求餘數,這個餘數就是密文.
下面來看具體的RSA加密程式碼實現:
1 2 3 4 5 6 7 8 |
# RSA加密 def RSAencrypt(randomstrs, key, f): # 隨機字串逆序排列 string = randomstrs[::-1] # 將隨機字串轉換成byte型別資料 text = bytes(string, 'utf-8') seckey = int(codecs.encode(text, encoding='hex'), 16)**int(key, 16) % int(f, 16) return format(seckey, 'x').zfill(256) |
RSA加密後得到的字串長為256,這裡不夠長我們用x字元填充.
最後就是獲取那兩個加密引數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# 獲取引數 def get_params(page): # msg也可以寫成msg = {"offset":"頁面偏移量=(頁數-1) * 20", "limit":"20"},offset和limit這兩個引數必須有(js) # limit最大值為100,當設為100時,獲取第二頁時,預設前一頁是20個評論,也就是說第二頁最新評論有80個,有20個是第一頁顯示的 # 偏移量 offset = (page-1) * 20 # offset和limit是必選引數,其他引數是可選的,其他引數不影響data資料的生成,最好還是保留 msg = '{"offset":' + str(offset) + ',"total":"True","limit":"20","csrf_token":""}' key = '0CoJUm6Qyw8W8jud' f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' e = '010001' enctext = AESencrypt(msg, key) # 生成長度為16的隨機字串 i = generate_random_strs(16) # 兩次AES加密之後得到params的值 encText = AESencrypt(enctext, i) # RSA加密之後得到encSecKey的值 encSecKey = RSAencrypt(i, e, f) return encText, encSecKey |
四,獲取全部評論
上面我們獲取到了兩個引數encText和encSecKey,利用這兩個引數來構造post表單資料(Form Data),即data的值:
1 2 |
params, encSecKey = get_params(page) data = {'params': params, 'encSecKey': encSecKey} |
歌曲評論的URL為:
1 |
url = 'https://music.163.com/weapi/v1/resource/comments/R_SO_4_' + str(songid) + '?csrf_token=' |
然後把data加到post的引數中去就能獲取到json格式的評論資料.
1 |
html = requests.post(url, headers=headers, data=data) |
至此,獲取網易雲音樂全部評論的Python爬蟲實現原理分析全部完成!若想獲取全部程式碼請上我的Github:獲取網易雲音樂歌曲全部評論程式碼
作者簡介:志穎,一個狂熱的 python 爬蟲愛好者