网易云音乐评论爬取

35 篇文章 1 订阅
14 篇文章 0 订阅

前言

我是个很喜欢听歌的人,手机里面下了几百首歌,而且还会每个月还会增加几首,因为我觉得听歌能让我活得更有趣。平常的我,喜欢听一些流行歌曲或者被翻唱突然又火起来的老歌、无聊时会听些欢快的歌、运动时会听写激昂的歌、伤心时也会听些伤感的歌。
我特别喜欢一句话: 初闻不识曲中意,再闻已是曲中人。 也许是因为我也是曲中人把。。。
所以我想把那些触动我的歌曲的评论利用我所学的知识爬取下来,将他们的故事收集起来细细品读。

话不多说,开始我们的爬虫之旅吧!!!

环境

总体思路

通过跟踪点击评论翻页时发出的请求,得到具体变化参数,然后通过查看js文件破解采用了加密算法生成的paramsencSecKey,但是通过对js脚本的分析,encSecKey参数是不需要破解的,只需要在浏览器获取到encSecKey的值然后复制到代码里,然后模拟POST请求程序就可以啦!!!

详细步骤

  • 打开网易云音乐官网,随便打开一首歌,这里是我以前追的一部剧的主题曲忽而今夏。打开开发者模式,依次点击NetworkXHR,然后到页面里点击下一页。通过查看返回结果追踪到翻页发出的请求。
    在这里插入图片描述
  • 通过查看请求的Headers知道了这条请求是POST请求,传输的参数是paramsencSecKey
    在这里插入图片描述
  • 那么很明显这两个长长的参数是加密过的,因为通过翻页发现每次这两个参数的值都不一样,而且不是明文。
  • 我们就只能通过查看JS文件来模拟加密过程,因为找到对应的加密方法的操作步骤过于复杂,所以我录制了一个操作视频,你们可以慢慢观看。

    查找对应加密方法

  • 将上面找到的两个JS脚本复制出来
  • 脚本一:
    (function() {
        var c0x = NEJ.P,
        eq2x = c0x("nej.g"),
        v0x = c0x("nej.j"),
        k0x = c0x("nej.u"),
        QI7B = c0x("nm.x.ek"),
        l0x = c0x("nm.x");
        if (v0x.bl1x.redefine) return;
        window.GEnc = true;
        var bqv4z = function(cHv1x) {
            var m0x = [];
            k0x.be0x(cHv1x,
            function(cHu1x) {
                m0x.push(QI7B.emj[cHu1x])
            });
            return m0x.join("")
        };
        var cHs1x = v0x.bl1x;
        v0x.bl1x = function(Y0x, e0x) {
            var i0x = {},
            e0x = NEJ.X({},
            e0x),
            lY5d = Y0x.indexOf("?");
            if (window.GEnc && /(^|\.com)\/api/.test(Y0x) && !(e0x.headers && e0x.headers[eq2x.zU9L] == eq2x.Gj1x) && !e0x.noEnc) {
                if (lY5d != -1) {
                    i0x = k0x.gY3x(Y0x.substring(lY5d + 1));
                    Y0x = Y0x.substring(0, lY5d)
                }
                if (e0x.query) {
                    i0x = NEJ.X(i0x, k0x.fP3x(e0x.query) ? k0x.gY3x(e0x.query) : e0x.query)
                }
                if (e0x.data) {
                    i0x = NEJ.X(i0x, k0x.fP3x(e0x.data) ? k0x.gY3x(e0x.data) : e0x.data)
                }
                i0x["csrf_token"] = v0x.gO3x("__csrf");
                Y0x = Y0x.replace("api", "weapi");
                e0x.method = "post";
                delete e0x.query;
                var bYl4p = window.asrsea(JSON.stringify(i0x), bqv4z(["流泪", "强"]), bqv4z(QI7B.md), bqv4z(["爱心", "女孩", "惊恐", "大笑"]));
                e0x.data = k0x.cz1x({
                    params: bYl4p.encText,
                    encSecKey: bYl4p.encSecKey
                })
            }
            cHs1x(Y0x, e0x)
        };
        v0x.bl1x.redefine = true
    })();
    
  • 脚本二:
    function() {
        function a(a) {
            var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
            c = "";
            for (d = 0; a > d; d += 1) e = Math.random() * b.length,
            e = Math.floor(e),
            c += b.charAt(e);
            return c
        }
        function b(a, b) {
            var c = CryptoJS.enc.Utf8.parse(b),
            d = CryptoJS.enc.Utf8.parse("0102030405060708"),
            e = CryptoJS.enc.Utf8.parse(a),
            f = CryptoJS.AES.encrypt(e, c, {
                iv: d,
                mode: CryptoJS.mode.CBC
            });
            return f.toString()
        }
        function c(a, b, c) {
            var d, e;
            return setMaxDigits(131),
            d = new RSAKeyPair(b, "", c),
            e = encryptedString(d, a)
        }
        function d(d, e, f, g) {
            var h = {},
            i = a(16);
            return h.encText = b(d, g),
            h.encText = b(h.encText, i),
            h.encSecKey = c(i, e, f),
            h
        }
        function e(a, b, d, e) {
            var f = {};
            return f.encText = c(a + e, b, d),
            f
        }
        window.asrsea = d,
        window.ecnonasr = e
    } ();
    
  • 先观察脚本一,明显的发现这一段应该是向服务器发出了请求,指定了请求链接请求方式和传输的data的值,而我们观察的重点应该是var bYl4p = window.asrsea这一句生成data的JS脚本代码,还有注意的一点是在这里params变成了encText
    在这里插入图片描述
  • 观察脚本二发现data值对应的window.asrsea在这里又是d方法的返回值
    在这里插入图片描述

源码

# -*- coding: utf-8 -*-
"""
@Author : YYN
@Email  : coderyyn@qq.com
文中的加密算法借鉴了知乎上的一些看法 其中主要是@平胸小仙女
地址为:https://www.zhihu.com/question/36081767
另一种方法:
http://music.163.com/api/v1/resource/comments/R_SO_4_553310138?limit=20&offset=6000

使用方法:
输入歌曲id,然后再在浏览器调试(source)下获取到 encSecKey 和16位随机数
"""
from Crypto.Cipher import AES
import base64
import requests
import json
import time
import random
import codecs

class Music(object):
    def __init__(self):
        print('欢迎来到评论下载器')
        print('我们将根据你输入的歌曲id爬取所有的评论')
        sid=input('你想搜索的歌曲id:\n')
        start_time = time.time() # 开始时间
        #####################
        #调试获得的参数
        r_num='PoJQcHsqewwTmxI2'
        encSecKey='568fdde1124fbb2a653e8f1a382371b0dff28e6f9f266dfcfb5b54db9ec925be18ad612b5b04af49f1d0b024e8c2c9fdbbdea6a41d5af1f88a796a7cc4f2f2d78d012ee77fd52b2d89a7fce57422f81f06e93e47ab54b071da86930369c020ed6610f2cd6bfc56195036d28bb709385d1f9642247ade7c78cb695d24bdc579fe'
        #####################
        self.get_comment(sid,r_num,encSecKey)
        end_time = time.time() #结束时间
        print("总共耗时%f秒:" % (end_time - start_time))
        
    def get_comment(self,sid,r_num,encSecKey):
        #获取首页
        params = self.get_params(sid,1,r_num)#获取params
        data=self.get_data(params,encSecKey)#合成data
        response=self.get_requests(sid,data)
        result=json.loads(response)
        comments_num = int(result['total'])
        if(comments_num % 20 == 0):
            page = int(comments_num/20)
        else:
            page = int(comments_num / 20) + 1
        print("共有%d页评论!" % page)
        for i in range(page):  # 逐页抓取
            all_comments = [] # 存放所有评论
            all_comments.append(u"用户昵称 评论内容 点赞总数 评论时间 用户ID \n") # 头部信息
            params =self.get_params(sid,i+1,r_num)
            encSecKey='568fdde1124fbb2a653e8f1a382371b0dff28e6f9f266dfcfb5b54db9ec925be18ad612b5b04af49f1d0b024e8c2c9fdbbdea6a41d5af1f88a796a7cc4f2f2d78d012ee77fd52b2d89a7fce57422f81f06e93e47ab54b071da86930369c020ed6610f2cd6bfc56195036d28bb709385d1f9642247ade7c78cb695d24bdc579fe'
            data=self.get_data(params,encSecKey)#合成data
            response=self.get_requests(sid,data)
            result=json.loads(response)
            if i == 0:
                print("共有%d条评论!" % comments_num) # 全部评论总数
            for item in result['comments']:
                nickname = item['user']['nickname'] # 昵称
                comment = item['content'] # 评论内容
                likedCount = item['likedCount'] # 点赞总数
                comment_time = item['time'] # 评论时间(时间戳)
                comment_time = time.localtime(float(comment_time/1000))
                comment_time = time.strftime("%Y-%m-%d %H:%M:%S", comment_time)
                userID = item['user']['userId'] # 评论者id
                comment_info = "@"+str(nickname) + "-=" + str(comment) + "-=" + str(likedCount) + "-=" + str(comment_time) + "-=" + str(userID) + "\n"
                all_comments.append(comment_info)
            print("第%d页抓取完毕!" % (i+1))
            self.save_file(sid,all_comments)#存储
    
    def get_requests(self,sid,data):
        url = 'https://music.163.com/weapi/v1/resource/comments/R_SO_4_'+str(sid)+'?csrf_token='
        headers={
                'Host':'music.163.com',
                'Referer':'http://music.163.com/song?id='+str(sid),
                'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'
        }
        response = requests.post(url,headers=headers,data=data,timeout=3)
        return response.text
    
    def get_params(self,sid,page,r_num):# page为页数
        iv="0102030405060708"#偏移量
        first_key='0CoJUm6Qyw8W8jud'#第一次加密的key
        second_key=r_num#第二次加密的key:一个16位的随机字符串,需要更换 来匹配encSecKey
        if(page == 1):
            musicinfo = '{rid:"R_SO_4_'+str(sid)+'", offset:"0", total:"true", limit:"20", csrf_token:""}'
            param = self.AES_encrypt(musicinfo, first_key, iv)
        else:
            offset = str((page-1)*20)
            musicinfo = '{rid:"R_SO_4_'+str(sid)+'", offset:"%s", total:"%s", limit:"20", csrf_token:""}' %(offset,'false')
            param = self.AES_encrypt(musicinfo, first_key, iv)
        param = self.AES_encrypt(param,second_key,iv)
        return param
    
    def AES_encrypt(self,text,key,iv):#加密
        pad = 16 - len(text) % 16
        text = text + pad * chr(pad)
        encryptor = AES.new(key, AES.MODE_CBC, iv)
        encrypt_text = encryptor.encrypt(text)
        encrypt_text = base64.b64encode(encrypt_text)
        encrypt_text = str(encrypt_text, encoding="utf-8") #将字节转化为字符串
        return encrypt_text
    
    def get_data(self,params,encSecKey):
        data={
                'params':params,
                'encSecKey':encSecKey,
        }
        return data
    
    def save_file(self,sid,comments):
        filename = str(sid)+'.txt' #修改歌曲名称
        with codecs.open(filename, 'a', 'utf8') as fp:
            for comment in comments:
                fp.write( comment )           
        
if __name__ == '__main__':
    Music()

我的个人博客网站是:www.coderyyn.cn
上面会不定期分享有关爬虫、算法、环境搭建以及有趣的帖子
欢迎大家一起交流学习

转载请注明

  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值