目錄
一、概述
第一次學爬蟲,正常來講應該是爬百度百科或者是豆瓣之類的,但這倆網站我沒興趣,因此選擇爬網易雲。
學習過程中主要參考該網址。
二、爬取流程
1、爬取評論
1.1、資源定位
當我們進入網易雲音樂的網頁版,進入一首歌的頁面:
我們可以看到歌名、歌詞、評論、相似歌曲、收藏該歌曲的歌單等。
這些信息我們如何得知的?
瀏覽器向網易的服務器發送請求,網易的服務器返回數據,瀏覽器再給我們看。
嗯,意思就是,如果我們能模仿瀏覽器的請求,讓網易的服務器把我們想要的信息給我們,這樣我們就能不斷的收集信息了。
這也就是爬蟲的基本功能了。
怎麼看瀏覽器向網易發送請求呢?f12。
當我們在按下f12之後按f5刷新網頁時,控制檯就會顯示所有的瀏覽器向客戶端發出的請求。
注意,第一行爲“Network”。filter選擇“All”。
下面有一大堆亂七八糟的東西:jpg、png、js這些是文件,還有一些像是網址的東西:cdns?csrf_token=。看不明白。
由於我們需要的是瀏覽器向服務器發出的請求,我們不要選擇“All”,選擇“XHR”。這個XHR是個什麼?
XMLHttpRequest對象(簡稱XHR)是ajax技術的核心,ajax可以無刷新更新頁面得益於xhr。
沒怎麼看明白,不過不要緊,這個是前端的活,不用理解。看XHR之後,只剩下以下幾項:
這不是之前看起來很像是網頁的東西嘛。隨便點進去一個看看,這個lyric看上去應該是歌詞:
嗯,猜對了。也就是說,我們想要爬取歌詞的話,就要分析這個。
R_SO呢?這個是啥?
看上去應該是評論。成了,我們需要的就是這個。
於是我們就找到了評論的位置。
對於一些小網站,對反爬蟲沒什麼要求或者是沒有那麼多資源的話,整個網頁的內容都在element裏面。xhr是一個沒有的,比如這樣一個小說網站:
網易雲應該是爬的人太多了加上爲了用戶體驗,所以用了那麼多XHR。
1.2、爬取準備
既然我們知道了評論是由XHR中的某一條顯示的,那就看看這一條裏面有什麼:
先看Headers,這是瀏覽器向服務器發送的信息儲存的位置:
第一個url很有用,要記下來。
再看Response Headers,回覆頭部?應該是服務器返回的信息的頭部:
時間啊,解碼方式什麼的。服務器竟然用的是nginx,有點熟悉。
然後是Request Headers,重頭戲:
這個是瀏覽器發往服務器的request的頭部,主要記住兩個參數:referer和user-agent。這兩個參數在服務器返回cheating時候加載head裏。
接下來是Form data。不知道有什麼用:
來分析一下:
我們的瀏覽器向服務器發送一條請求:
發送的地址應該就是第一個url,通過Request Headers表明我的身份。那問題來了:如何表明我的目的呢?
比如說我的目的是要殘酷天使的行動綱領這首歌的第100-200條評論,那麼我們要發給服務器的信息有:
歌名:殘酷天使的行動綱領
需求:評論
條數:100
起始評論:第100條
一共四個關鍵信息。
根據我們分析這個XHR,顯示提供的信息有:url、瀏覽器配置、時間等。
平平無奇。
這說明有隱式信息。
首先分析顯示信息的url:
https://music.163.com/weapi/v1/resource/comments/R_SO_4_657666?csrf_token=
當我們調換不同的歌的時候,發現該url整體不會變動,只有R_SO後面的數字會變。因此,這數字就是我們傳遞的一個變量,該數字應與歌一一對應。
url中的comments指的是評論,可以與我們的需求對應上。
現在還有兩個關鍵信息,不能顯示傳遞:起始評論和評論條數。
url有傳遞過去的參數。藏在哪裏呢?
藏在Form Data裏。
這兩串好長的字符串是加密過的,解密的過程參見我在第一部分所示的鏈接。
我的主要代碼也是參考的這位大佬。
解密的過程可以不去在意,但是這種定位方式一定要學會,十分有用。
1.3、代碼實現
大佬的代碼爲python2,如下:
#coding = utf-8
from Crypto.Cipher import AES
import base64
import requests
import json
headers = {
'Cookie': 'appver=1.5.0.75771;',
'Referer': 'http://music.163.com/'
}
first_param = "{rid:\"\", offset:\"0\", total:\"true\", limit:\"20\", csrf_token:\"\"}"
second_param = "010001"
third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
forth_param = "0CoJUm6Qyw8W8jud"
def get_params():
iv = "0102030405060708"
first_key = forth_param
second_key = 16 * 'F'
h_encText = AES_encrypt(first_param, first_key, iv)
h_encText = AES_encrypt(h_encText, second_key, iv)
return h_encText
def get_encSecKey():
encSecKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c"
return encSecKey
def AES_encrypt(text, key, iv):
pad = 16 - len(text) % 16
text = text + pad * chr(pad)
encryptor = AES.new(key, AES.MODE_CBC, iv)
encrypt_text = encryptor.encrypt(text)
encrypt_text = base64.b64encode(encrypt_text)
return encrypt_text
def get_json(url, params, encSecKey):
data = {
"params": params,
"encSecKey": encSecKey
}
response = requests.post(url, headers=headers, data=data)
return response.content
if __name__ == "__main__":
url = "http://music.163.com/weapi/v1/resource/comments/R_SO_4_30953009/?csrf_token="
params = get_params();
encSecKey = get_encSecKey();
json_text = get_json(url, params, encSecKey)
json_dict = json.loads(json_text)
print json_dict['total']
for item in json_dict['comments']:
print item['content'].encode('gbk', 'ignore')
有以下幾點要注意:
第一,代碼使用了Crypto.Cipher包,這個包我安裝失敗,可以通過安裝pycrypto代替。
第二,由於我用的python3,而python3和2的一大區別就是str與bytes的分家,所以有以下幾句代碼要更改一下:
def get_params():
iv = "0102030405060708"
first_key = forth_param
second_key = 16 * 'F'
h_encText = AES_encrypt(first_param.encode(), first_key, iv)#第一個參數要更改
h_encText = AES_encrypt(h_encText, second_key, iv)
return h_encText
def AES_encrypt(text, key, iv):
pad = 16 - len(text) % 16
text2=bytes(pad * chr(pad),encoding='gbk')#這裏也要改
text3 = text + text2#這裏也要改
encryptor = AES.new(key, AES.MODE_CBC, iv)
encrypt_text = encryptor.encrypt(text3)
encrypt_text = base64.b64encode(encrypt_text)
return encrypt_text
第三,該url需要五個參數,在代碼中體現爲:
first_param = "{rid:\"\", offset:\"0\", total:\"true\", limit:\"20\", csrf_token:\"\"}"
即:rid,offset,total,limit,token。
整份代碼絕大多數工作就是把這五個參數組成的bytes轉換爲加密後的形式。
其中,rid是歌曲id,這個爲空的話就直接默認是url帶的參數,所以可以無視掉,當我們需要在爬蟲中換歌的時候就要用這個了;
offset是偏移量,也就是相對第一個評論向後便宜多少;
total在第一頁是true,其餘是false。
其實知道參數以後自己試就可以了,整個爬蟲最難的就在於解密,其次就是url傳遞的參數的獲取。
然後就是按部就班的帶參數post,解析返回值即可。
其四,在解析返回值時,返回的是一個嵌套字典,字典對應的關鍵字可以在PreView中查看,如下:
這就指的是“熱門評論”中的第一條的內容。
效果如下:
這樣就實現了評論的爬取。
2、爬取聽歌記錄
這部分纔是我自己主要做的。難點就在於url的參數找不到。
2.1、資源定位
類似評論,我們要想找聽歌記錄,肯定要去他的個人主頁。這個有點涉及隱私,我就以爬我自己的爲例:
如圖,XHR有以下幾個,很容易就一眼看到records,以我半吊子的英語水平也能看得出來這個是記錄的意思。
點進去看一下吧:
不出所料,果然是。不過網頁版沒法看聽歌次數,而是用一個score代替,看上去這個分值應該就是100*count/countMAX,看我第一首歌是100分,第二首就只有41分了。
2.2、爬取準備
爬聽歌記錄肯定也要看Headers啊。
先看General:
wc,這個url好狠毒,一個參數都沒有顯示給我們,也就是說全部參數都是在DataForm裏。
看看Request Headers:
平平無奇,只有referer變成了我的主頁的鏈接。看來想要攻克這個,就得去找url帶的參數了。
參數不可能是憑空出來的,它一定是一個函數生成的,我們找到這個函數就好了。怎麼找呢?要看控制檯中的一項:
也就是initiator。這一列中就是對應的XHR的媽媽,調用這個文件產生所需的XHR。我們需要的文件叫core_亂碼.js。
看看這個文件裏有什麼:
wdnmd死機了。這文件有點大,複製到notepad++裏面好長好長。我們需要什麼呢?需要Data Form裏面的params和encSerKey。直接ctrl+f找一找,定位到第90行:
(function()
{
var c4g=NEJ.P,et7m=c4g("nej.g"),v4z=c4g("nej.j"),k4o=c4g("nej.u"),
Sj0x=c4g("nm.x.ek"),l4p=c4g("nm.x");
if(v4z.bg5l.redefine)
return;
window.GEnc=true;
var bry7r=function(cxa3x)
{
var m4q=[];
k4o.be5j(cxa3x,function(cwZ3x)
{
m4q.push(Sj0x.emj[cwZ3x])
});
return m4q.join("")
};
var cwX3x=v4z.bg5l;
v4z.bg5l=function(Z5e,e4i)
{
var i4m={},e4i=NEJ.X({},e4i),md9U=Z5e.indexOf("?");
if(window.GEnc&&/(^|\.com)\/api/.test(Z5e)&&!
(e4i.headers&&e4i.headers[et7m.zx3x]==et7m.Gv5A)&&!e4i.noEnc)
{
if(md9U!=-1)
{
i4m=k4o.gU7N(Z5e.substring(md9U+1));
Z5e=Z5e.substring(0,md9U)
}
if(e4i.query)
{
i4m=NEJ.X(i4m,k4o.fO7H(e4i.query)?k4o.gU7N(e4i.query):e4i.query)
}
if(e4i.data)
{
i4m=NEJ.X(i4m,k4o.fO7H(e4i.data)?k4o.gU7N(e4i.data):e4i.data)
}
i4m["csrf_token"]=v4z.gQ7J("__csrf");
Z5e=Z5e.replace("api","weapi");
e4i.method="post";
delete e4i.query;
window.console.info(i4m);//這一行是後加的
var bUS6M=window.asrsea(JSON.stringify(i4m),
bry7r(["流淚","強"]),
bry7r(Sj0x.md),
bry7r(["愛心","女孩","驚恐","大笑"]));
e4i.data=k4o.cx5C(
{
params:bUS6M.encText,
encSecKey:bUS6M.encSecKey
})
}
cwX3x(Z5e,e4i)
};
v4z.bg5l.redefine=true
}
)();
我真是日了,原文件裏面一行代碼結果弄出來這麼多行。看最後可以看到我們需要的params和encSecKey。
這倆怎麼來的呢?看一下它們是BUS6M的兩個屬性,這個BUS6M又是怎麼來的?是window.asrsea的返回值。
這個window.asrsea就是加密算法了。具體解析去看我上文的鏈接。
現在只看這個函數的參數,有四個,第一個是i4m轉爲string格式,剩下三個看上去怪怪的。你說這個流淚啊,強啊,愛心,女孩什麼的都是常量,肯定不是我們要的參數,那參數只可能是i4m或者Sj0x.md了。先看前者吧。
看上一行,i4m應該是一個字典,裏面的鍵有csrf_token,那有九成概率確定它就是參數的原始形態了。
我們只要輸出i4m不就行了。怎麼輸出呢?
修改這個core就行啊。
那問題來了,這文件在人家網易的服務器裏,瀏覽器是調用服務器的這個文件生成參數加密前的序列的。我又沒法進網易的服務器。
這時候就需要js劫持了。
2.3、js劫持
簡而言之,就是瀏覽器要使用core這個文件的時候,不去向網易要這個文件,而是和我要——反正我都有這個文件的所有代碼了,從我這裏運行一樣能得到結果不是。
怎麼實現“瀏覽器和我要這個文件”的功能呢?
要用到fiddler。這個程序很厲害,很簡單就能實現js劫持:
上面鏈接中的方法不好用,我建議用我的方法:
第一:安裝並使用fiddler。安裝很簡單,使用它可能會有一點麻煩,要是有代理就更麻煩。我建議使用
Chrome的switchOmega插件,選擇系統代理。
判斷是否好用的一個方法爲:開着fiddler,隨便開一個網址,如果fiddler界面出來一堆花花綠綠的就是好用。
第二:修改fiddler的過濾器。默認過濾器是不會顯示js文件的請求的,我們需要知道瀏覽器發出調用core的請求,因此要把它顯示出來。如下圖:
在Show only if URL contains中添加如下的正則表達式:
REGEX:(?insx)/[^\?/]*\.(css|js|json|ico|jpg|png|gif|bmp|wav)(\?.*)?$
即可。
第三:設定劫持條件。如下圖:
在界面右側選擇AutoResponder,也就是1;下面三個框勾上前面兩個也就是2和3;add之後添加4和5,其中4不用自己寫,5是我們上面複製下來的core.js的絕對地址,我是放在桌面的,加上輸出i4m的那一句。
如何自動填充4呢?
首先清除瀏覽器緩存,如果不清除緩存,瀏覽器會將core下載下來,不會再次請求服務器(在這裏可能也可以改,但我沒試過),fiddler就找不到調用core的請求了。
然後進入網易雲音樂用戶主頁,刷新,就可以看到了:
如圖,第一條紫色的就是core的調用請求。左鍵單擊,按住後拽到右邊的AutoResponder裏面,4就填充好了。
這時我們把默認的*200-SESSION_10改成修改後的core的絕對路徑即可。
然後重新刷新網頁,就是調用我們自己的core了。效果如下:
在f12的console裏,輸出了以下幾個i4m,我們可以猜測,後兩個應該就是歌單和聽歌歷史的url對應參數了。這樣我們就找到了答案。然後將1中的代碼改一下就可以:
#first_param = "{rid:\"\", offset:\"0\", total:\"true\", limit:\"20\", csrf_token:\"\"}"#歌曲評論
first_param = "{uid: \"77824233\",type: \"-1\",limit: \"1000\",offset: \"0\",total: \"true\",csrf_token: \"\"}"#聽歌記錄
上面是歌曲評論的參數,下面是我們找到的聽歌歷史的參數。
然後把url改一下,顯示部分的代碼也改一下就好了。
如果報錯
把fiddler關閉即可。
效果如下:
成功獲取到聽歌記錄。
三、總結
本次爬蟲結束之後,對爬蟲的具體流程有了一個大概的認識:
簡而言之,爬蟲就是定位資源→模擬請求→獲取資源的一個過程。
通過在f12中尋找可以找到資源的位置,所需的url或者直接的元素等,通過將網頁元素保存或者是向url發出請求得到所有資源,在所有資源中尋找我們想要的。
沒有利用ip池等技術,八成我這個爬一百首歌ip就會被封了。