Python 爬取歌曲评论内容(仅供学习参考)

前言

我是一个喜欢边听歌边写代码的人,正好最近在学习爬虫的逆向,想起了以前师兄给我讲解过的音乐评论爬取,那时候的我还不知道什么是爬虫什么是逆向,更是听得一头雾水。不过,经过慢慢的学习现在已经慢慢开始了解、接触爬虫的知识了。接下来,我就来为大家讲解一下,把我的心得体会分享给大家。

一、分析网页

首先,打开我们要爬取的歌曲所在的网页,这里小编选择的是一首我超级喜欢的粤语《7538》,找到想要爬取的评论内容,因为音乐评论是动态加载的,所以我们不能通过页面源查找到评论内容。

因此,我们需要抓包。在开发者模式中,点击“Network”、“XHR”,然后点击网页中的下一页。这样我们就能抓取到返回结果数据的包。32a3aa256f69ffa8a532ac0e73cde944.png

点开抓取到的包,一个包一个包点进去查看,我们可以在“get?csrf_token=”这个包里找到我们需要爬取的评论内容,这说明了评论内容是通过js动态生成的。8272b4a598b6bc0bba42d4ba735fc163.png

点开标头,可以发现是POST请求,所以我们就需要带上它的表单数据,点开负载,发现表单数据中有两个参数分别是“params”、“encSecKey”而且都是奇怪的乱码。f88fda95c8b5986e4d7942229c8c2b08.jpeg

这就意味着,如果我们想要爬取评论内容的话就需要先破解这两个参数的加密,并能正确模拟加密参数的加密过程才能获得网站返回的数据。

二、找到参数加密的位置

小编喜欢用XHR断点调试来定位加密的位置,所以在这里小编就用XHR断点来为大家展示如何找到加密位置的吧

首先,把网址“?”前的地址复制下来,并点击源代码,在源代码页面右边找到“XHR/提取断点”并把复制好的网址复制进去。bd27a29d954c594e174be96324edf269.png

7d089a9d7dfb8e3e603f0d44bf65bac4.png

设置好断点后我们就可以开始抓包了,我们可以点击下一页,重新抓包,断住以后我们就可以开始找加密参数了。如下图,我们可以发现参数已经加密完成了,所以我们需要往前查找加密位置。这时我们就可以利用右边的“调用堆栈”来查找加密位置。因为堆栈越往下就是越先执行的代码。30248c7922f2a014af2b9a48cff99ffe.png

当我们一个一个堆栈查看很快就能发现疑似加密的代码了,比如在“u0x.be0x”这个堆栈中,我们能发现疑似参数加密的位置。所以我毫不犹豫地就在这个位置打了个断点,并重新抓包。f85507894d1f0a69673249da778cfcb2.png

重新抓包后,就能更加肯定参数加密的位置就是我们找的位置了。加密方法是:“window.asrsea(JSON.stringify(i0x), bsg8Y(["流泪", "强"]), bsg8Y(TH5M.md), bsg8Y(["爱心", "女孩", "惊恐", "大笑"]))”。这个方法大概的意思就是将“i0x”的值转为字符串格式后与“ bsg8Y(["流泪", "强"])”的值、“bsg8Y(TH5M.md)”的值、“ bsg8Y(["爱心", "女孩", "惊恐", "大笑"]))”的值放到方法“window.asrsea()”里面运行,并返回了我们需要的加密参数。

c068327efa746ecde2f8a99c08a91856.png

三、分析加密方法的参数

分析第一个参数“i0x”。在控制台中输入“i0x”,我们可以得到加密前的原值,它是一个JSON格式的数据。其中“pageNO”是评论的页数,“cursor”是一个特别的数字,第一页的数值是“-1”,但是往后每一页的数值都是不同的。因此“i0x”就这两个变量。c9c2fa97b987ffd361ecebb236d75981.png

分析第二个参数“bsg8Y(["流泪", "强"])”。 在控制台中输入“bsg8Y(["流泪", "强"])”,输出的结果是“010001”。这个值是固定的,我通过多次运行发现的。想了解为什么的朋友可以点进“bsg8Y”这个方法里查看,在这里就不多做解释了,我们知道这是个固定值就行了。

50adcca2ad17a08488bb9baf450f8eee.png

分析第三个参数“bsg8Y(TH5M.md)”。在控制台中输入“bsg8Y(TH5M.md)”,输出的结果是一大串字符串,这个值也是固定的,它是通过一个方法输出的固定值,所以我们可以写死。

456afd57e8429169e46f16a862457948.png

分析第四个参数“ bsg8Y(["爱心", "女孩", "惊恐", "大笑"]))”。我可以很负责人的告诉大家,这个值也是固定的。它是通过某种加密方法实现的,在这里就不做介绍了。感兴趣的小友可以自己点进去查看。56af8807f976c8f4e9ad979c56e6fc7e.png

经过分析,我们知道了加密参数相对应的值。其中,有三个参数的值是固定不变的,只有“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位的时间戳,但是当我用时间戳带入后发现无法爬取下一页数据,我就开始猜想是不是这个时间戳有问题。b47d0dc28a568c2aa9724577d1ad3273.png

在这里我就不卖关子了,这个值是上一页面返回值里的一个参数,也就是说这个13位的数字是第一页响应数据里的一个参数信息,如下图所示:18c1555c930fdc4708925b6de8f84867.png

所以如果想翻页的话不仅要修改“pageNO”的值,更要从上一页的响应值中提取出“cursor”的值放入到“i0x”里面去。

四、分析加密方法

加密方法是:“window.asrsea()”,我们可以把鼠标放到这个方法上,然后点击就可以跳转到这个方法所在的位置了38f913b60b2a4f6ca4f152ef7323ddfa.png

点进去之后定位到了方法d,其中,我们可以看到window.asrsea=d,并且方法d与方法a,c,d都有关系,所以我们需要对这四个函数进行分析。37db0063eed2e361452a9dc3f9aee46d.png

首先分析d函数。从上图中,我们可以大概了解到函数d的执行过程。首先从函数a中得到一个值给到“i”,然后再用d(这个是“i0x”字符串格式的值)和g(这个是“ bsg8Y(["爱心", "女孩", "惊恐", "大笑"]))”的值)传入函数b中进行加密,再把得到的值与“i”再进行一次加密,然后就能得到“h.encText”也就是“params”的值了。 而“h.encSecKey”的值是通过函数c,传入“i”,“bsg8Y(["流泪", "强"])”和“bsg8Y(TH5M.md)”进行加密所得。

其中a函数是一个随机生成一串长度为16的字母与数字组合的字符串。b1a8ec0ba137bb7e25ecd33d31b46fbd.png

然后b函数是一个加密函数,首先传入参数a和b,然后分别将a和b进行utf-8编码赋值,然后再把编码后的参数用AES进行加密,其中函数内定义了一个iv,也就是偏移量,是AES加密方式中必须的一个参数,mode也就是模式,加密模式有四种,这里采用CBC的加密模式。0a7902ee1a0f08fa515cfc60a045440e.png

最后是c函数,c函数是生成encSecKey的关键函数。其中,c里面的参数中“i”是一个随机值,而参数“e”和参数“f”都是固定值,所以我们只要能确定“i”的值就能确定encSecKey的值了。32dec9516bd79e89abce5f9fa29b0496.png

总结:“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())

最终得到的结果如下:693d6fc84ce505963e9cb083789ef8a7.png

六、完整代码

小编将放出完整的源码,以供参考。


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()

七、总结

通过这次对评论的解析让我从中获得的很多感悟,在此,我也将我所获得的感悟在这里给大家分享。希望我的文章能给有缘人提供参考和帮助。这篇文章也是小编第一次写有关于代码的文章,如有不足的地方欢迎各位英雄豪杰提供建议。小编也会在后面更新一下爬虫相关的文章,欢迎感兴趣的朋友一起学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值