爬取網易雲評論

任務爬取網易雲黃老闆的shape of you下面贊超過1000的評論

網頁爬取

本次任務的難點就在於網頁爬取,可以結合知乎關於此問題的回答一起看

網頁分析

打開網頁之後切換評論的頁數,可以看到網址的URL並沒有變化,沒有像豆瓣一樣出現page=X,猜測是直接通過加載JavaScript數據包改變評論。
在這裏插入圖片描述
打開F12,刷新一下,選擇NetWork,勾選XHR,經過分析,評論數據是由R_SO_4_…數據包發過來的。
在這裏插入圖片描述
選中這個數據包,我們分析一下。
這是一個POST數據包,對每一頁評論URL沒有變。服務器應該是用過請求的其他數據確定我們需要的是哪一頁。
在這裏插入圖片描述
往下翻,到From Data,顯然我們這兩個參數是經過加密的,大概率就是我們在找的數據。
在這裏插入圖片描述
我們去看看對應的JavaScript請求,點擊Initiator,可以看到對應的JavaScript請求,點擊一下core_f69…
在這裏插入圖片描述
可以看到跳轉到了Sources部分,代碼不太方便看,可以點擊一下左下角的{}符號
在這裏插入圖片描述
經過查找,發現我們要的params參數和enSecKey參數由一個bVj7c的變量提供的,而bVj7c是通過window.asrsea函數得到的,其共有四個參數
JSON.stringify(i8a),
brx9o([“流淚”, “強”]),
brx9o(Xs4w.md),
brx9o([“愛心”, “女孩”, “驚恐”, “大笑”])
(選這幾個詞來加密的程序員一定是個有故事的程序員~)
我們把斷點打在13092(左擊一下行號就可以設置斷點)
在這裏插入圖片描述
現在點擊一下網頁評論的其他頁可以看到對應的參數
在這裏插入圖片描述
按下esc鍵調出console,在console中依次輸入四個參數,可以得到對應的值,經過對比,發現後三個爲常數,而第一個參數通過改變offset來確定頁數,每次變化20,從0開始變化。
在這裏插入圖片描述

參數獲取

現在我們來實現一下window.asrsea得到我們要的params和enSecKey。
把代碼下載下來後,找到window.asrsea位置。
簡單分析一下,
function a實現生成長度爲a的隨機字符串;
function b是把a和b一起進行AES加密,iv設置爲0102030405060708;
function c將a,b,c一起進行RSA加密
function d也就是我們要用的window.asrsea,可以由四個參數得到params和enSecKey
在這裏插入圖片描述
我們也用pycrypto模仿實現一下(可以搜一下願意對應着看)
安裝pycrypto模塊報錯的話,可以用

pip install -i https://pypi.douban.com/simple/ pycryptodome

代碼:

class MusicSpider:

    def __init__(self):
        self.headers = {
            'accept' : "*/*",
            'origin' : "https://music.163.com",
            'Host': "music.163.com",
            'user-agent' : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36",
        }
        # 第二個參數
        self.second_param = "010001"
        # 第三個參數
        self.third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
        # 第四個參數
        self.forth_param = "0CoJUm6Qyw8W8jud"

    def get_params(self, page):
        offset = str((page - 1) * 20)
        self.first_param = '{rid:"", offset:"%s", total:"%s", limit:"20", csrf_token:""}' % (offset, 'true')
        self.random_strs = self.generate_random_strs(16) # 生成長度爲16的隨機字符串
        # 兩次AES加密之後得到params的值
        self.params = self.AES_encrypt(self.first_param, self.forth_param)
        self.params = self.AES_encrypt(self.params.decode('utf-8'), self.random_strs)

    def get_encSecKey(self):
        # RSA加密之後得到encSecKey的值
        self.encSecKey = self.RSAencrypt(self.random_strs, self.second_param, self.third_param)

    #生成隨機字符串
    def generate_random_strs(self, length):
        string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        random_strs = ""
        for i in range(length):
            temp = random.randint(0, len(string)-1)
            random_strs += list(string)[temp]
        return random_strs

    #AES加密
    def AES_encrypt(self, msg, key):
        # 如果不是16的倍數則進行填充(paddiing)
        padding = 16 - len(msg) % 16
        # 這裏使用padding對應的單字符進行填充
        msg = msg + padding * chr(padding)
        # 用來加密或者解密的初始向量(必須是16位)
        iv = '0102030405060708'

        encryptor = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
        # 加密後得到的是byte類型的數據
        encrypt_text = encryptor.encrypt(msg.encode('utf-8'))
        # 使用Base64進行編碼,返回byte字符串
        encrypt_text = base64.b64encode(encrypt_text)
        return encrypt_text

    # RSA加密
    def RSAencrypt(self, 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)

數據分析

這部分與知乎分析json數據類似
回到Network 欄,找到Preview,可以看到,評論內容在comments下的content,點贊數在comments下的likedCount
在這裏插入圖片描述
將params和encSecKey作爲數據,發送post請求,返回json文件

    def get_json(self, url):
        self.post = {
            'params' : self.params,
            'encSecKey': self.encSecKey,
        }
        try:
            self.response = requests.post(url, data=self.post, headers = self.headers)
            if self.response.status_code == 200:
                return self.response.json()
        except requests.ConnectionError:
            return None

數據存儲

在得到的json文件中獲取content和likedcount,當likedcount超過100就保存content

    def get_comments(self, url):
        f = open('./comments.txt', 'w', encoding='utf-8')
        self.get_params(1)
        self.get_encSecKey()
        data = self.get_json(url)
        page = data.get('total') // 20 + 1 if (data.get('total')%20) else 0
        for i in range(1, page):
            self.get_params(i)
            self.get_encSecKey()
            data = self.get_json(url)
            for comment in data.get("comments"):
                likedcount = comment.get('likedCount')
                content = comment.get("content")
                if likedcount > 100 :
                    f.write(content+'\n')
            print("第%d頁抓取完畢"%i)
            time.sleep(5)

得到的評論做個詞雲叭
在這裏插入圖片描述

完整代碼

from Crypto.Cipher import AES
import base64
import time
import requests
import random
import codecs
from urllib.parse import urlencode

class MusicSpider:

    def __init__(self):
        self.headers = {
            'accept' : "*/*",
            'origin' : "https://music.163.com",
            'Host': "music.163.com",
            'user-agent' : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36",
        }
        # 第二個參數
        self.second_param = "010001"
        # 第三個參數
        self.third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
        # 第四個參數
        self.forth_param = "0CoJUm6Qyw8W8jud"

    def get_params(self, page):
        offset = str((page - 1) * 20)
        self.first_param = '{rid:"", offset:"%s", total:"%s", limit:"20", csrf_token:""}' % (offset, 'true')
        self.random_strs = self.generate_random_strs(16) # 生成長度爲16的隨機字符串
        # 兩次AES加密之後得到params的值
        self.params = self.AES_encrypt(self.first_param, self.forth_param)
        self.params = self.AES_encrypt(self.params.decode('utf-8'), self.random_strs)
       
    def get_encSecKey(self):
        # RSA加密之後得到encSecKey的值
        self.encSecKey = self.RSAencrypt(self.random_strs, self.second_param, self.third_param)

    #生成隨機字符串
    def generate_random_strs(self, length):
        string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        random_strs = ""
        for i in range(length):
            temp = random.randint(0, len(string)-1)
            random_strs += list(string)[temp]
        return random_strs

    #AES加密
    def AES_encrypt(self, msg, key):
        # 如果不是16的倍數則進行填充(paddiing)
        padding = 16 - len(msg) % 16
        # 這裏使用padding對應的單字符進行填充
        msg = msg + padding * chr(padding)
        # 用來加密或者解密的初始向量(必須是16位)
        iv = '0102030405060708'

        encryptor = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
        # 加密後得到的是byte類型的數據
        encrypt_text = encryptor.encrypt(msg.encode('utf-8'))
        # 使用Base64進行編碼,返回byte字符串
        encrypt_text = base64.b64encode(encrypt_text)
        return encrypt_text

    # RSA加密
    def RSAencrypt(self, 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)

    def get_json(self, url):
        self.post = {
            'params' : self.params,
            'encSecKey': self.encSecKey,
        }
        try:
            self.response = requests.post(url, data=self.post, headers = self.headers)
            if self.response.status_code == 200:
                return self.response.json()
        except requests.ConnectionError:
            return None

    def get_comments(self, url):
        f = open('./comments.txt', 'w', encoding='utf-8')
        self.get_params(1)
        self.get_encSecKey()
        data = self.get_json(url)
        page = data.get('total') // 20 + 1 if (data.get('total')%20) else 0
        for i in range(1, page):
            self.get_params(i)
            self.get_encSecKey()
            data = self.get_json(url)
            for comment in data.get("comments"):
                likedcount = comment.get('likedCount')
                content = comment.get("content")
                if likedcount > 100 :
                    f.write(content+'\n')
            print("第%d抓取完畢"%i)
            time.sleep(5)

if __name__ == "__main__":
	#要其他歌曲的話,改一下URL的R_SO_4_後面的歌曲id即可~
    url = "https://music.163.com/weapi/v1/resource/comments/R_SO_4_451703096?csrf_token="
    musicspider = MusicSpider()
    musicspider.get_comments(url)

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