emo云爬取评论(day1)

学习爬虫的第一天,目标:获得网易云音乐的评论信息。

定位

查看网页和框架的源代码,发现源代码中并没有评论信息,评论信息大概率是通过js后续加载的。

image-20250707203651749

浏览器在渲染了html文件后,会执行其中的js文件,这些js代码会向特定的api接口发送请求,比如下图,发送请求评论的请求。

image-20250707204113720

请求头是这样的。

image-20250707204929052

负载是这样的。

image-20250707204959521

如果要爬取评论,需要完成下面3件事情。

  1. 找到未加密的参数来源。
  2. 对参数进行加密(符合网易的加密逻辑),充当负载部分。
  3. 发送请求,获得评论。

参数来源

在浏览器的开发工具中,启动器可以查看发出这个请求之前,执行了哪些js脚本。

image-20250707210446871

点开了最后一个执行的js文件,下断点。

image-20250707213804782

image-20250707213703557

一路跳过不匹配的包,直到匹配目标url。

image-20250707223728212

我发现js的调用堆栈有个好处,记录了调用时的值。

一路翻看调用堆栈,直到翻到data处于明文的时候。

image-20250707223831766

发现处于下面这个匿名调用时,Ce2x的内容尚未加密,说明加密点在t6n.be6Y。

image-20250707224251028

在be6Y中下了一个断点,发现:当执行完window.asrsea(JSON.stringify(i6c), bsC7v(["流泪", "强"]), bsC7v(BA1x.md), bsC7v(["爱心", "女孩", "惊恐", "大笑"]));后,数据就加密完了,这个window.asrsea就是加密点。

image-20250707225004805

image-20250707225137666

将明文导出来。

1
2
3
4
5
6
7
8
9
10
data = {
"rid": "R_SO_4_2722391361",
"threadId": "R_SO_4_2722391361",
"pageNo": "1",
"pageSize": "20",
"cursor": "-1",
"offset": "0",
"orderType": "1",
"csrf_token": "fb96f04e2bf711be4bf31bba80bf766a"
}

加密算法

差一个加密流程,就可以构造报文发包了。

加密函数是window.asrsea,搜索一下。

image-20250707230102711

对下面的内容进行分析,获得加密方式。

函数a:获得长度为a的字符串。

函数b:AES对称加密,数据是a、密钥是b。

函数c:利用服务器的公钥b和c对数据a进行RSA加密,b是公钥、c是模数。

函数d:负责调用函数a/b/c,得到结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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

这个加密流程调用了函数d,函数d的4个参数有3个是固定值。

image-20250707232333262

将上述流程转换成python代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import base64
import json
import random
import string
from Crypto.Cipher import AES

# 核心加密类
class NeteaseEncrypt:
"""
这个类封装了网易云音乐API请求加密所需的所有方法。
- 对应JS函数a: _create_random_key
- 对应JS函数b: _aes_encrypt
- 对应JS函数c: _rsa_encrypt
- 对应JS函数d (asrsea): encrypt
"""

def __init__(self):
# 这些是网易云音乐API中硬编码的、固定的值
self.MODULUS = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
self.NONCE = "010001"
self.PUBKEY = "0CoJUm6Qyw8W8jud" # 预设的AES密钥 (JS函数d里的g)
self.IV = "0102030405060708" # AES加密的初始化向量 (JS函数b里的d)

def _create_random_key(self, size: int) -> str:
"""
对应JavaScript函数 a: 生成指定长度的随机字符串
"""
char_set = string.ascii_letters + string.digits
return "".join(random.choices(char_set, k=size))

def _aes_encrypt(self, text: str, key: str) -> str:
"""
对应JavaScript函数 b: 进行AES加密
"""
key_bytes = key.encode("utf-8")
iv_bytes = self.IV.encode("utf-8")
# PKCS7填充:确保数据长度是16字节的倍数
pad = 16 - len(text) % 16
text_padded = text + pad * chr(pad)
text_padded_bytes = text_padded.encode("utf-8")

# 创建AES加密器
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
# 加密并进行Base64编码
encrypted_bytes = cipher.encrypt(text_padded_bytes)
return base64.b64encode(encrypted_bytes).decode("utf-8")

def _rsa_encrypt(self, text: str) -> str:
"""
对应JavaScript函数 c: 进行RSA加密
"""
# RSA加密过程的核心是模幂运算: (plaintext ^ exponent) % modulus

# 1. 将随机密钥反转并转为十六进制字节
text_bytes = text.encode('utf-8')
reversed_text_hex = text_bytes[::-1].hex()

# 2. 将十六进制字符串、公钥、模数都转换为大整数
plaintext_int = int(reversed_text_hex, 16)
pubkey_int = int(self.NONCE, 16)
modulus_int = int(self.MODULUS, 16)

# 3. 执行模幂运算
encrypted_int = pow(plaintext_int, pubkey_int, modulus_int)

# 4. 将结果转为十六进制字符串,并补足256位
return format(encrypted_int, 'x').zfill(256)

def encrypt(self, data: dict) -> dict:
"""
对应JavaScript函数 d (asrsea): 执行完整的混合加密流程
"""
# 将Python字典转换为JSON字符串
data_json = json.dumps(data)

# 1. 生成16位随机密钥 (对应JS函数i = a(16))
random_key = self._create_random_key(16)

# 2. 第一次AES加密: 使用预设密钥(PUBKEY)加密业务数据
enc_text_part1 = self._aes_encrypt(data_json, self.PUBKEY)

# 3. 第二次AES加密: 使用随机密钥加密第一次的结果
enc_text = self._aes_encrypt(enc_text_part1, random_key)

# 4. RSA加密: 加密随机密钥
enc_sec_key = self._rsa_encrypt(random_key)

# 返回最终的两个加密参数
return {
"params": enc_text,
"encSecKey": enc_sec_key
}

爬取评论

跑脚本。

image-20250707233034845

发现不需要”csrf_token”也可以获得返回值。

之后修改了一下代码,爬取结果变好看了。

image-20250707234252552