1 changed files with 179 additions and 0 deletions
@ -0,0 +1,179 @@ |
|||
import org.apache.commons.lang3.RandomStringUtils; |
|||
import org.apache.http.HttpEntity; |
|||
import org.apache.http.client.methods.CloseableHttpResponse; |
|||
import org.apache.http.client.methods.HttpPost; |
|||
import org.apache.http.entity.StringEntity; |
|||
import org.apache.http.impl.client.CloseableHttpClient; |
|||
import org.apache.http.impl.client.HttpClients; |
|||
import org.apache.http.util.EntityUtils; |
|||
import org.bouncycastle.jce.provider.BouncyCastleProvider; |
|||
import com.alibaba.fastjson.JSON; |
|||
import com.alibaba.fastjson.JSONArray; |
|||
import com.alibaba.fastjson.JSONObject; |
|||
|
|||
import javax.crypto.Cipher; |
|||
import javax.crypto.spec.IvParameterSpec; |
|||
import javax.crypto.spec.SecretKeySpec; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.security.Security; |
|||
import java.util.Base64; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 网易云音乐热门评论爬虫 |
|||
*/ |
|||
public class NeteaseCommentCrawler { |
|||
// 网易云固定加密密钥 |
|||
private static final String FIRST_KEY = "0CoJUm6Qyw8W8jud"; |
|||
private static final String IV = "0102030405060708"; |
|||
// 固定encSecKey(适配热门评论接口) |
|||
private static final String ENC_SEC_KEY = "00e0b50bcfcc08f1f8d6a8a9a0c96969b6f928a5069f07aa09f6062446f4224f42f42f42f42f42f42f42f42f42f42f42f42f42f42f42f42f42f42f42f42f42f4"; |
|||
|
|||
static { |
|||
// 注册BouncyCastle加密提供者 |
|||
Security.addProvider(new BouncyCastleProvider()); |
|||
} |
|||
|
|||
/** |
|||
* AES加密(CBC模式,PKCS7填充) |
|||
*/ |
|||
private static String aesEncrypt(String content, String key) throws Exception { |
|||
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"); |
|||
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8)); |
|||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); |
|||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); |
|||
byte[] encrypted = cipher.doFinal(pad(content).getBytes(StandardCharsets.UTF_8)); |
|||
return Base64.getEncoder().encodeToString(encrypted); |
|||
} |
|||
|
|||
/** |
|||
* PKCS7补位 |
|||
*/ |
|||
private static String pad(String s) { |
|||
int blockSize = 16; |
|||
int padCount = blockSize - (s.length() % blockSize); |
|||
StringBuilder sb = new StringBuilder(s); |
|||
for (int i = 0; i < padCount; i++) { |
|||
sb.append((char) padCount); |
|||
} |
|||
return sb.toString(); |
|||
} |
|||
|
|||
/** |
|||
* 生成加密参数(params + encSecKey) |
|||
*/ |
|||
private static Map<String, String> getEncryptedParams(String songId) throws Exception { |
|||
// 构造原始请求参数 |
|||
JSONObject originParam = new JSONObject(); |
|||
originParam.put("rid", "R_SO_4_" + songId); |
|||
originParam.put("offset", 0); |
|||
originParam.put("total", true); |
|||
originParam.put("limit", 20); |
|||
originParam.put("csrf_token", ""); |
|||
|
|||
// 生成16位随机密钥 |
|||
String secondKey = RandomStringUtils.randomAlphanumeric(16); |
|||
// 双重AES加密 |
|||
String params = aesEncrypt(aesEncrypt(originParam.toJSONString(), FIRST_KEY), secondKey); |
|||
|
|||
Map<String, String> result = new HashMap<>(); |
|||
result.put("params", params); |
|||
result.put("encSecKey", ENC_SEC_KEY); |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 获取热门评论 |
|||
*/ |
|||
public static JSONArray getHotComments(String songId) { |
|||
String url = "https://music.163.com/weapi/v1/resource/comments/R_SO_4_" + songId + "?csrf_token="; |
|||
CloseableHttpClient httpClient = HttpClients.createDefault(); |
|||
try { |
|||
// 1. 构造POST请求 |
|||
HttpPost httpPost = new HttpPost(url); |
|||
// 2. 设置请求头 |
|||
httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); |
|||
httpPost.setHeader("Referer", "https://music.163.com/song?id=" + songId); |
|||
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded"); |
|||
|
|||
// 3. 生成加密参数并构造请求体 |
|||
Map<String, String> encryptedParams = getEncryptedParams(songId); |
|||
String requestBody = "params=" + encryptedParams.get("params") + "&encSecKey=" + encryptedParams.get("encSecKey"); |
|||
httpPost.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8)); |
|||
|
|||
// 4. 发送请求并解析响应 |
|||
CloseableHttpResponse response = httpClient.execute(httpPost); |
|||
HttpEntity entity = response.getEntity(); |
|||
String responseStr = EntityUtils.toString(entity, StandardCharsets.UTF_8); |
|||
JSONObject responseJson = JSON.parseObject(responseStr); |
|||
|
|||
// 5. 返回热门评论数组 |
|||
return responseJson.getJSONArray("hotComments"); |
|||
|
|||
} catch (Exception e) { |
|||
System.err.println("爬取失败:" + e.getMessage()); |
|||
e.printStackTrace(); |
|||
} finally { |
|||
try { |
|||
httpClient.close(); |
|||
} catch (Exception e) { |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
return new JSONArray(); |
|||
} |
|||
|
|||
/** |
|||
* 格式化输出评论 + 保存到CSV |
|||
*/ |
|||
public static void printAndSaveComments(JSONArray hotComments) { |
|||
// 1. 格式化打印 |
|||
System.out.println("===== 网易云热门评论 ====="); |
|||
for (int i = 0; i < hotComments.size(); i++) { |
|||
JSONObject comment = hotComments.getJSONObject(i); |
|||
JSONObject user = comment.getJSONObject("user"); |
|||
String nickname = user.getString("nickname"); |
|||
String content = comment.getString("content").replace("\n", " "); |
|||
int likedCount = comment.getInteger("likedCount"); |
|||
String timeStr = comment.getString("timeStr"); |
|||
|
|||
System.out.printf("%d. %s:%s(%d赞)- %s%n", |
|||
i + 1, nickname, content, likedCount, timeStr); |
|||
} |
|||
|
|||
// 2. 保存到CSV(可选) |
|||
/* |
|||
try (FileWriter writer = new FileWriter("netease_hot_comments.csv", StandardCharsets.UTF_8)) { |
|||
// 写入表头 |
|||
writer.write("序号,用户,评论,点赞数,时间\n"); |
|||
// 写入评论数据 |
|||
for (int i = 0; i < hotComments.size(); i++) { |
|||
JSONObject comment = hotComments.getJSONObject(i); |
|||
JSONObject user = comment.getJSONObject("user"); |
|||
String nickname = user.getString("nickname"); |
|||
String content = comment.getString("content").replace("\n", " ").replace(",", ","); |
|||
int likedCount = comment.getInteger("likedCount"); |
|||
String timeStr = comment.getString("timeStr"); |
|||
|
|||
writer.write(String.format("%d,%s,%s,%d,%s%n", |
|||
i + 1, nickname, content, likedCount, timeStr)); |
|||
} |
|||
System.out.println("评论已保存到 netease_hot_comments.csv"); |
|||
} catch (Exception e) { |
|||
System.err.println("保存CSV失败:" + e.getMessage()); |
|||
} |
|||
*/ |
|||
} |
|||
|
|||
// 主方法:测试爬取《晴天》(ID:186016)热门评论 |
|||
public static void main(String[] args) { |
|||
String songId = "186016"; // 晴天的歌曲ID |
|||
JSONArray hotComments = getHotComments(songId); |
|||
if (!hotComments.isEmpty()) { |
|||
printAndSaveComments(hotComments); |
|||
} else { |
|||
System.out.println("未获取到评论,请检查歌曲ID或网络"); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue