网易云音乐评论爬取
前言
我是个很喜欢听歌的人,手机里面下了几百首歌,而且还会每个月还会增加几首,因为我觉得听歌能让我活得更有趣。平常的我,喜欢听一些流行歌曲或者被翻唱突然又火起来的老歌、无聊时会听些欢快的歌、运动时会听写激昂的歌、伤心时也会听些伤感的歌。
我特别喜欢一句话: 初闻不识曲中意,再闻已是曲中人。 也许是因为我也是曲中人把。。。
所以我想把那些触动我的歌曲的评论利用我所学的知识爬取下来,将他们的故事收集起来细细品读。
话不多说,开始我们的爬虫之旅吧!!!
环境
- Anaconda3(Python3):Windows10下安装Anaconda3(64位)详细过程
- Python的第三方库:Crypto、base64、requests:
conda install 库名
- Windows10
- Chrome浏览器(谷歌浏览器)
总体思路
通过跟踪点击评论翻页时发出的请求,得到具体变化参数,然后通过查看js文件
破解采用了加密算法生成的params
和encSecKey
,但是通过对js脚本
的分析,encSecKey
参数是不需要破解的,只需要在浏览器获取到encSecKey
的值然后复制到代码里,然后模拟POST
请求程序就可以啦!!!
详细步骤
- 打开网易云音乐官网,随便打开一首歌,这里是我以前追的一部剧的主题曲
忽而今夏
。打开开发者模式,依次点击Network
、XHR
,然后到页面里点击下一页。通过查看返回结果追踪到翻页发出的请求。 - 通过查看请求的
Headers
知道了这条请求是POST
请求,传输的参数是params
和encSecKey
- 那么很明显这两个长长的参数是加密过的,因为通过翻页发现每次这两个参数的值都不一样,而且不是明文。
- 我们就只能通过查看
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
上面会不定期分享有关爬虫、算法、环境搭建以及有趣的帖子
欢迎大家一起交流学习
转载请注明
还没有评论,来说两句吧...