简介:面向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 = b'0102030405060708' 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就是常见的 010001,MODULUS是那个 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, headers=headers, data=params, timeout=10) 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_type='song'): 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: 460 | IP 或 cookie 被风控 | 停止请求 3 到 5 分钟,刷新 MUSIC_U |
KeyError: '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字段记录抓取时间和来源接口,后续做增量更新时直接对比时间戳就行。
本文还有配套的精品资源,点击获取