本文借鉴了@平胸小仙女的知乎回复 https://www.zhihu.com/question/36081767 以及@lyrichu的博客 https://www.cnblogs.com/lyrichu/p/6635798.html

话不多说,直接开始正题

----------------------------------------------------------------------------------------------------------------------------------

首先从网页代码入手,随便找一首网易云音乐上面的歌曲,我这里用的是邓丽君的《南海姑娘》

python爬虫-爬取网易云音乐歌曲评论

右键-检查-network(我用的是chrome,firefox是右键-web控制台-网络)

然后刷新页面,就能看到network的活动

可以发现,点击下一页评论,只有评论会刷新,页面的URL不会变,因此向服务器发送的请求是xhr(XMLHttpRequset)对象。关于这个概念大家可以百度,简单来说就是能够实现在后台与服务器交换数据,在不重新加载页面的情况下更新网页。

这就缩小了范围,然后再一个个点进去查看response,就能找到包含评论的数据包,如图

python爬虫-爬取网易云音乐歌曲评论

点进去就能看到request header

python爬虫-爬取网易云音乐歌曲评论

python爬虫-爬取网易云音乐歌曲评论

以及两个参数 params以及encSecKey

python爬虫-爬取网易云音乐歌曲评论

在其response里能找到当前页面的所有评论,但是仅限当前这一页的评论。那么其他页的评论如何获得呢?

前面提到了,点击下一页的时候只刷新评论,而不会重新加载页面。那么既然这个进程是向服务器发起获取评论的请求,我们点击下一页看看这个进程会有什么变化。没错,只有这两个参数 params和encSecKey会随之改变,进而response也刷新成了下一页的评论。得出结论:这是通过不同页面的params以及encSecKey参数的不同来向服务器发起获取相应评论的请求。

因此,下一步就是弄清楚不同页面的params以及encSecKey参数是如何改变的,这样我们就能在爬虫程序中通过生成随页面变化的这两个参数,发送至服务器获取相应的response

这两个参数一看就是js加密的,而这个进程的initiator是core.js,因此将点进去并save as将core.js下载到本地查看

python爬虫-爬取网易云音乐歌曲评论

将代码美化后,查找这两个参数,可以看到这两个参数都是bRB5G函数中的变量,而这个函数也就是window.asrea这个函数

python爬虫-爬取网易云音乐歌曲评论

我们暂且不管window.asrea这个函数是如何实现的,先来看看它的四个参数。先不管这四个参数是哪来的,可以先把它们输出来看一下,这时候就需要线上调试js。首先将本地的core.js文件添加几行代码,以便使这四个参数显示出来

python爬虫-爬取网易云音乐歌曲评论

接着选择Fiddler,在Fiddler的AutoResponder页添加Rule,如下图所示

python爬虫-爬取网易云音乐歌曲评论

这步实际就是用本地修改过得core.js文件替换服务器的core.js对请求作出响应。这些设置完之后,清除浏览器缓存,刷新页面,就可以在console里面看到输出的参数了,如图,分别是第一页和第二页的第一个参数值

python爬虫-爬取网易云音乐歌曲评论

python爬虫-爬取网易云音乐歌曲评论

可以根据不同的歌曲和页数多试几次,可以发现rid就是R_SO_4_加上歌曲的id(其实这个参数也是可以没有的),offset就是(评论页数-1) * 20,total在第一页是true,其余是false。
按这样的方式可以得到其余三个参数
第二个参数:
010001
第三个参数:
00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
第四个参数:
0CoJUm6Qyw8W8jud
可以发现,只有第一个参数随页面变化,其余三个参数都是不变的常量。至此,这四个参数我们都能够在程序中通过代码生成了。
那么,现在我们只要知道函数window.asrsea如何处理的就可以了,定位到这个函数发现它其实是一个叫d的函数
python爬虫-爬取网易云音乐歌曲评论
研究过后,你就会发现,i就是一个长度为16的随机字符串,既然是随机的,就直接让他等于16个F了。这个encText明显就是params,encSecKey明显就是encSecKey。而b函数就是一个AES加密,encText的获得经过了两次加密,第一次对d也就是第一个参数加密,key是第四个参数,第二次对第一次加密结果进行加密,key是i。在b函数中我们可以看到
python爬虫-爬取网易云音乐歌曲评论

**偏移量iv是0102030405060708,模式是CBC,那么就不难写出对于第一个参数的加密了。
接下来是第二个参数encSecKey,你会发现c函数是一个RSA加密
python爬虫-爬取网易云音乐歌曲评论
这里传入c的三个参数i是16个F,e是第二个参数,f是第三个参数,全部是固定的值,那么无论歌曲id或评论页数如何变化,这个encSecKey都不随之发生变化,所以这个encSecKey对我们来说就是个常量,抄一个下来就是可以使用的。至此,我们就能在程序中通过代码获取params和encSecKey这两个参数了。

上述看起来很复杂的解析过程,实际上思路很清晰,在这里总结一下
1. 查看下一页评论时发现,网页没有重新加载,但是却刷新了评论,说明是xhr。据此缩小范围,找到返回评论的数据包,获取其中的request header以及params和encSecKey这两个参数
2. 在看不同页评论的时候,发现params和encSecKey这两个参数在变化,至此目标是弄清楚这两个参数随页数变化的原理。因为只有这样才知道如何生成每一页的这两个参数并将其发送至服务器,从而实现全部评论的获取
3. 这个请求进程的initiator是core.js,在这个文件中找到params和encSecKey这两个参数,发现是window.asrea函数中的变量,而这个函数有四个参数。至此目标变成如何得到这四个参数,这样才能得到params和encSecKey。
4. 利用Fiddler的AutoResponder功能,在线上调试js文件。将下载到本地的js文件修改,使之能输出这四个参数。结果发现第一个参数是随页数的变化而改变的变量,其余三个参数都是常量。至此,已经弄清楚这四个参数并且得到了它们,目标变成探索window.asrea函数的功能
5. 研究后发现,window.asrea函数就是调用d函数以及c函数分别对输入的参数进行加密,最终返回params以及encSecKey这两个参数。至此已经获得了params以及encSecKey的生成原理,能够用代码生成它们。

最后,附上代码

# -*- coding:utf-8 -*-

import urllib.request
import http.cookiejar
import urllib.parse
import json
import time
import codecs
from Crypto.Cipher import AES
import base64
import os


class music:

    #初始化
    def __init__(self):
        #设置代理,以防止本地IP被封
        self.proxyUrl = "http://202.106.16.36:3128"
        #request headers,这些信息可以在ntesdoor日志request header中找到,copy过来就行
        self.Headers = {
            'Accept': "*/*",
            'Accept-Language': "zh-CN,zh;q=0.9",
            'Connection': "keep-alive",
            'Host': "music.163.com",
            'User-Agent':"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.79 Safari/537.36"

        }

        # 使用http.cookiejar.CookieJar()创建CookieJar对象
        self.cjar = http.cookiejar.CookieJar()
        # 使用HTTPCookieProcessor创建cookie处理器,并以其为参数构建opener对象
        self.cookie = urllib.request.HTTPCookieProcessor(self.cjar)
        self.opener = urllib.request.build_opener(self.cookie)
        # opener安装为全局
        urllib.request.install_opener(self.opener)
        #第二个参数
        self.second_param = "010001"
        #第三个参数
        self.third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
        #第四个参数
        self.forth_param = "0CoJUm6Qyw8W8jud"

    def get_params(self, page):
        #获取encText,也就是params
        iv = "0102030405060708"
        first_key = self.forth_param
        second_key = 'F' * 16
        if page == 0:
            first_param = '{rid:"", offset:"0", total:"true", limit:"20", csrf_token:""}'
        else:
            offset = str((page - 1) * 20)
            first_param = '{rid:"", offset:"%s", total:"%s", limit:"20", csrf_token:""}' % (offset, 'false')
        self.encText = self.AES_encrypt(first_param, first_key, iv)
        self.encText = self.AES_encrypt(self.encText.decode('utf-8'), second_key, iv)
        return self.encText

    def AES_encrypt(self, text, key, iv):
        #AES加密
        pad = 16 - len(text) % 16
        text = text + pad * chr(pad)
        encryptor = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
        encrypt_text = encryptor.encrypt(text.encode('utf-8'))
        encrypt_text = base64.b64encode(encrypt_text)
        return encrypt_text

    def get_encSecKey(self):
        #获取encSecKey
        encSecKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c"
        return encSecKey

    def get_json(self, url, params, encSecKey):
        # post所包含的参数
        self.post = {
            'params': params,
            'encSecKey': encSecKey,
        }
        # post编码转换
        self.postData = urllib.parse.urlencode(self.post).encode('utf8')
        try:
            #发出一个请求
            self.request = urllib.request.Request(url,self.postData,self.Headers)
        except urllib.error.HTTPError as e:
            print(e.code)
            print(e.read().decode("utf8"))
        #得到响应
        self.response = urllib.request.urlopen(self.request)
        #需要将响应中的内容用read读取出来获得网页代码,网页编码为utf-8
        self.content = self.response.read().decode("utf8")
        #返回获得的网页内容
        return self.content




    def get_hotcomments(self, url):
        #获取热门评论
        params = self.get_params(1)
        encSecKey = self.get_encSecKey()
        content = self.get_json(url, params, encSecKey)
        json_dict = json.loads(content)
        hot_comment = json_dict['hotComments']
        f = open('c:/Users/zsx/Desktop/HotComments.txt', 'w', encoding='utf-8')
        for i in hot_comment:
            #将评论输出至txt文件中
            time_local = time.localtime(int(i['time'] / 1000))  # 将毫秒级时间转换为日期
            dt = time.strftime("%Y-%m-%d %H:%M:%S", time_local)
            f.write('用户: ' + i['user']['nickname'] + '\n')
            f.write('点赞数: ' + str(i['likedCount']) + '\n')
            f.write('发表时间: ' + dt + '\n')
            f.write('评论: ' + i['content'] + '\n')
            f.write('-' * 40 + '\n')
        f.close()



    def get_allcomments(self, url):
        #获取全部评论
        params = self.get_params(1)
        encSecKey = self.get_encSecKey()
        content = self.get_json(url, params, encSecKey)
        json_dict = json.loads(content)
        comments_num = int(json_dict['total'])
        f = open('c:/Users/zsx/Desktop/AllComments.txt', 'w', encoding='utf-8')
        present_page = 0
        if (comments_num % 20 == 0):
            page = comments_num / 20
        else:
            page = int(comments_num / 20) + 1
        print("共有%d页评论" % page)
        print("共有%d条评论" % comments_num)
        # 逐页抓取
        for i in range(page):
            params = self.get_params(i + 1)
            encSecKey = self.get_encSecKey()
            json_text = self.get_json(url, params, encSecKey)
            json_dict = json.loads(json_text)
            present_page = present_page + 1
            for i in json_dict['comments']:
                # 将评论输出至txt文件中
                time_local = time.localtime(int(i['time'] / 1000))# 将毫秒级时间转换为日期
                dt = time.strftime("%Y-%m-%d %H:%M:%S", time_local)
                f.write('用户: ' + i['user']['nickname'] + '\n')
                f.write('点赞数: ' + str(i['likedCount']) + '\n')
                f.write('发表时间: ' + dt + '\n')
                f.write('评论: ' + i['content'] + '\n')
                f.write('-' * 40 + '\n')
            print("%d页抓取完毕" % present_page)

        f.close()








mail = music()
mail.get_hotcomments("https://music.163.com/weapi/v1/resource/comments/R_SO_4_227323?csrf_token")
mail.get_allcomments("https://music.163.com/weapi/v1/resource/comments/R_SO_4_227323?csrf_token")



效果如下

python爬虫-爬取网易云音乐歌曲评论

相关文章: