网易云音乐评论爬取

短命女 2022-03-16 13:50 514阅读 0赞

前言

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

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

环境

  • Anaconda3(Python3):Windows10下安装Anaconda3(64位)详细过程
  • Python的第三方库:Crypto、base64、requests:conda install 库名
  • Windows10
  • Chrome浏览器(谷歌浏览器)

总体思路

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

详细步骤

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

    1. (function() {
    2. var c0x = NEJ.P,
    3. eq2x = c0x("nej.g"),
    4. v0x = c0x("nej.j"),
    5. k0x = c0x("nej.u"),
    6. QI7B = c0x("nm.x.ek"),
    7. l0x = c0x("nm.x");
    8. if (v0x.bl1x.redefine) return;
    9. window.GEnc = true;
    10. var bqv4z = function(cHv1x) {
    11. var m0x = [];
    12. k0x.be0x(cHv1x,
    13. function(cHu1x) {
    14. m0x.push(QI7B.emj[cHu1x])
    15. });
    16. return m0x.join("")
    17. };
    18. var cHs1x = v0x.bl1x;
    19. v0x.bl1x = function(Y0x, e0x) {
    20. var i0x = {},
    21. e0x = NEJ.X({},
    22. e0x),
    23. lY5d = Y0x.indexOf("?");
    24. if (window.GEnc && /(^|\.com)\/api/.test(Y0x) && !(e0x.headers && e0x.headers[eq2x.zU9L] == eq2x.Gj1x) && !e0x.noEnc) {
    25. if (lY5d != -1) {
    26. i0x = k0x.gY3x(Y0x.substring(lY5d + 1));
    27. Y0x = Y0x.substring(0, lY5d)
    28. }
    29. if (e0x.query) {
    30. i0x = NEJ.X(i0x, k0x.fP3x(e0x.query) ? k0x.gY3x(e0x.query) : e0x.query)
    31. }
    32. if (e0x.data) {
    33. i0x = NEJ.X(i0x, k0x.fP3x(e0x.data) ? k0x.gY3x(e0x.data) : e0x.data)
    34. }
    35. i0x["csrf_token"] = v0x.gO3x("__csrf");
    36. Y0x = Y0x.replace("api", "weapi");
    37. e0x.method = "post";
    38. delete e0x.query;
    39. var bYl4p = window.asrsea(JSON.stringify(i0x), bqv4z(["流泪", "强"]), bqv4z(QI7B.md), bqv4z(["爱心", "女孩", "惊恐", "大笑"]));
    40. e0x.data = k0x.cz1x({
    41. params: bYl4p.encText,
    42. encSecKey: bYl4p.encSecKey
    43. })
    44. }
    45. cHs1x(Y0x, e0x)
    46. };
    47. v0x.bl1x.redefine = true
    48. })();
  • 脚本二:

    1. function() {
    2. function a(a) {
    3. var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
    4. c = "";
    5. for (d = 0; a > d; d += 1) e = Math.random() * b.length,
    6. e = Math.floor(e),
    7. c += b.charAt(e);
    8. return c
    9. }
    10. function b(a, b) {
    11. var c = CryptoJS.enc.Utf8.parse(b),
    12. d = CryptoJS.enc.Utf8.parse("0102030405060708"),
    13. e = CryptoJS.enc.Utf8.parse(a),
    14. f = CryptoJS.AES.encrypt(e, c, {
    15. iv: d,
    16. mode: CryptoJS.mode.CBC
    17. });
    18. return f.toString()
    19. }
    20. function c(a, b, c) {
    21. var d, e;
    22. return setMaxDigits(131),
    23. d = new RSAKeyPair(b, "", c),
    24. e = encryptedString(d, a)
    25. }
    26. function d(d, e, f, g) {
    27. var h = {},
    28. i = a(16);
    29. return h.encText = b(d, g),
    30. h.encText = b(h.encText, i),
    31. h.encSecKey = c(i, e, f),
    32. h
    33. }
    34. function e(a, b, d, e) {
    35. var f = {};
    36. return f.encText = c(a + e, b, d),
    37. f
    38. }
    39. window.asrsea = d,
    40. window.ecnonasr = e
    41. } ();
  • 先观察脚本一,明显的发现这一段应该是向服务器发出了请求,指定了请求链接请求方式和传输的data的值,而我们观察的重点应该是var bYl4p = window.asrsea这一句生成data的JS脚本代码,还有注意的一点是在这里params变成了encText
    在这里插入图片描述
  • 观察脚本二发现data值对应的window.asrsea在这里又是d方法的返回值
    在这里插入图片描述

源码

  1. # -*- coding: utf-8 -*-
  2. """
  3. @Author : YYN
  4. @Email : coderyyn@qq.com
  5. 文中的加密算法借鉴了知乎上的一些看法 其中主要是@平胸小仙女
  6. 地址为:https://www.zhihu.com/question/36081767
  7. 另一种方法:
  8. http://music.163.com/api/v1/resource/comments/R_SO_4_553310138?limit=20&offset=6000
  9. 使用方法:
  10. 输入歌曲id,然后再在浏览器调试(source)下获取到 encSecKey 和16位随机数
  11. """
  12. from Crypto.Cipher import AES
  13. import base64
  14. import requests
  15. import json
  16. import time
  17. import random
  18. import codecs
  19. class Music(object):
  20. def __init__(self):
  21. print('欢迎来到评论下载器')
  22. print('我们将根据你输入的歌曲id爬取所有的评论')
  23. sid=input('你想搜索的歌曲id:\n')
  24. start_time = time.time() # 开始时间
  25. #####################
  26. #调试获得的参数
  27. r_num='PoJQcHsqewwTmxI2'
  28. encSecKey='568fdde1124fbb2a653e8f1a382371b0dff28e6f9f266dfcfb5b54db9ec925be18ad612b5b04af49f1d0b024e8c2c9fdbbdea6a41d5af1f88a796a7cc4f2f2d78d012ee77fd52b2d89a7fce57422f81f06e93e47ab54b071da86930369c020ed6610f2cd6bfc56195036d28bb709385d1f9642247ade7c78cb695d24bdc579fe'
  29. #####################
  30. self.get_comment(sid,r_num,encSecKey)
  31. end_time = time.time() #结束时间
  32. print("总共耗时%f秒:" % (end_time - start_time))
  33. def get_comment(self,sid,r_num,encSecKey):
  34. #获取首页
  35. params = self.get_params(sid,1,r_num)#获取params
  36. data=self.get_data(params,encSecKey)#合成data
  37. response=self.get_requests(sid,data)
  38. result=json.loads(response)
  39. comments_num = int(result['total'])
  40. if(comments_num % 20 == 0):
  41. page = int(comments_num/20)
  42. else:
  43. page = int(comments_num / 20) + 1
  44. print("共有%d页评论!" % page)
  45. for i in range(page): # 逐页抓取
  46. all_comments = [] # 存放所有评论
  47. all_comments.append(u"用户昵称 评论内容 点赞总数 评论时间 用户ID \n") # 头部信息
  48. params =self.get_params(sid,i+1,r_num)
  49. encSecKey='568fdde1124fbb2a653e8f1a382371b0dff28e6f9f266dfcfb5b54db9ec925be18ad612b5b04af49f1d0b024e8c2c9fdbbdea6a41d5af1f88a796a7cc4f2f2d78d012ee77fd52b2d89a7fce57422f81f06e93e47ab54b071da86930369c020ed6610f2cd6bfc56195036d28bb709385d1f9642247ade7c78cb695d24bdc579fe'
  50. data=self.get_data(params,encSecKey)#合成data
  51. response=self.get_requests(sid,data)
  52. result=json.loads(response)
  53. if i == 0:
  54. print("共有%d条评论!" % comments_num) # 全部评论总数
  55. for item in result['comments']:
  56. nickname = item['user']['nickname'] # 昵称
  57. comment = item['content'] # 评论内容
  58. likedCount = item['likedCount'] # 点赞总数
  59. comment_time = item['time'] # 评论时间(时间戳)
  60. comment_time = time.localtime(float(comment_time/1000))
  61. comment_time = time.strftime("%Y-%m-%d %H:%M:%S", comment_time)
  62. userID = item['user']['userId'] # 评论者id
  63. comment_info = "@"+str(nickname) + "-=" + str(comment) + "-=" + str(likedCount) + "-=" + str(comment_time) + "-=" + str(userID) + "\n"
  64. all_comments.append(comment_info)
  65. print("第%d页抓取完毕!" % (i+1))
  66. self.save_file(sid,all_comments)#存储
  67. def get_requests(self,sid,data):
  68. url = 'https://music.163.com/weapi/v1/resource/comments/R_SO_4_'+str(sid)+'?csrf_token='
  69. headers={
  70. 'Host':'music.163.com',
  71. 'Referer':'http://music.163.com/song?id='+str(sid),
  72. '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'
  73. }
  74. response = requests.post(url,headers=headers,data=data,timeout=3)
  75. return response.text
  76. def get_params(self,sid,page,r_num):# page为页数
  77. iv="0102030405060708"#偏移量
  78. first_key='0CoJUm6Qyw8W8jud'#第一次加密的key
  79. second_key=r_num#第二次加密的key:一个16位的随机字符串,需要更换 来匹配encSecKey
  80. if(page == 1):
  81. musicinfo = '{rid:"R_SO_4_'+str(sid)+'", offset:"0", total:"true", limit:"20", csrf_token:""}'
  82. param = self.AES_encrypt(musicinfo, first_key, iv)
  83. else:
  84. offset = str((page-1)*20)
  85. musicinfo = '{rid:"R_SO_4_'+str(sid)+'", offset:"%s", total:"%s", limit:"20", csrf_token:""}' %(offset,'false')
  86. param = self.AES_encrypt(musicinfo, first_key, iv)
  87. param = self.AES_encrypt(param,second_key,iv)
  88. return param
  89. def AES_encrypt(self,text,key,iv):#加密
  90. pad = 16 - len(text) % 16
  91. text = text + pad * chr(pad)
  92. encryptor = AES.new(key, AES.MODE_CBC, iv)
  93. encrypt_text = encryptor.encrypt(text)
  94. encrypt_text = base64.b64encode(encrypt_text)
  95. encrypt_text = str(encrypt_text, encoding="utf-8") #将字节转化为字符串
  96. return encrypt_text
  97. def get_data(self,params,encSecKey):
  98. data={
  99. 'params':params,
  100. 'encSecKey':encSecKey,
  101. }
  102. return data
  103. def save_file(self,sid,comments):
  104. filename = str(sid)+'.txt' #修改歌曲名称
  105. with codecs.open(filename, 'a', 'utf8') as fp:
  106. for comment in comments:
  107. fp.write( comment )
  108. if __name__ == '__main__':
  109. Music()

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

转载请注明

发表评论

表情:
评论列表 (有 0 条评论,514人围观)

还没有评论,来说两句吧...

相关阅读

    相关 音乐评论

    前言 我是个很喜欢听歌的人,手机里面下了几百首歌,而且还会每个月还会增加几首,因为我觉得听歌能让我活得更有趣。平常的我,喜欢听一些流行歌曲或者被翻唱突然又火起来的老歌、无