爬取网易云评论

任务爬取网易云黄老板的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)

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