1. 项目概述与核心思路
作为一名长期从事数据采集工作的开发者,我经常遇到需要爬取网站评论的需求。音乐平台的用户评论蕴含着丰富的UGC内容,对市场分析、用户行为研究具有重要价值。网易云音乐作为国内主流音乐平台,其评论区采用了典型的动态参数加密机制,这给爬虫开发带来了独特挑战。
本项目核心目标是突破网易云音乐评论区的翻页限制,实现自动化采集。与常规网页不同,网易云的评论接口采用了三层加密防护:
- 请求参数动态加密(params和encSecKey)
- 加密逻辑嵌套在混淆后的JavaScript中
- 翻页关键参数(cursor)随请求动态变化
经过逆向分析,我发现突破口在于定位加密前的原始参数生成逻辑。具体技术路线如下:
- 通过Chrome开发者工具追踪网络请求
- 定位加密函数调用栈
- 提取并移植关键加密算法
- 构建参数动态生成机制
2. 加密机制逆向分析
2.1 关键参数定位
使用Chrome开发者工具的Network面板监控评论请求时,发现核心加密参数:
javascript复制params: "e4x8wVXqK2eZpJ3T..."
encSecKey: "a12b3c4d5e6f..."
这两个参数每次请求都会变化,且无法通过简单规律推导。通过全局搜索encSecKey(因其出现频率较低),我们快速定位到加密函数入口:
javascript复制window.asrsea(JSON.stringify(i5n), bod3x(["流泪", "强"]), bod3x(AY1x.md), bod3x(["爱心", "女孩", "惊恐", "大笑"]))
2.2 函数调用栈分析
在Sources面板设置断点后,观察到加密调用层级:
- 用户点击翻页触发事件
- 生成原始参数对象
i5n - 经过4层函数包装处理
- 最终生成
params和encSecKey
关键发现是i5n对象的结构:
javascript复制{
rid: "R_SO_4_29764560", // 歌曲ID
threadId: "R_SO_4_29764560",
pageNo: 2, // 当前页码
cursor: "1654321000000",// 翻页游标
pageSize: 20, // 每页条数
offset: 20 // 偏移量
}
2.3 动态参数规律
通过设置i5n对象的日志断点,观察到翻页时仅以下字段变化:
pageNo:线性递增(2,3,4...)cursor:取上一页返回的cursor值offset:计算值为(pageNo-1)*pageSize
重要提示:首次请求时
cursor值为"-1",这是系统约定的初始值
3. 加密函数移植方案
3.1 核心加密逻辑提取
从window.asrsea进入加密函数,发现其由四个子函数构成:
javascript复制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) { // AES加密
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) { // RSA加密
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
}
3.2 浏览器环境适配
由于原始代码依赖浏览器环境,在Node.js中需要做如下适配:
javascript复制// 环境兼容处理
if (typeof window === 'undefined') {
window = global;
window.CryptoJS = require('crypto-js');
window.BigInteger = require('jsbn').BigInteger;
window.RSAKeyPair = require('./rsa').RSAKeyPair;
window.encryptedString = require('./rsa').encryptedString;
}
避坑指南:必须确保引入的crypto-js和jsbn版本与网页使用的保持一致,否则可能导致加密结果不一致
4. 完整爬虫实现
4.1 工程结构设计
code复制netease-comment-crawler/
├── core/
│ ├── crypto.js # 加密算法移植
│ └── rsa.js # RSA相关函数
├── utils/
│ └── request.js # 网络请求封装
└── main.py # 主程序
4.2 核心代码实现
加密参数生成(Python调用JS):
python复制import execjs
# 初始化JS环境
with open('core/crypto.js', 'r', encoding='utf-8') as f:
ctx = execjs.compile(f.read())
def generate_params(page_no, cursor):
"""生成加密参数"""
return ctx.call('getEncParams', {
'rid': 'R_SO_4_29764560',
'pageNo': page_no,
'cursor': cursor,
'pageSize': 20
})
请求处理与数据解析:
python复制import requests
from datetime import datetime
def get_comments(page_no, cursor, retry=3):
url = 'https://music.163.com/weapi/comment/resource/comments/get'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'Referer': 'https://music.163.com/song?id=29764560'
}
params = generate_params(page_no, cursor)
for attempt in range(retry):
try:
resp = requests.post(url, data=params, headers=headers)
data = resp.json()
if data['code'] != 200:
raise ValueError(f"API Error: {data['code']}")
return {
'comments': parse_comments(data['data']['comments']),
'next_cursor': data['data']['cursor']
}
except Exception as e:
if attempt == retry - 1:
raise
time.sleep(2 ** attempt)
数据存储方案:
python复制import csv
from typing import List
def save_to_csv(comments: List[dict], filename='comments.csv'):
fieldnames = ['nickname', 'location', 'date', 'time', 'likes', 'content']
with open(filename, 'a', encoding='utf-8-sig', newline='') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
if f.tell() == 0: # 新文件写入表头
writer.writeheader()
writer.writerows(comments)
5. 反爬对抗策略
5.1 请求特征优化
网易云会检测以下特征:
- Cookie新鲜度:过新的Cookie会被限制
- 请求频率:单IP高频请求会触发验证
- Header完整性:缺少Referer等字段会拒绝服务
优化方案:
python复制headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://music.163.com/',
'Accept': 'application/json, text/javascript',
'X-Real-IP': '211.161.244.70', # 伪造可信IP
'Cookie': '...' # 使用长期有效的Cookie
}
5.2 动态代理配置
建议使用住宅代理并实现自动切换:
python复制import random
PROXY_POOL = [
'http://user:pass@proxy1:port',
'http://user:pass@proxy2:port'
]
def get_session():
session = requests.Session()
session.proxies = {'http': random.choice(PROXY_POOL)}
session.verify = False # 忽略SSL验证
return session
6. 高级技巧与优化
6.1 增量采集方案
为避免重复采集,建议实现以下机制:
python复制import sqlite3
class CommentDB:
def __init__(self, db_file='comments.db'):
self.conn = sqlite3.connect(db_file)
self._create_table()
def _create_table(self):
self.conn.execute('''CREATE TABLE IF NOT EXISTS comments
(id INTEGER PRIMARY KEY AUTOINCREMENT,
comment_id TEXT UNIQUE,
content TEXT,
user_id TEXT,
created_at TIMESTAMP)''')
def exists(self, comment_id):
cursor = self.conn.execute(
'SELECT 1 FROM comments WHERE comment_id=?',
(comment_id,))
return cursor.fetchone() is not None
6.2 分布式扩展设计
使用Redis实现任务队列:
python复制import redis
from rq import Queue
r = redis.Redis(host='localhost', port=6379)
q = Queue(connection=r)
def enqueue_task(song_id, max_pages):
q.enqueue('crawler.tasks.crawl_song_comments',
song_id=song_id,
max_pages=max_pages)
7. 法律合规建议
在实施此类爬虫项目时,务必注意:
- 遵守Robots协议:检查网易云的robots.txt文件
- 控制请求频率:单IP请求间隔建议≥3秒
- 数据使用限制:不得将数据用于商业用途
- 用户隐私保护:对采集到的用户信息进行脱敏处理
建议在代码中加入自动限速机制:
python复制import time
class RateLimiter:
def __init__(self, calls_per_second=0.3):
self.period = 1.0 / calls_per_second
self.last_call = 0
def __call__(self):
now = time.time()
elapsed = now - self.last_call
if elapsed < self.period:
time.sleep(self.period - elapsed)
self.last_call = time.time()
在实际部署中,我建议采用分布式架构配合良好的错误处理机制,这样可以确保长时间稳定运行。对于大规模采集需求,可以考虑结合selenium模拟真实用户操作,但要注意这种方式会显著增加资源消耗。