【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java

Python/Java调用网易云音乐搜歌功能并爬取评论

  • 网易云音乐的加密格式
    • 1、搜索功能抓包解密
    • 2、搜索网易云歌曲代码Java/Python
    • 3、仍可用的旧版API抓取评论
    • 4、【懒人用】没有加密的评论抓取API
    • 5、最新的API抓取评论

网易云音乐的加密格式

网易云音乐是通过用ajax和json来实现从服务器加载评论信息,所以想直接用requests.get是无法调用到搜索API和获取评论的API,只能通过慢慢分析和解密JS文件来慢慢摸清加密API的参数,因为我不太懂前端的知识,是根据知乎大佬https://www.zhihu.com/question/36081767/answer/140287795的思路来分析JS的,我就是在此文章的帮助下写出了网易云音乐搜索功能的代码,在此由衷感谢这位大牛。

知乎大佬链接1

但是因为年代久远,网易云音乐现在的API和json和这位答主的有少许出入(当然不影响原代码的使用),而且当时题主是用Python2写的代码,我就用Python3和Java重新写了一遍,那我就用自己的理解来解释一下这位大佬的思路,以便让每个像我一样的初学者都能看懂。

1、搜索功能抓包解密

按照这位知乎大佬的思路,我们还是先用fiddler抓包 (前三步不是必须的,如果你能直接通过浏览器的F12开发者功能找到请求API的js文件,可以之间跳到第四步)

  1. 打开fiddler ,先开启fiddler的断点功能,在访问url之前截获请求;【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第1张图片

  2. 按下Ctrl+X把所有乱七八糟的请求都清空了【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第2张图片
    ,然后立刻加载网易云音乐的搜索页面:https://music.163.com/#/search/m/?s=%E7%A8%BB%E9%A6%99&type=1,我在这里的搜索内容以"稻香"为例子【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第3张图片
    经过一步一步的调试网页的请求,在运行了这个链接后https://music.163.com/weapi/cloudsearch/get/web?csrf_token=
    浏览器已经能看到了歌曲搜索内容的页面了【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第4张图片
    说明这个链接https://music.163.com/weapi/cloudsearch/get/web?csrf_token= 很可能是歌曲搜索功能的API接口,为了验证这个想法,先把fiddler的断点功能关了【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第5张图片

接着我们先打开浏览器的F12键,然后刷新页面看看网页的加载信息
可以很明显的发现这个链接就是搜歌的API【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第6张图片
返回值response里面就是我们需要的json文件,里面包含了搜索到的歌曲信息
【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第7张图片

  1. 抓到了歌曲搜索的API后,可以看出这是个POST请求,而关键参数就是params和encSecKey这两个参数,这很明显是经过加密后的参数,所以任务就是破解加密的参数。【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第8张图片
    那么我们先分析发出这个请求的JS文件,在initiator中找到发起请求的JS文件分析代码【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第9张图片

  2. 这个JS代码缩进被压缩的太厉害了,就复制下来在sublime中格式化一下【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第10张图片
    在sublime text中用插件JSFormat的快捷键Ctrl alt + f美化代码(不过按这个快捷键我的fiddler老是跳出了只能先关闭fiddler再美化了)【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第11张图片
    这样看可读性好多了,不过这代码太长了差点把我电脑搞死机了

  3. 接着安装那位知乎大神的思路,查询参数params和encSecKey【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第12张图片
    在js代码中跳到了下面位置 【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第13张图片
    在此引用知乎大佬的分析(某些变量不一样但代码结构都是一样的):【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第14张图片

当然,这位大佬说的有点儿抽象,对于我这样的JS真小白来说是跟不上大佬的思路的,自己慢慢分析才知道意思大概,根据下面代码
在这里插入图片描述
分析可知,参数paramsencSecKey的值由bVX2x决定,而bVX2x又是由函数window.asrsea得到的,那现在研究重心是在函数window.asrsea,由代码可知给这个函数的第一个参数就是i3x(知乎大佬文章的参数是bl,因为年代久远这些变量改名了,但总体结构没改变),而这个i3x的值是多少呢我们不知道,只能通过函数,所以我们想在浏览器控制台显示两个变量的值,给JS加个window.console.warn(i3x);window.console.warn(bVX2x.encText);
【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第15张图片
然后打开我们fiddler,添加规则,用本地JS文件代替请求的JS文件【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第16张图片

  1. 然后我们的重头戏就来了,我们要通过看浏览器控制台的输出得到变量的关键信息
    【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第17张图片
    (如果控制台一直没有信息输出,而且刷新也没有效果,此时可以把窗口关闭,一般出现打开一个窗口就行了)
    然后我们对比一下Network里面params和enSecKey的值与控制台值的对比
    【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第18张图片
    可以发现这里的params和控制台的值相等,所以加密的关键就是下面的那串字典【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第19张图片
    那么现在已经确定了,我们只要经过函数window.asrsea的处理就能得到bVX2x的值了,而且我们只要关注传入该函数的第一个参数就行了,而第一个参数**JSON.stringify(i3x)**在打印台打印的结果就是:
    {“hlpretag”:"",“hlposttag”:"",“id”:“33789165”,“s”:“稻香”,“type”:“1”,“offset”:“0”,“total”:“true”,“limit”:“30”,“csrf_token”:""}
    经过后面的分析可知道,第一个参数里面的值 offset,total,limit是决定返回搜索到的哪一页的结果,而且无论歌曲id或评论页数如何变化,这个encSecKey都不随之发生变化,所以这个encSecKey对我们来说就是个常量,抄一个下来就是可以使用的(并且结果后期验证,encSecKey值一点儿都不重要,就是个常数,无论是访问搜索歌曲的API还是评论的API都不影响)
    在这里插入图片描述
    而剩下的三个参数我们可以通过windows.console.info打印到控制台,在这里可以得到三个参数的值分别是:
    第二个参数 bqK4O([“流泪”, “强”])
    010001

第三个参数 bqK4O(Ya8S.md):00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7

第四个参数 bqK4O([“爱心”, “女孩”, “惊恐”, “大笑”])
0CoJUm6Qyw8W8jud

  1. 然后我们可以得到这个window.asrsea函数实际是函数d(d, e, f, g)在这里插入图片描述
    可以看出变量i是个长度为16的随机字符串,既然是随机的,就直接让他等于16个F:在这里插入图片描述
    因为我没学过JS,而且这个return返回值的格式是我在任何代码中都没见过的(怪我才疏学浅),我开始以为是像Python那样返回的是四个值,但经过分析后还是觉得函数返回的就是h,因为后面的params是等于bVX6R.encText的,而很明显返回值h之前的那三个赋值代码就是获取params和encSecKey的;那么既然知道了这三个是赋值代码,那就能明显看出h.encText是经过b函数两次处理的结果,而b函数的返回值就是经过AES128 cbc pkcs7加密后的结果
    【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第20张图片

  2. 现在整体思路就理清了,我们主要任务就是写出实现 AES128 cbc pkcs7加密的函数,而输入的参数经过两次加密 的返回的结果就是我们需要的params。

  3. 而获取歌曲评论的分析是和抓取搜索差不多的,而且那位知乎答主的代码(Python2版本)就是抓取评论的,这里就不再赘述了,就直接在最后放上代码就行了

2、搜索网易云歌曲代码Java/Python

Python3版搜索歌曲代码

# -*- coding: utf-8 -*-
import requests
import json
import pprint
from urllib import parse
import base64
from Crypto.Cipher import AES


Search_api='https://music.163.com/weapi/cloudsearch/get/web?csrf_token='


search_headers = {
   'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36',
   'Referer':'https://music.163.com/search/',
   'Origin':'http://music.163.com',
   'Host':'music.163.com'
}


second_param = "010001"
third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
forth_param = "0CoJUm6Qyw8W8jud"


def pkcs7padding(text):
    """
    明文使用PKCS7填充
    最终调用AES加密方法时,传入的是一个byte数组,要求是16的整数倍,因此需要对明文进行处理
    :param text: 待加密内容(明文)
    :return:
    """
    bs = AES.block_size  # 16
    length = len(text)
    bytes_length = len(bytes(text, encoding='utf-8'))
    # tips:utf-8编码时,英文占1个byte,而中文占3个byte
    padding_size = length if(bytes_length == length) else bytes_length
    padding = bs - padding_size % bs
    # tips:chr(padding)看与其它语言的约定,有的会使用'\0'
    padding_text = chr(padding) * padding
    return text + padding_text

def aes_en(text,key,iv):
    #print('pkcs7padding处理之前:',text)
    text =pkcs7padding(text)
    #print('pkcs7padding处理之后:',text)
    #entext = text + ('\0' * add)
    # 初始化加密器
    aes = AES.new(key.encode(encoding='utf-8'), AES.MODE_CBC, iv)
    enaes_text = str(base64.b64encode(aes.encrypt(str.encode(text))),encoding='utf-8')
    return enaes_text

def get_params(first_param):
    iv = b"0102030405060708"
    first_key = forth_param
    second_key = 16 * 'F'
    
    h_encText = aes_en(first_param, first_key, iv)
    h_encText = aes_en(h_encText, second_key, iv)
    return h_encText

if __name__ == "__main__":
    
    search_name = '周杰伦'
    page = "0"     #必须是30的整数倍,因为下面first_param参数中的limit值为30,意思是每页最多显示30个评论
    if page != 0:
        if_firstPage = "true"   #如果是第一页(即page=0)则if_firstPage为false,否则都为true
    else:
        if_firstPage = "false"   #page为0,这是评论第一页则if_firstPage为false
        
    first_param = "{\"hlpretag\":\"\",\"hlposttag\":\"\",\"s\":\"%s\",\"type\":\"1\",\"offset\":\"%s\",\"total\":\"%s\",\"limit\":\"30\",\"csrf_token\":\"\"}" %(search_name,page,if_firstPage)

    user_data = {
   'params': get_params(first_param),
   'encSecKey': "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c"
    }

    response = requests.post(Search_api,headers=search_headers,data=user_data)

    #pprint.pprint(response.text)
    json_dict = json.loads(response.text)
    
    print (json_dict)
    print('----------------------------------')
    
    for directory_temp in json_dict['result']['songs']:
    
        song_name = directory_temp['name'];
        song_id = directory_temp['id']
        
        singer_name = directory_temp['ar'][0]['name']
        singer_id = directory_temp['ar'][0]['id']
        
        album_name = directory_temp['al']['name']
        album_id = directory_temp['al']['id']
        
        print('歌名:'+song_name)
        print('歌名ID',song_id)
        print('歌手:'+singer_name)
        print('歌手ID:',singer_id)
        print('专辑:'+album_name)
        print('专辑ID:',album_id)
        
        print('++++++++++++++++++++++++++++++++')

JAVA版搜索歌曲代码

package com.example.api;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Hex;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;


public class EncryptTools {
	
	public static void main(String[] args) throws Exception {
		searchAPI("稻香","0");	//第二个参数是页数,0就是第一页
	}
	
	
    //AES加密
    public static String encrypt(String text, String secKey) throws Exception {
        byte[] raw = secKey.getBytes("utf-8");
        int padding_size = text.getBytes(StandardCharsets.UTF_8).length;
        int padding = 16 - padding_size % 16;
        String padding_text = String.join("", Collections.nCopies(padding, Character.toString ((char) padding)));
        text = text+ padding_text;

        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        // "算法/模式/补码方式"
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 
        // 使用CBC模式,需要一个向量iv,可增加加密算法的强度
        IvParameterSpec iv = new IvParameterSpec("0102030405060708".getBytes("utf-8"));
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        byte[] encrypted = cipher.doFinal(text.getBytes("utf-8"));
        return Base64.getEncoder().encodeToString(encrypted);
    }
    
  //字符填充
  public static String zfill(String result, int n) {
        if (result.length() >= n) {
            result = result.substring(result.length() - n, result.length());
        } else {
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = n; i > result.length(); i--) {
                stringBuilder.append("0");
            }
            stringBuilder.append(result);
            result = stringBuilder.toString();
        }
        return result;
    }
  

  public static void searchAPI(String input_content,String page) throws Exception {
      //私钥,随机16位字符串(自己可改)
      String secKey = "FFFFFFFFFFFFFFFF";

      String text = String.format("{\"hlpretag\":\"\",\"hlposttag\":\"\",\"s\":\"%s\",\"type\":\"1\",\"offset\":\"%s\",\"total\":\"%s\",\"limit\":\"30\",\"csrf_token\":\"\"}", input_content, page, page == "0" ? "true":"false");
      String modulus = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";
      String nonce = "0CoJUm6Qyw8W8jud";
      String pubKey = "010001";
      //2次AES加密,得到params
      String params = EncryptTools.encrypt(EncryptTools.encrypt(text, nonce), secKey);
      StringBuffer stringBuffer = new StringBuffer(secKey);
      //逆置私钥
      secKey = stringBuffer.reverse().toString();
      String hex = Hex.encodeHexString(secKey.getBytes());
      BigInteger bigInteger1 = new BigInteger(hex, 16);
      BigInteger bigInteger2 = new BigInteger(pubKey, 16);
      BigInteger bigInteger3 = new BigInteger(modulus, 16);
      //RSA加密计算
      BigInteger bigInteger4 = bigInteger1.pow(bigInteger2.intValue()).remainder(bigInteger3);
      String encSecKey= Hex.encodeHexString(bigInteger4.toByteArray());
      //字符填充
      encSecKey= EncryptTools.zfill(encSecKey, 256);
      //评论获取
      Document document = Jsoup.connect("https://music.163.com/weapi/cloudsearch/get/web?csrf_token=").cookie("appver", "1.5.0.75771")
              .header("Referer", "https://music.163.com/search/").data("params", params).data("encSecKey", encSecKey)
              .ignoreContentType(true).post();
      
      JSONObject json = JSONObject.parseObject(document.text());

      String temp_string = json.getString("result");
      
      System.out.println(temp_string);
      
      List<HashMap> list = JSONObject.parseArray(JSONObject.parseObject(temp_string).getString("songs"), HashMap.class);
      
      System.out.println(JSONObject.parseObject(temp_string).getString("songs"));
      System.out.println();
      
      for (HashMap<String,Object> directory_temp : list) {
    	  String song_name = directory_temp.get("name").toString();
    	  String song_id = directory_temp.get("id").toString();
    	  
    	  String singer_info = directory_temp.get("ar").toString();
    	  JSONArray test_json = JSONObject.parseArray(singer_info);
    	  JSONObject jsonTemp = JSONObject.parseObject(test_json.get(0).toString()); 

    	  System.out.println(jsonTemp);
    	  String singer_name = jsonTemp.getString("name");
    	  String singer_id = jsonTemp.getString("id").toString();
    	  System.out.println("歌手姓名是:"+singer_name);
    	  System.out.println("歌手ID是:"+singer_id);
        
    	  String album_info = directory_temp.get("al").toString();
    	  jsonTemp = JSONObject.parseObject(album_info.toString()); 
    	  System.out.println(jsonTemp);
    	  String album_name = jsonTemp.getString("name");
		  String album_id = jsonTemp.getString("id");
		  String album_pic = jsonTemp.getString("picUrl");
		  System.out.println("专辑名字是:"+album_name);
    	  System.out.println("专辑ID是:"+album_id);
    	  System.out.println("专辑图片链接是:"+album_pic);
    	  System.out.println("-----------------------------------");
      }   
      //System.out.println("这搜到的歌曲:" + document.text());
  }
}

3、仍可用的旧版API抓取评论

因为我最开始的需求只是抓取歌曲的评论,所以我没有一步一步按照知乎大佬的步骤去抓包和破解,只是直接用他写的代码也实现了功能,但我最近又需要新加一个搜索功能,并且通过网易云的搜索获取歌曲并抓取评论,所以我才从头到尾跟着那位答主的回答走了一遍,然后发现网易云早就更换了获取评论的API,新版本的评论API要比旧版本难用的多,因为多了一个参数course,这个course参数很奇葩,我会在后面详细的说,我先在这里给出抓取评论的Python3/Java代码

Python3版抓取歌曲评论代码

# -*- coding: utf-8 -*-
from Crypto.Cipher import AES
import base64
import requests
import json


headers = {
    'Cookie': 'appver=1.5.0.75771;',
    'Referer': 'http://music.163.com/'
}

first_param = '{rid:"", offset:"0", total:"true", limit:"20", csrf_token:""}'
#offset就是(评论页数-1* 20,total在第一页是true,其余是false

second_param = "010001"
third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
forth_param = "0CoJUm6Qyw8W8jud"

def get_params():
    iv = b"0102030405060708"
    first_key = forth_param
    second_key = 16 * 'F'
    h_encText = AES_encrypt(first_param, first_key, iv)
    h_encText = AES_encrypt(h_encText, second_key, iv)
    return h_encText


def get_encSecKey():
    encSecKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c"
    return encSecKey
    

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


def get_json(url, params, encSecKey):
    data = {
         "params": params,
         "encSecKey": encSecKey
    }
    response = requests.post(url, headers=headers, data=data)
    return response.content


if __name__ == "__main__":
    url = "http://music.163.com/weapi/v1/resource/comments/R_SO_4_452601948/?csrf_token="   #  R_SO_4_加上歌曲的id就是抓取评论的API
    
    
    params = get_params();          #获取 first_param 经过AES两次加密后的结果
    encSecKey = get_encSecKey();
    json_text = get_json(url, params, encSecKey)
    json_dict = json.loads(json_text)
    
    try:
        print (json_dict['total'])
        for item in json_dict['comments']:
            print (item['content'])
    except:
        print (json_dict)

运行结果:
【逆向JS】调用网易云音乐搜歌功能并爬取评论Python/Java_第21张图片
JAVA版抓取歌曲评论代码

package com.example.api;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Hex;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;


public class EncryptTools {
	
	public static void main(String[] args) throws Exception {
		EncryptTools.commentAPI();
	}
	
	
    //AES加密
    public static String encrypt(String text, String secKey) throws Exception {
        byte[] raw = secKey.getBytes("utf-8");
        int padding_size = text.getBytes(StandardCharsets.UTF_8).length;
        int padding = 16 - padding_size % 16;
        String padding_text = String.join("", Collections.nCopies(padding, Character.toString ((char) padding)));
        text = text+ padding_text;

        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        // "算法/模式/补码方式"
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 
        // 使用CBC模式,需要一个向量iv,可增加加密算法的强度
        IvParameterSpec iv = new IvParameterSpec("0102030405060708".getBytes("utf-8"));
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
        byte[] encrypted = cipher.doFinal(text.getBytes("utf-8"));
        return Base64.getEncoder().encodeToString(encrypted);
    }
    
  //字符填充
  public static String zfill(String result, int n) {
        if (result.length() >= n) {
            result = result.substring(result.length() - n, result.length());
        } else {
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = n; i > result.length(); i--) {
                stringBuilder.append("0");
            }
            stringBuilder.append(result);
            result = stringBuilder.toString();
        }
        return result;
    }
  
  public static void commentAPI() throws Exception {
      //#私钥,随机16位字符串(自己可改)
      String secKey = "cd859f54539b24b7";
      String text = "{rid:\"\", offset:\"0\", total:\"true\", limit:\"20\", csrf_token:\"\"}";
      //offset就是(评论页数-1) * 20,total在第一页是true,其余是false
      
      String modulus = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7";
      String nonce = "0CoJUm6Qyw8W8jud";
      String pubKey = "010001";
      //2次AES加密,得到params
      String params = EncryptTools.encrypt(EncryptTools.encrypt(text, nonce), secKey);
      StringBuffer stringBuffer = new StringBuffer(secKey);
      //逆置私钥
      secKey = stringBuffer.reverse().toString();
      String hex = Hex.encodeHexString(secKey.getBytes());
      BigInteger bigInteger1 = new BigInteger(hex, 16);
      BigInteger bigInteger2 = new BigInteger(pubKey, 16);
      BigInteger bigInteger3 = new BigInteger(modulus, 16);
      //RSA加密计算
      BigInteger bigInteger4 = bigInteger1.pow(bigInteger2.intValue()).remainder(bigInteger3);
      String encSecKey= Hex.encodeHexString(bigInteger4.toByteArray());
      //字符填充
      encSecKey= EncryptTools.zfill(encSecKey, 256);
      //评论获取
      Document document = Jsoup.connect("http://music.163.com/weapi/v1/resource/comments/R_SO_4_452601948/").cookie("appver", "1.5.0.75771")
              .header("Referer", "http://music.163.com/").data("params", params).data("encSecKey", encSecKey)
              .ignoreContentType(true).post();
      System.out.println("评论:" + document.text());
  }
}

其实返回的数据里面还有被回复的评论,但当时没注意,现在也懒得写了,需要的可以自己写一下

4、【懒人用】没有加密的评论抓取API

是的,你没有看错,网易云就是有个bug式未加密API,如果不是看了知乎我也不敢相信,话说知乎大佬还是牛人多啊,可看

  • 肖飞的回答 - 知乎
    https://www.zhihu.com/question/36081767/answer/310726622

这个使用方法就很简单了,直接进去这个链接:http://music.163.com/api/v1/resource/comments/R_SO_4_516997458
剩下的你们都懂了把,不过这个只是第一页的评论,如果想改变页数就直接写成下面格式:http://music.163.com/api/v1/resource/comments/R_SO_4_516997458?limit=20&offset=40
limit和offset就是改变页数的参数,我在代码注释里面写的有,limit是每页获取的(最大)评论数,offset就是(评论页数-1) * 20

5、最新的API抓取评论

我前面说了,因为年代久远,网易云已经更改web端的歌曲评论获取API,虽然老版本的API依然能用,甚至还有未加密的bug级的API获取,但还是很有可能会被官方封掉(虽然好几年过去了依然能用),所以我就写下最新获取评论的API:
https://music.163.com/weapi/comment/resource/comments/get?csrf_token=的参数破解思路。
这个加密格式和要传入的参数格式和前面分析的都是一样的,唯一不一样的就是最关键的参数first_param的内容,因为我们能发现这个最新的API是没有歌曲ID参数的,所以歌曲ID参数是要放到里面的,具体格式就是:
{“rid”:“R_SO_4_33789165”,“threadId”:“R_SO_4_33789165”,“pageNo”:“1”,“pageSize”:“20”,“cursor”:"-1",“offset”:“40”,“orderType”:“1”,“csrf_token”:""}
但这个只是访问第一页的评论的参数格式,第一页以后的里面的值就变化多了,虽然第一页对于抓取热评来说也够了,但出于刨根问底的精神,我还是想要抓取第一页以后的评论,经过分析,除了要把pageNo改成想要的页数外,还要修改cursor的值,而这个cursor的值可以固定也可以不固定,不过我可以明确的告诉你每首歌的cursor肯定不一样,因为这个cursor是根据每一页最后一个用户的评论时间确定的,而且会对offset有影响的。
代码和具体的分析以后有机会再说吧,今天写博客太累了,而且这些天我还要写个JS逆向分析抓取Oh漫画里面漫画的博客,有点儿累了,睡了睡了。


  1. https://www.zhihu.com/question/36081767/answer/140287795 ↩︎

你可能感兴趣的:(python,js,java,网易)