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