网易云音乐爬虫实战:weapi接口加密与评论翻页抓取全解析 简介面向Python爬虫入门者与数据采集开发者这套源码围绕网易云音乐数据获取展开覆盖歌手、专辑、歌曲、歌词、评论热评前1000条等核心对象并提供建表SQL和评论词云分析脚本便于从爬取、存储到简单可视化全流程练习。压缩包内共22个文件以14个Python脚本为主搭配SQL、MD说明、词云结果图片等辅助材料整体体积约12.26MB目录按数据维度拆分结构清晰可按需运行。已有860人学习下载。脚本按歌手、专辑、歌曲、歌词、评论拆分为独立模块既可直接采集入库也适合学习接口参数构造、JSON解析、数据去重与频率控制项目中还附带词云分析结果图能让读者直观看到评论数据的文本挖掘效果。资源说明中特别提示采集量大时可能触发反爬有助于在实际操作中理解请求频率与异常处理的重要性。1. 先搞清楚这个网易云音乐爬虫到底要抓什么给歌单做数据分析、给音乐公众号整理专辑资料、或者只是想把一位歌手的全部歌曲和评论区拉下来做词频统计都会遇到同一个问题网易云音乐的网页版页面里歌手、专辑、歌曲、评论这些数据分散在十几个不同的接口里直接抓HTML解析又慢又脆页面结构一改就全废。这里可以直接给一个反直觉的结论抓这个网站的数据最省事、最不容易挂的入口不是网页版接口而是模拟客户端调用的 weapi 接口。这套接口按歌手、专辑、歌曲、评论、歌词分别提供数据并且封掉了最常见的无脑抓取所以实际写爬虫时真正研究的是参数加密和分页游标而不是解析HTML。这个标题对应的是一份打包好的抓取工具但拿到压缩包之前你得先知道一个能抓完这么多数据的爬虫背后至少要解决签名参数、登录态、翻页界限和反爬限制这四件事。这篇文章就把这套方案从接口选型到数据落盘完整拆开适合已经会 requests 基础用法、想升级到带加密参数的实战爬虫的开发者。新手能照步骤把最小可用版本跑起来老手则可以直接跳过接口部分看评论翻页和断点续抓。2. 网易云音乐爬虫的 API 选型与请求参数准备2.1 为什么网页版接口不如客户端 API 顺手网易云音乐的网页版有 /api/search/get/web 这类接口用浏览器打开开发者工具能在 Network 里直接看到 JSON 返回看起来很好抓。但实际操作几次就会发现网页版接口经常在参数里要 csrf_token还时不时要求先访问一次页面拿到动态 cookie而最关键的是部分榜单、歌手详情和评论数据根本不出现在网页版接口里只对客户端开放。所以常见做法是走 weapi 这条线它模拟的是 Windows 客户端和移动端的请求方式接口路径里带 /api/ 前缀数据字段也更全。weapi 和网页版接口的另一个区别是它在 POST 请求体里塞了 params 和 encSecKey 两个字段明文参数先做 AES 加密密钥再用 RSA 加密服务端拿到后解密再响应。这层加密不复杂但如果没有处理你会得到“请求参数错误”或者直接的 460 错误码。先把请求体构造逻辑跑通后面所有接口就都能通用了。2.2 先获取 cookie 和 token 再动手在写加密函数之前需要先拿到一个有效登录态。网易云音乐的大部分接口比如获取歌手的全部专辑、某首歌的评论匿名请求也能返回数据但会经常被风控弹 460带 cookie 之后请求配额宽松很多而且有些字段比如歌手的粉丝数、歌曲的播放数匿名接口里直接不返回或者返回 -1。实操建议是先在浏览器里打开网易云音乐网页版并登录你的账号然后按 F12 进入开发者工具切到 Application 面板在左边的 Storage 里找到 Cookies把 MUSIC_U 和 __csrf 两个字段的值复制出来。MUSIC_U 就是平时说的 token__csrf 用来拼在 POST 请求体里防跨站。把这两个值直接放进请求头比在代码里跑一遍登录流程简单得多而且 cookie 的有效期通常有几周足够跑完一次全量抓取。提示不要把包含 cookie 的代码提交到公开仓库。别人拿到你的 cookie 就能以你的身份调用接口这比程序本身更值得保护。2.3 weapi 的 params 和 encSecKey 到底怎么生成weapi 加密的核心逻辑是先将请求参数 JSON 序列化然后用一个固定的 AES 密钥加密得到一个 params客户端再用一个 RSA 公钥把 AES 的密钥加密得到 encSecKey。服务端配合固定公钥对应的私钥去解密 encSecKey拿到 AES 密钥后解开 params。好在这些密钥在客户端里是硬编码的所以我们的代码里同样可以固定下来。下面这段代码是请求体构造的核心部分基于 Python 的 pycryptodome 库import base64 import random from Crypto.Cipher import AES from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 # 固定密钥weapi 加密时使用的 aes key 和 iv以及 rsa 公钥 MODULUS (00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7 b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280 104eef2c4e2b0d0e5c2e2a9d0b8c4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9 b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2) PUBKEY 010001 def _aes_encrypt(text, key): pad 16 - len(text) % 16 text text chr(pad) * pad iv b0102030405060708 cipher AES.new(key.encode(), AES.MODE_CBC, iv) encrypted cipher.encrypt(text.encode()) return base64.b64encode(encrypted).decode() def _rsa_encrypt(text): text text[::-1] rs int(text.encode().hex(), 16) ** int(PUBKEY, 16) % int(MODULUS, 16) return format(rs, x).zfill(256) def create_weapi_params(data): key 0CoJUm6Qyw8W8jud random_key .join(random.choice(abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789) for _ in range(16)) params _aes_encrypt(data, key) params _aes_encrypt(params, random_key) return {params: params, encSecKey: _rsa_encrypt(random_key)}逻辑说明和参数说明_aes_encrypt做的是两层 AES-CBC 加密第一层用固定密钥0CoJUm6Qyw8W8jud第二层用每请求随机生成的 16 位字符串。返回的是 base64 编码的密文直接作为params。_rsa_encrypt把随机字符串反转后转成整数用固定公钥做模幂运算结果转成 256 位十六进制字符串填充这就是encSecKey。上面代码里的MODULUS和PUBKEY是 weapi 固定的公钥参数PUBKEY就是常见的 010001MODULUS是那个 256 字节的十六进制串。实际使用中不要随便改改了服务端就解不开了。这样create_weapi_params就能为任意接口生成合法的请求体。记住一个细节传入data时必须是一个 JSON 字符串接口路径也要是/api/开头的完整地址后面写爬虫时这两个地方最容易错。写代码时我习惯先封装一个统一的weapi_request函数把 cookie、csrf、POST 请求体都包进去这样后面每个接口都只关心自己的业务参数。3. 用 requests 把歌手、专辑、歌曲、歌词一次抓全3.1 最小可跑的请求骨架有了create_weapi_params之后先跑通一个最简单的接口验证 cookie 和加密逻辑没问题。这里选/api/song/detail试水它只需要传歌曲 id返回的是歌曲名称、歌手、专辑等基本信息没有额外权限要求。import requests import json COOKIE MUSIC_U你的token; __csrf你的csrf CSRF 你的csrf def weapi_request(url, data): headers { Cookie: COOKIE, Referer: https://music.163.com/, Content-Type: application/x-www-form-urlencoded, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36 } data[csrf_token] CSRF params create_weapi_params(json.dumps(data)) resp requests.post(url, headersheaders, dataparams, timeout10) return resp.json() # 用歌曲 id 验证请求是否通 url https://music.163.com/api/song/detail data {ids: [38411490], c: [{id:38411490}]} res weapi_request(url, data) print(res[songs][0][name])这段代码里需要注意weapi_request返回的直接是 JSON因为 weapi 接口响应体本身就是 JSON 格式不需要额外解析。Referer头固定写网易云音乐首页少了它部分接口会返回 404。data里的ids和c字段都要和接口文档保持一致c字段通常用来传歌曲 id 和来源标记可以写成[{id: 歌曲id}]的 JSON 字符串形式有些接口用列表也能过但用字符串更保险。如果这一步输出歌曲名说明 cookie 有效、加密没问题、网络也没被风控后面就是换接口地址和参数的事。3.2 顺着歌手 id 找出专辑和热门歌曲歌手维度的接口有三个是高频使用的/api/artist/head/info/get歌手头像、简介、粉丝数/api/artist/songs歌手的热门歌曲列表按播放量排序/api/artist/album歌手的所有专辑带分页用/api/artist/songs抓某位歌手的热门歌曲param 里传artistId和limit、offset返回的字段中name是歌名al.name是专辑名ar是歌手列表。这里有一个要小心的字段privilege对象里的st字段代表版权状态st为 -200 表示无版权下架做数据清洗时要过滤掉这类歌否则歌词接口会请求失败。专辑接口更简单一点传id歌手id、limit、offset一次最多返回 200 条专辑记录。如果一位歌手发过上千张专辑就得循环 offset 翻页。翻页时注意用while True判断返回列表长度是否小于 limit小于就说明到底了这个逻辑比固定循环次数可靠。def fetch_all_albums(artist_id): albums [] offset 0 limit 100 while True: data {id: artist_id, limit: limit, offset: offset, total: True} res weapi_request(https://music.163.com/api/artist/album, data) batch res.get(hotAlbums) or [] albums.extend(batch) if len(batch) limit: break offset limit return albums这里offset从 0 开始累加total参数固定传 True不做它可能有的接口直接少返回数据。返回的hotAlbums里每项有albumId和name继续把专辑 id 传给/api/album就能拿到歌手专辑里的所有歌曲列表。整条链路就是歌手 id - 专辑列表 - 每张专辑里的歌曲列表 - 歌曲 id。3.3 歌词不是公开字段得单独从 lyirc 接口取歌曲详情、专辑详情里都没有歌词字段需要单独请求/api/song/lyric。这个接口传歌曲 id 就能返回原始歌词文本不需要额外权限但如果歌曲是纯音乐返回的lrc字段会是空值解析时会报 KeyError。def fetch_lyric(song_id): data {id: song_id, lv: -1, kv: -1, tv: -1} res weapi_request(https://music.163.com/api/song/lyric, data) lrc res.get(lrc, {}).get(lyric, ) if not lrc: return None return lrclv、kv、tv三个参数分别代表原文歌词、翻译歌词、罗马音歌词的版本传 -1 表示都返回。比较坑的是这个接口定义的id必须是歌曲 id不能传专辑 id传错了会返回[object Undefined]这样的异常文本。拿到歌词文本后建议按\n切分去掉[00:12.34]这类时间标签再做词频统计省得后面分析时再清洗。另外歌手的简介和粉丝数可以在/api/artist/head/info/get里拿热词里常提到的“歌手详情”其实就是这个接口不需要额外解析网页。4. 评论区的拦截weapi 加密、游标翻页与线程控制4.1 评论接口的加密与游标参数评论是网易云音乐所有公开数据里反爬力度最大的一块。它的接口路径是/api/v1/resource/comments/A_R_xx_yy_0其中A_R_xx_yy_0是资源类型标志歌曲评论是R_SO_4_歌曲id专辑评论是R_AL_3_专辑id歌单评论是R_PL_3_歌单id。评论请求的 weapi 加密逻辑和前面完全一样难点在分页参数。评论接口不使用 offset 分页而是用游标 cursor 和 before 两个参数。第一次请求时cursor填 -1返回的数据里有一个cursor字段把它作为下一次请求的输入直到comments列表为空为止。分页参数里真正让人踩坑的是pageNo和pageSize网易云的评论接口一次最多返回 20 条超过 20 就可能被风控别想一口气拉 100 条。def fetch_all_comments(resource_id, resource_typesong): prefix {song: R_SO_4_, album: R_AL_3_, playlist: R_PL_3_}[resource_type] thread_id prefix str(resource_id) cursor -1 all_comments [] while True: data {rid: resource_id, threadId: thread_id, pageNo: 1, pageSize: 20, cursor: cursor} res weapi_request(https://music.163.com/api/v1/resource/comments/ thread_id, data) comments res.get(comments) or [] all_comments.extend(comments) if len(comments) 20: break cursor res.get(cursor, -1) return all_comments注意第一次请求时 cursor 传 -1服务端会返回第一页并给一个新的游标之后每一页都要用上一次返回的cursor不能自己手动 1。如果评论数量特别多比如热门单曲有几十万条接口会在翻到大约 2000 条之后开始返回空列表或重复数据这是正常的实际抓取时一般搭配time.sleep(0.3)控制速度避免请求过快触发 IP 限流。4.2 翻页上限和“评论区被清空”的判定评论区爬久了会遇到一种现象明明某首歌评论很多程序却只抓到第一页就停了。排查思路是先看接口返回的total字段把total打印出来对比抓到的条数如果 total 有两万但第一个循环就停了多半是cursor翻页判断出了错cursor 没更新每次请求都是同一页。另一种情况是评论区被折叠了接口返回comments为空但hotComments有大量数据注意热评和普通评论是分开返回的字段hotComments只在前几个页面才有不能混在一起统计。如果请求返回的是code: 460说明 IP 被风控常见应对是暂停几分钟再跑不要立刻换一堆号码去试。这几年接口对 cookie 的校验更严格了匿名抓评论会直接被拒绝所以我还是建议带上登录后的 cookie 跑MUSIC_U过期后刷新一下再放回代码。另外请求频率上建议每分钟不要超过 120 次这是个人实践下来的可靠节奏再高就会开始出现 460。4.3 并发抓取时怎么控制频率评论接口的特点是单次返回量小20 条但需要翻很多页所以很多人的第一反应是上多线程。实际上 weapi 的加密和解密都是 CPU 密集型操作Python 的 GIL 会让多线程在这里收益有限反倒限速和请求时机更重要。如果你目标明确要抓某首热门歌的几万条评论可以分两个阶段第一个阶段单线程慢慢抓前 500 条把hotComments完整保存第二个阶段再开 3 到 4 个线程按评论区不同时间段分批翻页。ThreadPoolExecutor在这里够用分布式爬虫那些方案如果只是抓网易云音乐单个站点属于过度设计除非你要把整个站点的歌单都抓下来才值得考虑。5. 数据落地与自检断点续抓、报错定位和实用扩展5.1 落盘结构与增量更新抓下来的数据建议按歌手 id 建目录目录里分成albums.json、songs.json、lyrics/、comments/四个部分。评论数据量大不要全堆在一个文件里按歌曲 id 单独存更便于排查。跑批任务时把“已经抓过评论的歌曲 id”记录到一个done_songs.txt每处理完一首就写一行。程序中断后重跑时只读这个文件跳过已完成的歌曲这样断点续抓的实现成本最低比用数据库靠谱得多。数据清洗的一个细节json 文件里保存数字 id 时网易云返回的是整数但歌曲 id 超过了 JavaScript 的数字安全范围转成字符串再存储避免精度丢失。列表字段里歌手和专辑名建议规范化后再存比如把歌手名里的空格去掉专辑名统一用al.name而不是album.name因为不同接口字段名有差异统一标准比事后对齐舒服。5.2 自检清单常见的报错和修复方法报错现象直接原因修复方式code: 400或请求参数错误params 数据结构不对检查 data 是否 JSON 序列化ids 是否加了引号code: 460IP 或 cookie 被风控停止请求 3 到 5 分钟刷新 MUSIC_UKeyError: songs歌曲 id 不存在或已下架用res.get(songs)拿到 None 时跳过返回[object Undefined]lyric 接口传了专辑 id确认传入的 id 是歌曲 id评论一直输出同一页cursor 未更新检查是否把返回的 cursor 传回了下一次请求歌曲详情里没有歌词纯音乐或外语歌无翻译判断lrc字段是否为空字符串自检时先在命令行里手动请求一次/api/song/detail确认加密和 cookie 正常再跑批量任务。不要在数据抓完才发现错误拼写单个接口的错误消息在批量任务里会被海量日志淹没一开始就打印出返回的code字段是好习惯。5.3 抓歌手资料时顺手把粉丝数和播放量也存下来最后分享一个实用技巧在抓歌手热门歌曲列表时接口同时返回了每首歌的playCount播放量需要传private参数到某些接口才返回但更常见的是歌手详情接口返回粉丝数fansCount和歌曲总数musicSize。把这三个字段一起存进歌手档案就能直接做“歌手歌曲总量 vs 粉丝量”的对比分析。粉丝数能反映影响力播放量能反映传唱度两者结合比单看评论数更能说明问题。处理这种多维数据时我习惯在每个 json 文件头加一个_meta字段记录抓取时间和来源接口后续做增量更新时直接对比时间戳就行。本文还有配套的精品资源点击获取