目的
获取网易云音乐热评数量,直接显示在歌单曲目
第一阶段:请求响应分析, 找到请求评论时所依赖的参数
- 复制 xhr 请求为 curl 命令
- 成功执行 curl 命令
用记事本记下 curl 命令所有请求参数
- --H 是 header
- ?xx = yy 是 查询参数
- --data 是 form data
- 不断删减增加参数测试 curl 直到找到最少可执行 curl 的所有参数(有点像容斥原理)
curl 'https://music.163.com/weapi/v1/resource/comments/R_SO_4_22760423' --data 'params=Th29b5D4%2FgeO1lXj7pG%2FEpnjFdAU24kvYR3tfPPk3M%2FEpSot%2FheuVRaxGe1RLHRccNBss9bkoNI92WTifEszJbDGNNOgAIraKsvbA4J9ple3ur0Pu1QKU0bnNzzKb7bM&encSecKey=4141d2ddbd34151d27ce92501b39875f924672652061f4084ebae54eb87e4b348a53aca44ed047fe70d268a330c995ced6e6c8f84e26c8fa34ad076d172578c99e9be35492e74f9f161da123cc9398c85955655441fbe9a6f13efe69b91475388eafbd2f0af33b1bf77d01a13ccc69b78287b2ec9418f8d08a5cc9535c5f5a33'总而言之,现在知道了评论数的获取依赖于两个参数
- R 路径
form data
- param
- encSecKey
注:查询参数并不必要(?csrf_token=9f4d07a484c0648734db43fd4bcdc85e)
至此可知,代入两个 “未知数”,就能拿到评论数目
未知数1:R_SO_?_????????
未知数2:form data
代入方程
$.ajax({
url : `https://music.163.com/weapi/v1/resource/comments/${未知数1}`,
type : "POST",
data : `${未知数2}`
}).then(data => {console.log(JSON.parse(data).total)})即得评论数
至此,问题转化为:如何获取未知数1 和 未知数2?
未知数1 的获取:
song?id=???????? 中 id 就是后 8 位,而前面的 ? 目前来看均为 4
var idArr = [...document.querySelectorAll('.txt >a')].map(a => `R_SO_4_${a.href.split('?id=')[1]}`)未知数2 的获取:
上网搜了一下, params 和 encSecKey 属于加密参数,只能在 js 分析
虽然实验表明可以取一组固定的参数,完成各种评论的获取,但为了完美解决这个问题,这里还是探索一下
至此,问题转化为: 如何获取网易每次请求时附带的 params 和 encSecKey 参数
第二阶段:fiddler的使用
fiddler 跳坑之路
不知道怎么入手,第一步可以把参数拷到网上搜索,也许别人已经找到方法了呢?
于是我找到了这篇博客
并通过这篇博客的介绍认识并入坑了 fiddler,它可以通过更改 js 的方式很方便地分析代码
如何使用fiddler工具替换网站文件 - 小王同学的博客 - CSDN博客
fiddler 无法抓取 js
实际使用 fiddler 替换 js 过程中,遇到了一个奇怪的问题,就是有的网站 js 抓取得到,有的网站抓取不到。
一开始反复搜索 “fiddler 无法获取 js” 之类的,总是找不到满意的方案
后来多试几次,发现一个规律:就是 http 开头的站点可行, https 开头的站点都是不可行
弄明白问题之后再问, “fiddler 无法获取 https”,果然效率高多了
最后,这篇博客解决了我的难题
Fiddler抓取HTTPS最全(强)攻略 - 华妹陀 - 博客园
fiddler 小插曲
第一次成功用 fiddler 换走 js 的我像得到了新玩具一样兴奋不已,想到之前在 playok 上写记牌器,该项目的瓶颈不就是这个吗?
于是应用 fiddler,直接插入关键代码

- 通过代码断点 log,捕获到出牌的关键内存:Q 函数的参数 g, k,一个代表大小,一个代表花色
- 然后给它加上自己的代码,计算牌面并通知记牌器类执行卡牌统计动作
调整几次之后,效果极佳


第三阶段:获取 params 和 encSecKey
参数规律分析
咳咳,以上都是题外话,拉回正题,现在我们来分析 代码的获取方法
在代码中找关键参数,第一步就是 ctrl + F 定位参数名称,查找 encSecKey,很快找到以下代码:
!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 = "UvSJ0znxEerxFqRD";
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
}();将所有函数入口 log 一下,打印函数名称和参数
!function() {
function a(a) {
console.log(`进入 a 函数`)
console.log(`----参数 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) {
console.log(`进入 b 函数`)
console.log(`----参数 a => ${a}`)
console.log(`----参数 b => ${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) {
console.log(`进入 c 函数`)
console.log(`----参数 a => ${a}`)
console.log(`----参数 b => ${b}`)
console.log(`----参数 c => ${c}`)
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}
function d(d, e, f, g) {
console.log(`进入 d 函数`)
console.log(`----参数 d => ${d}`)
var h = {}
, i = "UvSJ0znxEerxFqRD";
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
console.log(h),
h
}
function e(a, b, d, e) {
console.log(`进入 e 函数`)
console.log(`----参数 a => ${a}`)
console.log(`----参数 b => ${b}`)
console.log(`----参数 d => ${d}`)
console.log(`----参数 e => ${e}`)
var f = {};
return f.encText = c(a + e, b, d),
f
}
window.asrsea = d,
window.ecnonasr = e
}();刷新网页,控制台输出

检验一下最后输出的对象 h,发现它的 encText 和 encSecKey 就是请求里 form data 的 params 和 encSecKey,这里就是我们所需参数产生的地方。
由于网易音乐除了评论外,还有不少请求都附带 params 和 encSecKey,所以实际能看到该函数多次执行:
进入 d 函数
----参数 d => {"csrf_token":"98baefbe1b07d5482d99e2d896e57117"}
----参数 e => 010001
----参数 f => 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
----参数 g => 0CoJUm6Qyw8W8jud
进入 a 函数
----参数 a => 16
进入 b 函数
----参数 a => {"csrf_token":"98baefbe1b07d5482d99e2d896e57117"}
----参数 b => 0CoJUm6Qyw8W8jud
进入 b 函数
----参数 a => /k8rFb2wFLaJmPjZlf95vCOG+zMaOZhtllkx1AARPUO50JNCMP5GL2pVwYFtO7fQd/+6bI3FROIKxgZ5Qw9THA==
----参数 b => UvSJ0znxEerxFqRD
进入 c 函数
----参数 a => UvSJ0znxEerxFqRD
----参数 b => 010001
----参数 c => 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
进入 d 函数
----参数 d => {"offset ":" 0","limit":"1000","order":"true","csrf_token":"98baefbe1b07d5482d99e2d896e57117"}
----参数 e => 010001
----参数 f => 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
----参数 g => 0CoJUm6Qyw8W8jud
进入 a 函数
----参数 a => 16
进入 b 函数
----参数 a => {"offset ":" 0","limit":"1000","order":"true","csrf_token":"98baefbe1b07d5482d99e2d896e57117"}
----参数 b => 0CoJUm6Qyw8W8jud
进入 b 函数
----参数 a => oDsZG/jrEt8ECAccXAa683gzA1W4XZDq/fEVZLH2XCDaH9MhZXa/bprXyriA2bj8jT5tzXnzlASXToDH8bP33kx9kaXEqF3/7qVdeDhJj5RbBkEApbsDVjY4xhh6cbg5
----参数 b => PYsHnGo6Tj1XmorJ
进入 c 函数
----参数 a => PYsHnGo6Tj1XmorJ
----参数 b => 010001
----参数 c => 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
进入 d 函数
----参数 d => {"rid":"A_PL_0_30448263","offset":"0","total":"true","limit":"20","csrf_token":"98baefbe1b07d5482d99e2d896e57117"}
----参数 e => 010001
----参数 f => 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
----参数 g => 0CoJUm6Qyw8W8jud
进入 a 函数
----参数 a => 16
进入 b 函数
----参数 a => {"rid":"A_PL_0_30448263","offset":"0","total":"true","limit":"20","csrf_token":"98baefbe1b07d5482d99e2d896e57117"}
----参数 b => 0CoJUm6Qyw8W8jud
进入 b 函数
----参数 a => 1bjATZvnbm5jJWcDt31FGL0s2DNAv1co4I7Z5vEK5zXB24PPmCNg95aoHE2hQUU4N0Y2sVH058u8i8z+lCjy3Ytv4HJS9ZHwdUs8fu4Y7UP1Uh/cqz8OxYifdfXCW7GfTrhI6bCCPDz4jf26n2gXPzb/cWDy0Te+yaJHYHCxhag=
----参数 b => rVjXrIiNV6JYFNM8
进入 c 函数
----参数 a => rVjXrIiNV6JYFNM8
----参数 b => 010001
----参数 c => 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7分析以上输出可以知道几个信息:
- 加密函数执行顺序: d => a => b => b => c
同一组内,参数之间呈如下规律
- d(A, B, C, D)
b(A, D)
b(?, E)
c(E, B, C)- d(A, B, C, D)
- 第一组参数
A = {"csrf_token":"98baefbe1b07d5482d99e2d896e57117"}
B = 010001
C = 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
D = 0CoJUm6Qyw8W8jud
E = UvSJ0znxEerxFqRD = a(16)
第二组参数
A = {"offset ":" 0","limit":"1000","order":"true","csrf_token":"98baefbe1b07d5482d99e2d896e57117"}
B = 010001
C = 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
D = 0CoJUm6Qyw8W8jud
E = PYsHnGo6Tj1XmorJ = a(16)
第三组参数
A = {"rid":"A_PL_0_30448263","offset":"0","total":"true","limit":"20","csrf_token":"98baefbe1b07d5482d99e2d896e57117"}
B = 010001
C = 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
D = 0CoJUm6Qyw8W8jud
E = rVjXrIiNV6JYFNM8 = a(16)
观察 3 知,不同组间参数具有如下规律
- A 都有固定属性 "csrf_token":"98baefbe1b07d5482d99e2d896e57117"
B 都是固定的 010001
C 都是固定的 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
D 都是固定的 0CoJUm6Qyw8W8jud
E 是一个 16 位随机字符串(因为 a(xx) 这个函数的作用就是生成 xx 位随机字符串)- A 都有固定属性 "csrf_token":"98baefbe1b07d5482d99e2d896e57117"
上网验证了一下,一篇三年前的关于 params 和 encSecKey 相关讨论的文章的 BCD 也与上面相同,这说明这三参数确实固定而且在这三年间不曾改变。
如此一来,params 和 encSecKey 这两参数只依赖于参数 A,E,而 E 又是能随机给的,所以最终产生它们所需要提供的只有 A
代码逻辑分析
回到代码分析执行逻辑,d 的代码描述了关键过程,翻译一下就是
var h = {}
var i = a(16)
h.encText = b(d, g)
h.encText = b(h.encText, i)
h.encSecKey = c(i, e, f)
return h可见 d 是主体函数,a, b ,c 都是工具。
只要调用 d(A, B, C, D),就能以 {encText, encSecKey} 的方式拿到 params 和 encSecKey
回过头看代码末尾,有一句 window.asrsea = d,如此一来这个关键函数可以全局调用
事情立刻变简单,我们可以
function getParamsAndEncSecKey(A) {
const B = '010001'
const C = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
const D = '0CoJUm6Qyw8W8jud'
let h = window.asrsea(JSON.stringify(A), B, C, D)
return {
params: h.encText,
encSeckey: h.encSecKey
}
}直接拿到结果。现在万事俱备,只差变量 A
这里经过一些测试,发现 A 是一个总有属性 "csrf_token" 的对象,尽管有些请求里它会附带其它属性,但是绝大多数时候(请求评论数应该也是其中之一)只要 "csrf_token" 就够了
可以说:A 的属性 csrf_token 知道了,表单的两个参数就知道了
于是问题又转化为 如何获取 A 对象中 csrf_token 的值?
第四阶段:获取 csrf 值
同样的方法,第一步就是在代码里 Ctrl + F 一下 csrf,很快我发现一个可疑的函数 gO1x,插入 log 分析
j.gO1x = function() {
var df0x = new Date
, crM6G = +df0x
, bxz4D = 864e5;
var crX6R = function(X9O) {
var sZ5e = document.cookie
, ue5j = "\\b" + X9O + "="
, bam6g = sZ5e.search(ue5j);
if (bam6g < 0)
return "";
bam6g += ue5j.length - 2;
var yP7I = sZ5e.indexOf(";", bam6g);
if (yP7I < 0)
yP7I = sZ5e.length;
console.log(`csrf 是 ${sZ5e.substring(bam6g, yP7I)} 吗?`) // 看这里看这里看这里
return sZ5e.substring(bam6g, yP7I) || ""
};执行后收到肯定的答复
再 log 一下关键参数
console.log(`sZ5e => ${sZ5e}`)
console.log(`bam6g => ${bam6g}`)
console.log(`yP7I => ${yP7I}`)执行后告诉我后两者总是 1568 和 1600,而 sZ5e 就是 document.cookie
显然,csrf 就是 cookie 里面的 __csrf=???? 的 ????
可以通过以下方式拿到
var csrf = document.cookie.match(/__csrf=(.*?)(;|$)/)[1]解决问题
综上:
读取 cookie 知道 csrf => csrf 代入 window.asrsea 获取 params 和 encSecKey => params 和 encSecKey 作为 formData 发送请求 => 得到评论 JSON
这是解决问题的代码
// 获取 csrf_token
function getCsrf() {
return document.cookie.match(/__csrf=(.*?)(;|$)/)[1]
}
// 获取 params 和 encSecKey
function getData(A) {
var B = "010001",
C = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7",
D = "0CoJUm6Qyw8W8jud"
let h = window.asrsea(JSON.stringify(A), B, C, D)
return `params=${encodeURIComponent(h.encText)}&encSecKey=${encodeURIComponent(h.encSecKey)}`
}
// 发起请求
function post(data, id) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest()
xhr.open("POST", `/weapi/v1/resource/comments/R_SO_4_${id}`, true)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.onreadystatechange = () => {xhr.readyState === 4 && xhr.status === 200 && resolve(JSON.parse(xhr.responseText).total)}
xhr.send(data)
})
}
let data = getData({"csrf_token": getCsrf()})
post(data, `__这里填歌曲 id__`).then(data => {
console.log(`评论总数(热度): ${data}`)
})附录
踩过的坑
form 表单的 xhr 发送代码
关键在于 setHeader
var id = "479979314"
var xhr = new XMLHttpRequest()
xhr.open("POST", `/weapi/v1/resource/comments/R_SO_4_${id}`, true)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.onreadystatechange = () => {xhr.readyState === 4 && xhr.status === 200 && console.log(xhr.responseText)}
xhr.send(data)看得到元素,却选不到元素
可能是因为该元素在 iframe, 改为面向 iframe 的选择器
document.querySelectorAll('span.txt a')
document.querySelector('iframe').contentWindow.document.querySelectorAll('span.txt a')正则表达式,匹配分号或行尾该这么写
csrf=(.*?)(;|$)
付费歌曲的获取
- 找到 C:UsersAdministratorAppDataLocalNeteaseCloudMusicCache 一个 .uc 文件
- 用 010 Editor 打开
- 工具 => 二进制异或

- 把文件另存为 *.mp3
暂无评论