【python】爬蟲入門:爬取網易雲音樂的歌曲評論、用戶歌單、用戶聽歌記錄等

目錄

 

一、概述

二、爬取流程

1、爬取評論

1.1、資源定位

1.2、爬取準備

1.3、代碼實現

2、爬取聽歌記錄

2.1、資源定位

2.2、爬取準備

2.3、js劫持

三、總結


一、概述

第一次學爬蟲,正常來講應該是爬百度百科或者是豆瓣之類的,但這倆網站我沒興趣,因此選擇爬網易雲。

學習過程中主要參考該網址

二、爬取流程

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裏面的paramsencSerKey。直接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就會被封了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章