前言
我是一个喜欢边听歌边写代码的人,正好最近在学习爬虫的逆向,想起了以前师兄给我讲解过的音乐评论爬取,那时候的我还不知道什么是爬虫什么是逆向,更是听得一头雾水。不过,经过慢慢的学习现在已经慢慢开始了解、接触爬虫的知识了。接下来,我就来为大家讲解一下,把我的心得体会分享给大家。
一、分析网页
首先,打开我们要爬取的歌曲所在的网页,这里小编选择的是一首我超级喜欢的粤语《7538》,找到想要爬取的评论内容,因为音乐评论是动态加载的,所以我们不能通过页面源查找到评论内容。
因此,我们需要抓包。在开发者模式中,点击“Network”、“XHR”,然后点击网页中的下一页。这样我们就能抓取到返回结果数据的包。
点开抓取到的包,一个包一个包点进去查看,我们可以在“get?csrf_token=”这个包里找到我们需要爬取的评论内容,这说明了评论内容是通过js动态生成的。
点开标头,可以发现是POST请求,所以我们就需要带上它的表单数据,点开负载,发现表单数据中有两个参数分别是“params”、“encSecKey”而且都是奇怪的乱码。
这就意味着,如果我们想要爬取评论内容的话就需要先破解这两个参数的加密,并能正确模拟加密参数的加密过程才能获得网站返回的数据。
二、找到参数加密的位置
小编喜欢用XHR断点调试来定位加密的位置,所以在这里小编就用XHR断点来为大家展示如何找到加密位置的吧
首先,把网址“?”前的地址复制下来,并点击源代码,在源代码页面右边找到“XHR/提取断点”并把复制好的网址复制进去。
设置好断点后我们就可以开始抓包了,我们可以点击下一页,重新抓包,断住以后我们就可以开始找加密参数了。如下图,我们可以发现参数已经加密完成了,所以我们需要往前查找加密位置。这时我们就可以利用右边的“调用堆栈”来查找加密位置。因为堆栈越往下就是越先执行的代码。
当我们一个一个堆栈查看很快就能发现疑似加密的代码了,比如在“u0x.be0x”这个堆栈中,我们能发现疑似参数加密的位置。所以我毫不犹豫地就在这个位置打了个断点,并重新抓包。
重新抓包后,就能更加肯定参数加密的位置就是我们找的位置了。加密方法是:“window.asrsea(JSON.stringify(i0x), bsg8Y(["流泪", "强"]), bsg8Y(TH5M.md), bsg8Y(["爱心", "女孩", "惊恐", "大笑"]))”。这个方法大概的意思就是将“i0x”的值转为字符串格式后与“ bsg8Y(["流泪", "强"])”的值、“bsg8Y(TH5M.md)”的值、“ bsg8Y(["爱心", "女孩", "惊恐", "大笑"]))”的值放到方法“window.asrsea()”里面运行,并返回了我们需要的加密参数。
三、分析加密方法的参数
分析第一个参数“i0x”。在控制台中输入“i0x”,我们可以得到加密前的原值,它是一个JSON格式的数据。其中“pageNO”是评论的页数,“cursor”是一个特别的数字,第一页的数值是“-1”,但是往后每一页的数值都是不同的。因此“i0x”就这两个变量。
分析第二个参数“bsg8Y(["流泪", "强"])”。 在控制台中输入“bsg8Y(["流泪", "强"])”,输出的结果是“010001”。这个值是固定的,我通过多次运行发现的。想了解为什么的朋友可以点进“bsg8Y”这个方法里查看,在这里就不多做解释了,我们知道这是个固定值就行了。
分析第三个参数“bsg8Y(TH5M.md)”。在控制台中输入“bsg8Y(TH5M.md)”,输出的结果是一大串字符串,这个值也是固定的,它是通过一个方法输出的固定值,所以我们可以写死。
分析第四个参数“ bsg8Y(["爱心", "女孩", "惊恐", "大笑"]))”。我可以很负责人的告诉大家,这个值也是固定的。它是通过某种加密方法实现的,在这里就不做介绍了。感兴趣的小友可以自己点进去查看。
经过分析,我们知道了加密参数相对应的值。其中,有三个参数的值是固定不变的,只有“i0x”是一个会变的JSON格式的值。在“i0x”中会变的只有“pageNO”(页数),“cursor”特殊参数。
i0x = {
"rid": "R_SO_4_486111543",
"threadId": "R_SO_4_486111543",
"pageNo": "1",
"pageSize": "20",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"csrf_token": ""
}
“cursor”这个特殊参数非常特殊,当我们点击第二页的时候,这个参数就不是“-1”了,它就变成了一个13位长度的数字,一开始小编以为它是一个13位的时间戳,但是当我用时间戳带入后发现无法爬取下一页数据,我就开始猜想是不是这个时间戳有问题。
在这里我就不卖关子了,这个值是上一页面返回值里的一个参数,也就是说这个13位的数字是第一页响应数据里的一个参数信息,如下图所示:
所以如果想翻页的话不仅要修改“pageNO”的值,更要从上一页的响应值中提取出“cursor”的值放入到“i0x”里面去。
四、分析加密方法
加密方法是:“window.asrsea()”,我们可以把鼠标放到这个方法上,然后点击就可以跳转到这个方法所在的位置了
点进去之后定位到了方法d,其中,我们可以看到window.asrsea=d,并且方法d与方法a,c,d都有关系,所以我们需要对这四个函数进行分析。
首先分析d函数。从上图中,我们可以大概了解到函数d的执行过程。首先从函数a中得到一个值给到“i”,然后再用d(这个是“i0x”字符串格式的值)和g(这个是“ bsg8Y(["爱心", "女孩", "惊恐", "大笑"]))”的值)传入函数b中进行加密,再把得到的值与“i”再进行一次加密,然后就能得到“h.encText”也就是“params”的值了。 而“h.encSecKey”的值是通过函数c,传入“i”,“bsg8Y(["流泪", "强"])”和“bsg8Y(TH5M.md)”进行加密所得。
其中a函数是一个随机生成一串长度为16的字母与数字组合的字符串。
然后b函数是一个加密函数,首先传入参数a和b,然后分别将a和b进行utf-8编码赋值,然后再把编码后的参数用AES进行加密,其中函数内定义了一个iv,也就是偏移量,是AES加密方式中必须的一个参数,mode也就是模式,加密模式有四种,这里采用CBC的加密模式。
最后是c函数,c函数是生成encSecKey的关键函数。其中,c里面的参数中“i”是一个随机值,而参数“e”和参数“f”都是固定值,所以我们只要能确定“i”的值就能确定encSecKey的值了。
总结:“params”的值是通过两次AES加密后得到的,“encSecKey”的值则是通过c函数进行加密,只要得到随机值“i”就能得到encSecKey的值。由于JS太长就不在这里放了,感兴趣的小友可以私信小编问小编拿噢。
五、获取响应数据
从请求头中我们可以得知是“POST”请求,请求需要携带表单参数。
import requests, execjs
url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="
i0x = {
"rid": "R_SO_4_486111543",
"threadId": "R_SO_4_486111543",
"pageNo": "1",
"pageSize": "20",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"csrf_token": ""
}
ctll = execjs.compile(open('音乐评论.js', encoding='utf-8').read())
data = {
"params": ctll.call('get_params', i0x),
"encSecKey": ctll.call('get_encSecKey', i0x)
}
response = requests.post(url, headers=headers, data=data)
print(response.json())
最终得到的结果如下:
六、完整代码
小编将放出完整的源码,以供参考。
import requests, execjs
import pymongo
class Music(object):
def __init__(self):
self.url = "https://music.163.com/weapi/comment/resource/comments/get?csrf_token="
self.headers = {
"authority": "music.163.com",
"origin": "https://music.163.com",
"referer": "https://music.163.com/song?id=486111543",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36",
"x-music-loc-site": "100_https://music.163.com/song"
}
self.ctll = execjs.compile(open('音乐评论.js', encoding='utf-8').read())
self.client = pymongo.MongoClient(host='localhost', port=27017)
self.collection = self.client['spider']['评论']
def get_data_01(self):
i0x_1 = {
"rid": "R_SO_4_486111543",
"threadId": "R_SO_4_486111543",
"pageNo": "1",
"pageSize": "20",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"csrf_token": ""
}
data = {
"params": self.ctll.call('get_params', i0x_1),
"encSecKey": self.ctll.call('get_encSecKey', i0x_1)
}
return data
def get_data_new(self, cursor, page):
i0x_new = {
"rid": "R_SO_4_486111543",
"threadId": "R_SO_4_486111543",
"pageNo": page,
"pageSize": "20",
"cursor": cursor,
"offset": "0",
"orderType": "1",
"csrf_token": ""
}
data = {
"params": self.ctll.call('get_params', i0x_new),
"encSecKey": self.ctll.call('get_encSecKey', i0x_new)
}
return data
def get_date(self, data):
response = requests.post(self.url, headers=self.headers, data=data)
for lists in response.json()['data']['comments']:
date = "评论:" + lists['content'].replace("\n", '') + " 所在省份:" + lists['ipLocation']['location'] + " 名称:" + lists['user']['nickname'] + " 评论时间:" + lists['timeStr']
print(date)
self.save_date(date)
return response.json()['data']['cursor']
def save_date(self, date):
with open('评论.txt', 'a+', encoding='utf-8')as f:
f.write(date + "\n")
f.write('\n')
def run(self):
global cursor
for i in range(1, 200):
if i == 1:
data = self.get_data_01()
cursor = self.get_date(data)
else:
data = self.get_data_new(cursor, i)
cursor = self.get_date(data)
print(f'爬取完成第{i}页评论')
if __name__ == '__main__':
music = Music()
music.run()
七、总结
通过这次对评论的解析让我从中获得的很多感悟,在此,我也将我所获得的感悟在这里给大家分享。希望我的文章能给有缘人提供参考和帮助。这篇文章也是小编第一次写有关于代码的文章,如有不足的地方欢迎各位英雄豪杰提供建议。小编也会在后面更新一下爬虫相关的文章,欢迎感兴趣的朋友一起学习。