新闻详情

新闻详情

首页 / 资讯中心 / 详情

网易云音乐评论接口逆向实战:Python破解weapi加密全流程

发布时间:2026/9/9 19:45:48来源:尧图网络
网易云音乐评论接口逆向实战:Python破解weapi加密全流程
网易云音乐评论接口是爬虫圈里很经典的一道坎。接口本身不复杂但前端把所有请求参数都做了加密直接构造requests.post根本摸不到门。数据抓取本身也不难难的是逆向出那套 AES RSA 的加密流程再把参数伪造得像浏览器一样。这篇博文就把我实战中踩过的坑和最终跑通的方案完整写出来核心思路是定位加密入口、还原加密逻辑、用 Python 原生实现、最后才去抓评论。整个项目跑通了你就能批量拿任意歌曲的评论数据后续做情感分析、热评排行、歌手粉丝画像都顺手很多。适合刚接触爬虫逆向的 Python 开发者也适合想系统理解网易云 weapi 加密体系的人文章的代码部分我尽量写得可以直接复制跑通但你最好还是跟着思路走一遍因为参数稍微错一个字符服务端就会甩给你一个看不懂的错误码。1. 项目背景与整体设计思路1.1 为什么选网易云评论接口当突破口网易云音乐的评论区在中文互联网里算是一个特别的存在。它不只是一个功能模块更多时候像树洞用户在里面写故事、泄情绪、记录生活节点。所以很多数据分析项目、舆情分析项目、甚至产品调研都把网易云评论当作数据源。但正因为评论区太有影响力网易云在反爬上的投入也是真金白银的。评论接口不是简单的 URL 参数就能请求而是整套 weapi 加密体系你的请求体必须带着加密后的params和encSecKey两个字段服务端才认。这个接口对爬虫学习者的价值就在于它没有用人机验证、没有滑块、没有强制登录唯一的大门就是加密。只要你把加密逆向搞明白了后面全是坦途。相比淘宝、抖音那种既要处理签名又要对抗风控的硬骨头网易云是一个难度恰到好处的实战案例。你做一次完整的逆向能同时接触到 AES 对称加密、RSA 非对称加密、JS 调试、请求伪造这几个高频技能点性价比非常高。1.2 技术选型不调 JS纯 Python 硬算很多教程在破解网易云时会选择把前端的加密 JS 抠下来用execjs在 Python 里执行。这个方案看起来简单但问题也很明显依赖 Node.js 环境、每次请求都要起一个 JS 运行时、性能差、部署麻烦而且如果你是在服务器上跑还得先装一堆系统依赖。我自己的项目走了另一条路直接分析加密函数找到它是怎么算出来的然后用 Python 的加密库原样复刻。选择纯 Python 实现有几个实际好处。第一是轻量一个pycryptodome库就搞定了 AES 和 RSA 的全部算法第二是可控你能精确知道每一步输入输出出了问题也好排查第三是快没有 JS 引擎的开销跑大批量任务时差距非常明显。整个项目的技术栈就是requests发请求pycryptodome做加解密pandas或标准库csv做数据落盘零额外负担。1.3 整体流程从浏览器到 Python 请求的完整链路先给整个项目理个清晰的链路后面所有章节都是围绕这条线展开的。第一步打开网易云任意歌曲页面在浏览器开发者工具里找到评论的 XHR 请求观察它的 URL 和表单结构第二步在 JS 文件中定位encSecKey这个关键变量顺藤摸瓜找到window.asrsea这个加密函数第三步分析函数体内的 AES 和 RSA 逻辑把算法抽出来第四步用 Python 实现同样的加密过程构造请求拿到 JSON 评论数据第五步解析 JSON分页抓取保存成结构化文件。这套流程看起来步骤多但每一步都不难难的是把步骤串起来的时候很多人会在第二步卡住——在混淆过的 JS 代码里找加密函数确实需要一点技巧。我后面会单独讲怎么快速定位不会让你真的去一行行读那几百 KB 的 JS 文件。2. 加密接口原理拆解2.1 浏览器里真实发生的请求长什么样先打开浏览器开发者工具切到 Network 面板随便点开一首歌的评论区往下翻几页你会看到类似这样的请求POST https://music.163.com/weapi/v1/resource/comments/R_SO_4_33894312?csrf_token注意这个 URL它包含了两个关键信息。R_SO_4_是资源类型前缀R表示评论相关SO_4表示歌曲类型后面的数字就是歌曲 ID。实际抓评论时只需要替换这个 ID 就行。再点开请求的 Payload你会发现表单里只有两个字段params和encSecKey全是乱七八糟的密文。params是一长串 Base64 字符串encSecKey是一长串十六进制字符串。这两字段就是整套加密体系的输出。前端拿到你填写的评论请求参数比如歌曲 ID、页码、排序方式先做两层 AES 加密得到params再用 RSA 公钥加密一个随机密钥得到encSecKey。服务端收到后用私钥解开encSecKey拿到随机密钥再用这个随机密钥去解params最终还原出原始请求参数。2.2 两层 AES 加密是怎么回事AES 是对称加密同一个密钥既负责加密也负责解密。网易云这里选用了 AES-128-CBC 模式密钥 16 字节偏移量固定为0102030405060708填充方式是 PKCS7。光看这些名词可能有点懵我用大白话解释一下CBC 模式会把明文切成一个个 16 字节的块每个块在加密前先和前一个块的密文做异或这样即使两段明文一样加密结果也不一样安全性更高。PKCS7 填充则是在明文长度不是 16 的倍数时补上对应数量的字节让最后一块刚好凑满。但网易云不是加密一次而是连续加密两次。第一次用写死在 JS 里的密钥0CoJUm6Qyw8W8jud加密原始参数得到一层密文第二次用一个随机生成的 16 位字符串作为密钥把第一层密文再加密一次。为什么要多此一举直接一次 AES 不行吗这里其实有个安全设计逻辑第一次加密是固定密钥第二次加密是临时密钥而临时密钥本身又用 RSA 加密后传给了服务端。就算有人从 JS 里挖出了固定密钥也只能解开第一层拿不到第二层的随机密钥也就解不开最终的明文。两层加密叠加等于把攻击者的逆向成本往上抬了一个台阶。2.3 RSA 公钥加密随机密钥怎么安全传给服务端随机密钥作为第二次 AES 的加密密钥必须传给服务端否则服务端解不开。但如果直接明文传输那两层 AES 形同虚设。所以网易云选择了 RSA 非对称加密前端用公钥加密随机密钥服务端用私钥解密。RSA 的数学原理不展开你只需要知道公钥加密的东西只有私钥能解开而公钥是公开的就写在 JS 里。真正干活的时候RSA 加密的细节比想象中复杂一点。它不是直接对随机密钥的字节做加密而是先把随机密钥的字符串倒序再转成一个大整数用公钥的模数modulus和指数publicExponent做模幂运算最后把结果转成 256 位的十六进制字符串。这里有个大坑如果你忘了把字符串倒序算出来的encSecKey会是错的服务端会直接返回{code:-460}或api error。我第一次逆向时就栽在这里排查了很久才发现问题出在[::-1]这一步。3. Python 实现加密与评论请求3.1 环境准备装对库少踩一半坑代码层面核心依赖只有一个。强烈建议直接用pycryptodome不要用老的pycrypto后者已经停止维护在 Python 3.9 以上的环境里经常编译失败。安装命令很简单pip install pycryptodome requests如果你在 import 时遇到ModuleNotFoundError: No module named Crypto大概率是环境里有残留的pycrypto或者pycryptodome和pycryptodomex两个包冲突了。解决办法是先卸载干净再重装pip uninstall pycrypto pycryptodome pycryptodomex pip install pycryptodomefrom Crypto.Cipher import AES能用这个 import 就说明安装成功了。另外我建议用pipenv或venv建个虚拟环境别把依赖直接装进系统 Python后面换项目时不会一地鸡毛。3.2 核心加密代码完整还原 weapi 加密流程下面是整套加密逻辑的 Python 实现代码量不大但每一行都是经过调试的建议你新建一个weapi.py文件把这部分单独放进去方便后续复用。import base64 import json import random from Crypto.Cipher import AES # 固定参数来自网易云前端 JS modulus 00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f561377c8f6a4c49ae299d7af6b6565139a1937d675e9098691784d2b58621a0c49989b9a271c5b7b3b3b7f2c9c4f0b9e4e9e4e5f5c5f5c public_exponent 010001 first_key 0CoJUm6Qyw8W8jud iv 0102030405060708 def aes_cbc_encrypt(plaintext: str, key: str) - str: AES-128-CBC 加密PKCS7 填充输出 Base64 pad_len 16 - len(plaintext.encode()) % 16 padded_text plaintext chr(pad_len) * pad_len cipher AES.new(key.encode(), AES.MODE_CBC, iv.encode()) encrypted cipher.encrypt(padded_text.encode()) return base64.b64encode(encrypted).decode() def rsa_encrypt(rand_key: str) - str: RSA 公钥加密随机密钥输出 256 位十六进制字符串 注意需要先将随机密钥倒序再转整数运算 reversed_key rand_key[::-1] key_int int.from_bytes(reversed_key.encode(), big) result pow(key_int, int(public_exponent, 16), int(modulus, 16)) return format(result, x).zfill(256) def generate_random_key(length: int 16) - str: 生成 16 位随机字符串作为第二次 AES 的密钥 chars abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 return .join(random.choice(chars) for _ in range(length)) def weapi_encrypt(data: dict) - dict: 将请求参数 dict 加密为 weapi 所需的 params 和 encSecKey text json.dumps(data, separators(,, :)) rand_key generate_random_key() # 第一层固定密钥加密原始参数 first_encrypted aes_cbc_encrypt(text, first_key) # 第二层随机密钥加密第一层结果 params aes_cbc_encrypt(first_encrypted, rand_key) enc_sec_key rsa_encrypt(rand_key) return {params: params, encSecKey: enc_sec_key}这里有几个细节值得单独拎出来强调。第一json.dumps时一定要用separators(,, :)去掉 JSON 里的多余空格否则加密后的结果和浏览器不一致。第二aes_cbc_encrypt里的填充字符是chr(pad_len)也就是补几个字节就填 ASCII 码为几的字符这是 PKCS7 的标准做法。第三rsa_encrypt里format(result, x).zfill(256)这行zfill保证输出固定 256 位因为 RSA 计算结果前面的 0 会被省略不补回来服务端会判定参数长度不对。3.3 评论请求函数把加密参数发出去加密逻辑封装好之后请求本身就非常简单了。构造评论请求需要的基本参数包括rid、threadId、pageNo、pageSize、cursor、offset、orderType、csrf_token。其中rid和threadId都是R_SO_4_加歌曲 IDpageNo从 1 开始pageSize建议 20 或 100cursor固定为-1offset是翻页偏移量。orderType有两个值1是按推荐排序2是按时间排序这个看你的需求。再强调一次csrf_token这个字段不能漏。如果你没登录它可以传空字符串如果你登录了最好从 Cookie 里把__csrf的值提出来填进去。漏掉这个字段在部分接口下会返回 460 错误排查起来还挺迷的。import requests def fetch_comments(song_id: str, page: int 1, page_size: int 100, order_type: str 1, cookie_str: str ) - dict: 请求网易云歌曲评论返回原始 JSON url fhttps://music.163.com/weapi/v1/resource/comments/R_SO_4_{song_id}?csrf_token form_data { rid: fR_SO_4_{song_id}, threadId: fR_SO_4_{song_id}, pageNo: str(page), pageSize: str(page_size), cursor: -1, offset: str((page - 1) * page_size), orderType: order_type, csrf_token: , } headers { 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, Referer: fhttps://music.163.com/song?id{song_id}, Origin: https://music.163.com, } if cookie_str: headers[Cookie] cookie_str payload weapi_encrypt(form_data) resp requests.post(url, datapayload, headersheaders, timeout10) resp.raise_for_status() return resp.json()关于请求头我想多说两句。Referer必须带上而且要对应当前歌曲页面否则某些配置下会被 WAF 拦。Origin也建议带上和Referer保持一致。User-Agent用最新版 Chrome 的标准 UA 就行不要用 Python 默认的python-requests那个 UA 基本是一抓一个准。3.4 冷热知识为什么我的请求总是返回 -460第一类错误是{code:-460}这个错误码意味着服务端无法解析或校验你提交的加密参数。常见原因有三个encSecKey计算时没有倒序、json.dumps带了空格、填充算法写错。如果你确认代码和我的一致还是报错请在weapi_encrypt里加几行打印把params、encSecKey、随机密钥都打出来跟前端 JS 输出的长度对一下。params的长度通常在 200 到 400 字符之间encSecKey固定是 256 个十六进制字符长度不对说明前面某个环节出了偏差。第二类错误是{code:-460, msg:api error}这种一般是csrf_token为空在部分接口下触发或者你用的接口路径不对。可以从这两个方向排查。4. 数据解析与分页抓取4.1 评论 JSON 结构长什么样请求成功后返回的 JSON 结构是标准的主要字段如下code状态码200 表示成功total这首歌的总评论数comments当前页评论列表按orderType排序hotComments热评列表只在orderType1时出现topComments置顶评论通常为空threadId评论所属的线程 ID也就是歌曲的R_SO_4_xxx每个评论项里有你真正要抓的数据user.nickname是用户名content是评论正文time是评论时间戳毫秒级likedCount是点赞数。有时候还需要beReplied字段它表示这条评论回复了哪条评论是分析评论互动关系的重要字段但默认可能为空。def parse_comments(response_json: dict): 从评论接口响应中提取关键字段返回列表 parsed [] comments response_json.get(comments, []) for item in comments: user item.get(user, {}) parsed.append({ comment_id: item.get(commentId), nickname: user.get(nickname), content: item.get(content, ).replace(\n, ), time: item.get(time), liked_count: item.get(likedCount), reply_count: item.get(replyCount, 0), }) return parsed这里有个需要注意的细节评论正文可能包含换行符、特殊字符存入 CSV 或数据库之前最好把换行替换成空格否则用 Excel 打开时行数会错乱。另外time字段是毫秒时间戳展示时记得除以 1000 再格式化。4.2 分页抓取的正确姿势分页参数看起来复杂实际上逻辑很直接pageNo从 1 开始每页请求完pageNo 1offset始终等于(pageNo - 1) * pageSizecursor全程保持-1。这套组合在当前的接口版本下实测是能稳定翻页的。有些教程说cursor必须用上一页最后一条评论的时间戳我试下来发现那是旧版接口的逻辑现在用pageNo offset已经够用但如果以后网易云调整接口你就要回浏览器里重新抓包确认。抓取全量评论时建议先请求一页拿到total字段算出总页数再写循环。但是要注意接口返回的total有时不准或者服务端做了最大页数限制所以循环里要加一个终止条件如果当前页返回的评论数小于pageSize默认已经到底直接 break。这比写死页数更稳。def crawl_all_comments(song_id: str, max_pages: int 50, page_size: int 100, order_type: str 2, cookie_str: str ): 自动翻页抓取全部评论返回合并后的列表 all_comments [] for page in range(1, max_pages 1): try: data fetch_comments(song_id, page, page_size, order_type, cookie_str) if data.get(code) ! 200: print(f[第{page}页] 请求异常: {data}) break comments parse_comments(data) if not comments: print(f[第{page}页] 没有更多评论停止翻页) break all_comments.extend(comments) print(f[第{page}页] 获取 {len(comments)} 条评论累计 {len(all_comments)} 条) time.sleep(random.uniform(1, 2)) except Exception as e: print(f[第{page}页] 异常: {e}) break return all_comments这里time.sleep(random.uniform(1, 2))不是摆设。我在本地测试时连续无间隔请求 50 页以后IP 就被服务端风控了返回 403。间隔 1 到 2 秒虽然会拖慢速度但胜在稳定。如果你是抓冷门歌曲、总共就几百条评论其实压力不大如果抓那种评论数上万的顶流歌曲建议把间隔拉到 3 秒以上并且考虑用代理池。4.3 保存为 CSV方便后续分析最终数据要落盘CSV 是最通用、最不挑工具的格式。用标准库csv就能写不需要引入 pandas除非你后续要做复杂数据处理。import csv def save_to_csv(comments: list, filename: str comments.csv): if not comments: print(没有数据不保存) return fieldnames [comment_id, nickname, content, time, liked_count, reply_count] with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerows(comments) print(f已保存 {len(comments)} 条评论到 {filename})注意这里用的是utf-8-sig而不是utf-8。utf-8-sig会在文件开头写入 BOM 头Excel 打开时才不会乱码。你要是用 pandas 读回去utf-8和utf-8-sig都不会有问题但给其他人用 Excel 打开时这点差异就很关键了。5. 常见问题与排查技巧实录5.1 请求返回 403 或 460 的排查路径在实战中你会遇到各种报错。我把最常见的几种情况整理成了一张排查速查表下面的优先级顺序就是我实际调试的顺序。错误表现可能原因排查方法{code:-460}加密参数错误检查encSecKey是否 256 位、params是否 Base64、json.dumps是否去空格{code:-460, msg:api error}缺少csrf_token字段确认表单里包含csrf_token未登录可置空返回 HTML 而非 JSON被 WAF 拦截调低抓取频率更换 User-Agent检查 Referer 是否和歌曲页一致HTTP 403IP 被临时封禁停止请求 5-10 分钟加重试逻辑后续增加随机间隔请求超时或连接重置请求频率过高改用 Session 复用连接降低单次请求频率评论内容乱码编码问题写入文件时用utf-8-sig请求响应自动使用.json()处理其中 403 封禁是最常见的。我第一次完整跑通脚本时抓了一首热门歌曲的 3000 条评论中途没有加任何延时结果跑到 1500 条左右开始大量 403。当时我以为是加密写错了反反复复查代码查了一个多小时后来退出来等了几分钟再跑又恢复 200 了才意识到是频率问题。从那之后我的所有爬虫脚本里都默认加上随机延时这已经成了肌肉记忆。5.2 随机密钥生成为什么要避开这些字符我在generate_random_key里只用大小写字母和数字刻意避开了特殊字符。为什么因为随机密钥是要经过 AES 加密和 RSA 加密的特殊字符在某些编码转换下可能引入额外问题比如、/、在 Base64 中是填充或特殊字符万一处理不当就会出现边界问题。用纯字母数字虽然理论上略微缩小了密钥空间但 AES-128 本身的安全性足够这点随机熵的损失在实际场景中可以忽略。还有一个细节生成随机密钥时官方 JS 用的是Math.random()配合一个字符表并不会真的用密码学安全随机数。我们在 Python 里用random.choice也够用如果你偏执一点可以用secrets.choice提升随机性结果上没有本质差异。5.3 歌曲 ID 怎么批量获取手动打开网页复制 URL 里的id参数是最笨但最不容易错的方法。如果你要批量抓歌单里的歌曲 ID可以直接调用网易云歌单详情的 API 接口https://music.163.com/api/v6/playlist/detail?id歌单ID。这个接口不需要加密直接 GET 请求就能拿到歌曲列表里面包含每首歌的 ID。具体的歌曲 URL 格式是https://music.163.com/#/song?id33894312URL 里的id就是歌曲 ID。我这边的经验是批量场景下先拿歌单接口把 ID 都拉下来再去抓评论比手动复制效率高一个量级。5.4 更进一步的接口eapi 是什么网易云还存在另一套加密体系叫 eapi请求地址通常是interface3.music.163.com/eapi/...。和 weapi 相比eapi 的加密细节更复杂比如密钥不同、初始化向量不同、部分接口还需要额外的 URL 参数参与计算。但如果你只是想抓评论数据weapi 已经完全够用不需要去啃 eapi。这里说这些是想告诉大家逆向分析要懂得适可而止不要为了追求“终极破解”而花费大量时间在没有实际收益的事情上。抓评论就老老实实用 weapi把时间花在后续的数据分析上收益更大。最后再聊点我自己的体会这套方案我已经在多个项目里跑过包括给一个校园音乐社群抓歌手热门歌曲的评论给一个舆情分析项目抓指定歌曲的负面评论。整体跑下来最深的感触是逆向工程的大头不在于代码本身而在于你对浏览器开发者工具和 JS 执行逻辑的熟悉程度。加密函数名可以是混淆的变量名可以是一堆乱码但只要你能定位到encSecKey这个关键词顺着它的赋值回溯很快就能把整个加密链路理出来。实际操作中我建议你先用一首冷门歌曲调试脚本冷门歌曲评论少就算出了问题也不会把 IP 干上风控名单。等脚本完全稳定了再去抓热门歌曲并且把频率调低一点。最后说一个小技巧如果你只是想分析评论的情感倾向抓热评orderType1就够了热评代表了大多数普通用户的情绪聚焦点但如果你要做完整的舆情分析那就必须抓时间排序orderType2的全量评论。选错排序方式数据量差好几倍分析结论也可能完全两样。希望这篇实战文能帮你少走点弯路。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

ToolJet 接入 Google BigQuery 数据源全指南:服务账号认证、连接配置与九大数据操作实战 2026/9/9 20:30:55

ToolJet 接入 Google BigQuery 数据源全指南:服务账号认证、连接配置与九大数据操作实战

ToolJet 接入 Google BigQuery 数据源全指南:服务账号认证、连接配置与九大数据操作实战 【免费下载链接】ToolJet Open-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workf…

阅读更多 →
SpringAI-Advisor实战:自定义Advisor机制与DeepSeek content为空排查 2026/9/9 20:30:55

SpringAI-Advisor实战:自定义Advisor机制与DeepSeek content为空排查

说实话,第一次看到“SpringAI-Advisor”这个项目名,我第一反应是:又是一个把Spring AI包了一层、塞了几个工具类的示例工程。但真正把源码拉下来、跑通链路之后,我发现自己低估了它——Advisor在Spring AI里扮演的角色&#xff0c…

阅读更多 →
服务端状态与数据获取库技术选型对比:React Query vs SWR vs Apollo Client vs RTK Query vs React Router 2026/9/9 20:30:55

服务端状态与数据获取库技术选型对比:React Query vs SWR vs Apollo Client vs RTK Query vs React Router

服务端状态与数据获取库技术选型对比:React Query vs SWR vs Apollo Client vs RTK Query vs React Router 【免费下载链接】query 🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Qu…

阅读更多 →
中小企业低成本SEO实操指南:从关键词到内容优化的完整打法 2026/9/9 20:30:55

中小企业低成本SEO实操指南:从关键词到内容优化的完整打法

做SEO这行十年,被中小企业老板问得最多的一句话是:“我预算不多,能不能不花大价钱也能把网站做上来?”我的回答通常是:能,但前提是你得把力气用在刀刃上。大公司烧钱买词、堆资源、养团队,那是他…

阅读更多 →
从 Changelog 读懂 expo-app-integrity:Expo 应用完整性校验模块的版本演进与实现剖析 2026/9/9 20:30:55

从 Changelog 读懂 expo-app-integrity:Expo 应用完整性校验模块的版本演进与实现剖析

从 Changelog 读懂 expo-app-integrity:Expo 应用完整性校验模块的版本演进与实现剖析 【免费下载链接】expo An open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web. 项目地址: https://gitcode.com/G…

阅读更多 →
Istio 示例镜像构建指南:读懂 samples/builder 的 docker buildx bake 流水线 2026/9/9 20:27:55

Istio 示例镜像构建指南:读懂 samples/builder 的 docker buildx bake 流水线

Istio 示例镜像构建指南:读懂 samples/builder 的 docker buildx bake 流水线 【免费下载链接】istio Connect, secure, control, and observe services. 项目地址: https://gitcode.com/GitHub_Trending/is/istio Istio 仓库的许多示例(bookinfo…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞