用過網(wǎng)易云音樂聽歌的朋友都知道,網(wǎng)易云音樂每首歌曲后面都有很多評論,熱門歌曲的評論更是接近百萬或者是超過百萬條.現(xiàn)在我就來分享一下如何爬取網(wǎng)易云音樂歌曲的全部評論,由于網(wǎng)易云音樂的評論都做了混淆加密處理,因此我們需要深入了解它的加密過程之后才能爬取到網(wǎng)易云音樂歌曲的全部評論.
一,首先分析數(shù)據(jù)的請求方式
網(wǎng)易云音樂歌曲頁面的URL形式為https://music.163.com/#/song?id=歌曲id號(hào),這里我用Delacey的Dream it possible 為例進(jìn)行講解,它的URL為https://music.163.com/#/song?id=38592976.接下來開始分析數(shù)據(jù)的請求方式.
由于網(wǎng)易云音樂的評論是通過Ajax傳輸,我們打開瀏覽器的開發(fā)者工具(檢查元素),選中控制面板中的Network,再點(diǎn)擊XHR(捕獲ajax數(shù)據(jù)),然后點(diǎn)擊左上角的重新加載,會(huì)看到下面圖片中的數(shù)據(jù)請求列表
點(diǎn)擊R_SO_4_38592976?csrf_token=cdee144903c5a32e6752f50180329fc9這一行,再點(diǎn)擊Preview
發(fā)現(xiàn)我們所需要的數(shù)據(jù)就在這json格式的數(shù)據(jù)中,其中comments中是第一頁的全部評論,一共20條,hotcomments是精彩評論一共有15條,每首歌曲只有第一頁評論才有精彩評論.接著看一下它的請求頭,點(diǎn)擊Headers
我們發(fā)現(xiàn)的它是個(gè)post請求,向下滑你會(huì)發(fā)現(xiàn)這個(gè)post請求還帶有數(shù)據(jù)
這些數(shù)據(jù)都是經(jīng)過加密處理的,因此我們需要分析它的加密過程來生成相應(yīng)的參數(shù),然后把加密后的參數(shù)加到post請求中才能獲取到我們需要的評論數(shù)據(jù).
二,分析加密過程
通過斷點(diǎn)調(diào)試發(fā)現(xiàn)params和encSecKey是由js腳本中的window.asrsea()函數(shù)生成的.
我們發(fā)現(xiàn)window.asrsea()函數(shù)有4個(gè)參數(shù),在瀏覽器的js控制臺(tái)分別對這四個(gè)參數(shù)進(jìn)行調(diào)試:
后面三個(gè)參數(shù)是定值,只有第一個(gè)參數(shù)是控制評論頁面偏移量的參數(shù),它是一個(gè)變量.筆者經(jīng)過分析發(fā)現(xiàn)第一個(gè)參數(shù)的形式是:
{"rid":"R_SO_4_38592976","offset":"0","total":"True","limit":"20","csrf_token":""}
下面我來詳細(xì)講解這個(gè)變量的發(fā)現(xiàn)過程:
首先找到core_dfe56728795d119e4d476fd09ea2dc51.js這個(gè)js腳本,然后將斷點(diǎn)打在第12973行,點(diǎn)擊第一頁評論,頁面加載到斷點(diǎn)處便停止了
然后按下電腦的Esc鍵打開js控制臺(tái),輸入i1x,查看第一個(gè)變量:
這是第一頁的i1x的值,接下來看第二頁的(需要點(diǎn)擊第2頁,然后輸入i1x的值):
再看第3頁:
再看第4頁:
通過這幾頁的分析,我們可以得到i1x值的變化規(guī)律,且可以得到它的一般形式:
{"rid":"R_SO_4_38592976","offset":"0","total":"True","limit":"20","csrf_token":""}
offset和limit是必選參數(shù),其他參數(shù)是可選的,其他參數(shù)不影響data數(shù)據(jù)的生成,offset (頁面偏移量) = (頁數(shù)-1) * 20, 注意limit最大值為100,當(dāng)設(shè)為100時(shí),獲取第二頁時(shí),默認(rèn)前一頁是20個(gè)評論,也就是說第二頁最新評論有80個(gè),有20個(gè)是第一頁顯示的.因此我們可以構(gòu)造第一個(gè)參數(shù)為:
# 偏移量,page是頁數(shù)
offset = (page-1) * 20
msg = '{"offset":' + str(offset) + ',"total":"True","limit":"20","csrf_token":""}
接下來,我們來看一下window.asrsea()函數(shù)的整個(gè)加密過程:
!function() {
// 函數(shù)a生成長度為16的隨機(jī)字符串
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
}
// 函數(shù)b實(shí)現(xiàn)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()
}
// 函數(shù)c實(shí)現(xiàn)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()函數(shù)就是上面的d函數(shù),現(xiàn)在我們來看函數(shù)d:
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
}
參數(shù)h.encText是經(jīng)過兩次AES加密得到的,h.encSecKey是經(jīng)過一次RSA加密得到的,其中i是隨機(jī)生成的長度為16的隨機(jī)字符串.
三,生成加密參數(shù)
首先我們需要生成長度為16的隨機(jī)字符串,這里我們仿照上面的javascript的實(shí)現(xiàn),用Python生成16位長的隨機(jī)字符串:
# 生成隨機(jī)字符串
def generate_random_strs(length):
string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
# 控制次數(shù)參數(shù)i
i = 0
# 初始化隨機(jī)字符串
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實(shí)現(xiàn)AES加密,這里要用到pycrypto庫,先安裝好這個(gè)庫:
pip install pycrypto
然后導(dǎo)入加密模塊:
from Crypto.Cipher import AES
由于AES加密的明文長度必須是16的倍數(shù),因此我們需要對明文進(jìn)行必要的填充,以滿足它的長度是16的倍數(shù):
# msg是需要加密的明文,如果不是16的倍數(shù)則進(jìn)行填充(paddiing)
padding = 16 - len(msg) % 16
# 這里使用padding對應(yīng)的單字符進(jìn)行填充
msg = msg + padding * chr(padding)
AES加密的模式是AES.MODE_CBC,初始化向量iv=’0102030405060708′,具體的AES加密:
# AES加密
def AESencrypt(msg, key):
# 如果不是16的倍數(shù)則進(jìn)行填充(paddiing)
padding = 16 - len(msg) % 16
# 這里使用padding對應(yīng)的單字符進(jìn)行填充
msg = msg + padding * chr(padding)
# 用來加密或者解密的初始向量(必須是16位)
iv = '0102030405060708'
cipher = AES.new(key, AES.MODE_CBC, iv)
# 加密后得到的是bytes類型的數(shù)據(jù)
encryptedbytes = cipher.encrypt(msg)
# 使用Base64進(jìn)行編碼,返回byte字符串
encodestrs = base64.b64encode(encryptedbytes)
# 對byte字符串按utf-8進(jìn)行解碼
enctext = encodestrs.decode('utf-8')
return enctext
然后是RSA加密.首先我簡單介紹一下RSA的加密過程.在RSA中,明文,密鑰和密文都是數(shù)字.RSA的加密過程可以用下列的公式來表達(dá),這個(gè)公式非常的重要,你只有理解了這個(gè)公式,才能用Python實(shí)現(xiàn)RSA加密.
密文 = 明文E mod N (RSA加密)
RSA的密文是對代表明文的數(shù)字的E次方求mod N 的結(jié)果, 通俗的講就是將明文和自己做E次乘法,然后將其結(jié)果除以N 求余數(shù),這個(gè)余數(shù)就是密文.
下面來看具體的RSA加密代碼實(shí)現(xiàn):
# RSA加密
def RSAencrypt(randomstrs, key, f):
# 隨機(jī)字符串逆序排列
string = randomstrs[::-1]
# 將隨機(jī)字符串轉(zhuǎn)換成byte類型數(shù)據(jù)
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字符填充.
最后就是獲取那兩個(gè)加密參數(shù):
# 獲取參數(shù)
def get_params(page):
# msg也可以寫成msg = {"offset":"頁面偏移量=(頁數(shù)-1) * 20", "limit":"20"},offset和limit這兩個(gè)參數(shù)必須有(js)
# limit最大值為100,當(dāng)設(shè)為100時(shí),獲取第二頁時(shí),默認(rèn)前一頁是20個(gè)評論,也就是說第二頁最新評論有80個(gè),有20個(gè)是第一頁顯示的
# 偏移量
offset = (page-1) * 20
# offset和limit是必選參數(shù),其他參數(shù)是可選的,其他參數(shù)不影響data數(shù)據(jù)的生成,最好還是保留
msg = '{"offset":' + str(offset) + ',"total":"True","limit":"20","csrf_token":""}'
key = '0CoJUm6Qyw8W8jud'
f ='00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
e = '010001'
enctext = AESencrypt(msg, key)
# 生成長度為16的隨機(jī)字符串
i = generate_random_strs(16)
# 兩次AES加密之后得到params的值
encText = AESencrypt(enctext, i)
# RSA加密之后得到encSecKey的值
encSecKey = RSAencrypt(i, e, f)
return encText, encSecKey
四,獲取全部評論
上面我們獲取到了兩個(gè)參數(shù)encText和encSecKey,利用這兩個(gè)參數(shù)來構(gòu)造post表單數(shù)據(jù)(Form Data),即data的值:
params, encSecKey = get_params(page)
data = {'params': params, 'encSecKey': encSecKey}
歌曲評論的URL為:
url = 'https://music.163.com/weapi/v1/resource/comments/R_SO_4_' + str(songid) +
然后把data加到post的參數(shù)中去就能獲取到j(luò)son格式的評論數(shù)據(jù).
html = requests.post(url, headers=headers, data=data)
至此,獲取網(wǎng)易云音樂全部評論的Python爬蟲實(shí)現(xiàn)原理分析全部完成!