资讯动态

网易云音乐爬虫实战:从歌手到评论的全链路数据采集与反爬解析

发布时间:2026/9/11 22:54:48 来源:尧图企业网站定制
简介这份资源是一套基于Python的网易云音乐数据采集源码包面向爬虫学习者和需要批量获取音乐元数据的开发者。项目围绕歌手、专辑、歌曲、歌词与评论五大维度展开评论模块包含热评和前一千条完整抓取适合练习接口分析、请求发送、解析提取与MySQL存储等关键技能。压缩包共22个文件其中14个Python脚本承担核心采集逻辑按artist、album、music、lyric、comment等模块化切分还配有建表SQL、评论词云分析脚本与生成的词云图、Redis工具、Markdown说明、许可证和gitignore配置整体体积约12.26MB。目前已有860人学习下载。可重点学习的是不同数据表之间的关联抓取方式应对“爬多了可能被禁”的限流提醒以及从数据库到词云可视化的完整链路。目录组织清晰便于读者直接运行或二次改造是一份能省去不少踩坑时间的好资料。1. 网易云音乐爬虫的数据链路从歌手到评论的抓取边界接触过网易云音乐爬虫的人都知道这个站点的难点不在加密参数而在数据链路太长你想拿一首歌的评论得先有歌曲 ID而歌曲 ID 要从专辑里拿专辑 ID 又要从歌手页里翻。很多爬虫项目只给单点脚本比如单独爬评论或者单独爬歌词真正能串起“歌手 → 专辑 → 歌曲 → 歌词 → 评论”全链路的很少。这次拆的163MusicSpider-master就是完整跑通这条链路的工程它把每个环节拆成独立模块配合 MySQL 和 Redis适合做音乐数据分析、评论情感分析、冷启动歌单推荐的人直接二次开发。项目结构里值得注意的有两点一是所有请求都封装在src下的util里说明作者考虑过请求头、代理、重试的统一管理二是爬取结果不落文件而是落库db.sql定义了歌手、专辑、歌曲、歌词、评论五张表这给后续的统计分析留了接口。如果你的目标只是拿数据做一次性分析可以考虑只跑artists.py和comments_by_music.py但要在生产环境长期抓必须理解它的请求频率控制和去重策略——这也是我下面每一章都会强调的部分。2. 歌手与专辑抓取分页遍历时的参数陷阱2.1 歌手列表的两种抓取思路artists.py负责爬取所有歌手信息底层调的是/api/artist/list这个接口。网易云的歌手列表有三种口径按分类华语/欧美/日本/韩国、按首字母、按热门程度。这个项目默认按热门程度分页每页 20 条通过offset和limit控制翻页。初次跑的时候建议先限制分类比如只跑华语男歌手否则全量歌手会远超预期。# artists.py 关键逻辑 def fetch_artist_list(offset0, limit20): params { type: -1, # -1: 全部, 1: 男, 2: 女, 3: 乐队 area: 7, # 7: 华语, 8: 欧美, 9: 日本, 6: 韩国 initial: -1, # -1: 热门, 其他: 首字母对应数字 offset: offset, limit: limit } data request_api(/api/artist/list, params) for artist in data.get(artists, []): save_artist(artist)参数里的type和area组合会极大影响数据量比如type2area8是欧美女歌手type3area6是韩国乐队。initial只有在按字母检索时生效-1代表热门排序此时接口返回的more字段决定是否继续翻页。2.2 专辑列表的接口选择与去重album_by_artist.py通过歌手的artist_id调用/api/artist/albums拉取专辑。这里容易踩的坑是接口有两种返回hotAlbums和more。如果是热门专辑列表hotAlbums会返回全部热专但如果你传入limit200接口可能只回前 20 条需要根据more字段判断是否继续。实际抓取时我习惯先检查数据库里是否已有该歌手的专辑记录再决定是否发起请求。# album_by_artist.py 片段 def fetch_albums_by_artist(artist_id): offset 0 while True: params {id: artist_id, offset: offset, limit: 50} data request_api(/api/artist/albums, params) albums data.get(hotAlbums) or [] for album in albums: save_album(album, artist_id) if not data.get(more): break offset 50注意这里的offset是每 50 条递增但网易云对单个歌手的专辑数超过 500 时会限制翻页深度超过后接口会返回空列表。遇到这种情况我一般会改走专辑搜索接口/api/search用artist_name 专辑类型做关键词补充不过会引入更多脏数据需要人工校验专辑名。2.3 歌手表与专辑表的设计取舍db.sql里歌手表和专辑表的主键分别是artist_id和album_id直接使用网易云自身的 ID不搞自增主键。这个设计我很认同——因为评论、歌词、歌曲表都要引用这两个 ID用外部 ID 做外键能避免重复插入时的冲突判断。-- db.sql 关键表结构 CREATE TABLE artist ( artist_id BIGINT PRIMARY KEY, name VARCHAR(255) NOT NULL, pic_url VARCHAR(500), hot_score INT DEFAULT 0 ); CREATE TABLE album ( album_id BIGINT PRIMARY KEY, artist_id BIGINT NOT NULL, name VARCHAR(255) NOT NULL, publish_time DATETIME, song_count INT DEFAULT 0, KEY idx_artist (artist_id) );写库时要注意publish_time网易云返回的是毫秒时间戳需要先除以 1000 再转DATETIME否则入库时间全是 1970 年。另外专辑封面picUrl有时会带?param300y300这类尺寸参数建议去掉后再存方便后续做图片分析。3. 歌曲与歌词抓取从专辑到单曲的关联与加密参数处理3.1 歌曲列表的核验逻辑music_by_album.py根据album_id调/api/album拿歌曲清单。这个接口返回的tracks数组里包含歌曲名、歌手 ID、时长、音质信息。真正要留意的不是接口本身而是数据质量问题同一首歌可能出现在多个专辑里精选集、现场版、伴奏版如果你直接INSERT会造成大量重复。我观察这个项目里的做法是检查song_id是否已存在存在则跳过不存在才插入。但这会漏掉“同一首歌的不同版本”的采集需求。如果后面要做音质对比或版本分析建议在歌曲表加一个hash字段用song_name artist_name duration计算 MD5更准确。# music_by_album.py 处理逻辑 def fetch_music_by_album(album_id): data request_api(/api/album, {id: album_id}) for track in data.get(songs, []): song { song_id: track[id], album_id: album_id, name: track[name], duration: track[dt] // 1000, artists: ,.join(a[name] for a in track[ar]) } insert_ignore_song(song)dt字段单位是毫秒代码里转成秒存储。ar里的id才是歌手 IDname只适合展示不适合做关联查询。如果你想做歌手维度的歌曲列表建议把ar里的每个歌手拆出来单独建一张song_artist关联表否则多歌手合作的歌会丢数据。3.2 歌词接口的加密参数生成lyric_by_music.py是这个项目里技术含量最高的部分。网易云歌词接口/api/song/lyric需要两个 POST 参数params和encSecKey它们是通过 AES 和 RSA 加密生成的。直接复制网页端请求头会被风控常见做法是模拟 Web 端生成这两个参数。# lyric_by_music.py 中的加密参数构造 from Crypto.Cipher import AES import base64, random, hashlib, json def generate_enc_params(song_id): text {id:%d,lv:-1,kv:-1,tv:-1} % song_id nonce .join(random.choice(abcdefghijklmnopqrstuvwxyz0123456789) for _ in range(16)) key 0CoJUm6Qyw8W8jud iv 0102030405060708 # 第一层 AES 加密 pad 16 - len(text) % 16 text chr(pad) * pad aes1 AES.new(key.encode(), AES.MODE_CBC, iv.encode()) enc1 base64.b64encode(aes1.encrypt(text.encode())).decode() # 第二层 AES 加密使用随机 nonce aes2 AES.new(nonce.encode(), AES.MODE_CBC, iv.encode()) enc2 base64.b64encode(aes2.encrypt(enc1.encode())).decode() return enc2, rsa_encrypt(nonce)这里的nonce是随机 16 位字符串每次请求都要重新生成否则返回 403。rsa_encrypt是固定的公钥加密公钥值在项目util里写死。实际调测时我发现lv-1表示不区分语言版本tv-1表示不翻译如果换成lv0只能拿到原始歌词部分外语歌会丢翻译。如果你的分析需要中英对照接口里要加txv:-1。3.3 歌词清洗与断行处理拿到歌词后要处理lrc字段里的时间轴和tlyric里的翻译。项目里lyric_by_music.py存的是纯文本没有解析逐行时间戳。做歌词分析或“逐句跟唱”场景时需要拆成两列time_ms和content。我的做法是按[mm:ss.xx]正则切分后转成毫秒存lyric_line表这样后续做时间轴对齐才方便。pip install pycryptodome requests mysql-connector-python启动前记得装依赖Crypto库在新版本 Python 里包名改了。main.py里的执行顺序是先歌手、再专辑、再歌曲最后才是歌词因为每一步都依赖上一步的 ID 列表跑完一遍后你可以只做增量验证从库里随机挑 10 首歌 ID单独执行歌词抓取确认输出非空。4. 评论抓取与存储热评、前 1000 条与词云分析的完整链路4.1 评论接口的翻页参数与排序comments_by_music.py抓热评和前 1000 条评论核心接口是/api/v1/resource/comments/R_SO_4_song_id。排序参数sortType是关键sortType0是推荐排序热评sortType2是最新排序。热评翻页是滚动加载最新评论则是用cursor游标翻页不是offset。项目里写死了只抓前 1000 条这个量级对情感分析足够但也意味着热门歌曲 10 万条评论只取了 1%。# comments_by_music.py 内的翻页逻辑 def fetch_comments(song_id, max_pages50): page 0 cursor 0 while page max_pages: data { rid: R_SO_4_%s % song_id, cursor: cursor, offset: page * 20, pageSize: 20, sortType: 2 } resp request_api(/api/v1/resource/comments, data) comments resp.get(comments, []) for c in comments: save_comment(song_id, c) if resp.get(hasMore) is not True: break cursor resp[cursor] page 1sortType2时cursor是接口返回的不透明字符串不能自己拼sortType0时传pageSize和offset更稳。另外评论数超过 1000 后max_pages必须可控因为 50 页就是 1000 条跑一次热门歌曲要 50 次请求在高并发下极易触发验证码。4.2 评论表设计及用户信息丢弃问题db.sql里的评论表只存了评论 ID、歌曲 ID、内容、时间、点赞数没有存user_id和nickname。如果你要做“某用户评论了哪些歌”的行为分析这个表不够。我建议在现有表上扩展ALTER TABLE comment ADD COLUMN user_id BIGINT DEFAULT 0; ALTER TABLE comment ADD COLUMN user_nickname VARCHAR(100) DEFAULT ; ALTER TABLE comment ADD COLUMN liked_count INT DEFAULT 0; ALTER TABLE comment ADD KEY idx_user (user_id);liked_count在首次抓取后基本不变除非做定时跟踪。评论内容字段建议用TEXT网易云评论最长约 1000 字VARCHAR(255)会截断。另外time字段是毫秒时间戳和歌词表保持一致即可。4.3 词云分析的实现与问题word_cloud_by_comment.py用 jieba 分词后生成词云图commentCloud.png。源码里用的是默认字体中文会显示成方块所以必须指定中文字体路径。常见做法是找系统的msyh.ttc或下载simhei.ttf。# word_cloud_by_comment.py 关键片段 import jieba from wordcloud import WordCloud, STOPWORDS def build_word_cloud(song_id): comments load_comments(song_id) text .join(comments) wc WordCloud( font_path/usr/share/fonts/truetype/simhei.ttf, # 中文字体 width1200, height800, stopwordsSTOPWORDS | {真的, 感觉, 还是, 就是}, background_colorwhite, max_words200 ).generate(text) wc.to_file(commentCloud.png)stopwords必须根据歌曲类型调整比如慢歌的“深夜”“回忆”可能是有效词快歌的“啊啊啊”“哈哈”则是噪音。建议先看 jieba 分词结果再定停用词表别一开始就套通用词表。WordCloud的collocationsFalse可以关闭二元词组否则“我 爱”和“我爱”会重复计入。5. 进阶Redis 去重、分布式抓取与反爬降级策略5.1 Redis 在任务队列与去重中的角色redis_util.py封装了 Redis 的基本操作在项目里的角色是任务队列和 URL 去重。当你把歌手 ID、专辑 ID 灌入 Redis 的 List 后多个 worker 可以LPOP取任务配合SADD记录已处理 ID避免重复抓取。这种模式比单机多线程更稳因为单线程挂了任务会丢Redis 队列能保底。# redis_util.py 常用函数 import redis pool redis.ConnectionPool(hostlocalhost, port6379, db0, decode_responsesTrue) r redis.Redis(connection_poolpool) def push_task(queue, task_id): r.lpush(queue, task_id) def pop_task(queue): return r.rpop(queue) def is_processed(set_name, task_id): if r.sismember(set_name, task_id): return True r.sadd(set_name, task_id) return False注意rpop是阻塞还是非阻塞默认非阻塞队列空时返回None。多 worker 并发时会存在“一个任务同时被多个 worker 弹出”的竞争解决方式是brpoplpush把任务放入临时队列处理成功后再lrem否则回退原队列。这个技巧适合抓取服务偶发崩溃的场景但项目源码里没有实现需要自行补。5.2 限频策略从单纯 sleep 到自适应退避项目在util里用一个request_api统一封装了请求但那里没有限速逻辑。网易云的风控主要看两个维度单 IP 请求频率、账号维度。如果只爬歌手和专辑频率控制在每秒 2 次基本没问题一旦涉及评论和歌词每秒 2 次连续半小时也会触发验证码。# 自适应限频伪代码 import time class RateLimiter: def __init__(self, max_qps2): self.interval 1.0 / max_qps self.next_time 0 self.fail_count 0 def wait(self): now time.time() if now self.next_time: time.sleep(self.next_time - now) self.next_time time.time() self.interval def on_failure(self): self.fail_count 1 if self.fail_count 3: self.interval * 1.5 # 每次失败后拉长间隔 self.fail_count 0实际测试里返回 403 不一定是封 IP可能是缺少Referer或Cookie。先用浏览器打开网易云复制完整 Cookie 到util里的headers字典能解决 90% 的初次被拒问题。另外网易云返回-460错误码是“操作太频繁”-461是“登录状态失效”这两个要分别处理-460退避 30 秒-461重新登录。5.3 增量更新与断点续抓全量爬完一遍后增量更新才是常态化需求。我的做法是给歌手表加last_crawl_time字段每天只爬last_crawl_time之后发过新专辑的歌手。但这个项目没有这个字段你可以用歌曲表的publish_time反向过滤只抓最近 7 天入库的专辑再递归抓歌。断点续抓更简单artists.py每次运行都SELECT COUNT(*)已入库大于 0 就直接跳过分页参数小于当前总数的部分。不要用文件记录断点进程一重启就丢。把进度写入 RedisHash比如hset crawl_progress artist_offset 1000每次翻页前读取。如果项目部署在服务器上配合crontab每天凌晨跑一次增量脚本数据量不会比全量少太多但请求量能降一个数量级。5.4 歌词匹配的健壮性增强lyric_by_music.py拿到的歌词偶尔是纯音乐或没有词接口会返回nolyrictrue的标识。代码里如果没有判空会把None写进数据库。我建议抓取前在内存里做一次判断if not data.get(lrc, {}).get(lyric): return None。另外网易云部分翻唱歌词和原唱混在一起lyricUser字段可以区分上传者分析时只保留lyricUser为网易云官方账号的歌词更干净。最后一个技巧评论词云图生成后不要直接看 PNG先统计jieba.cut的词频表确认 Top20 里没有无意义词再出图。这个项目里commentCloud.png是静态输出你在二次开发时可以把词频表写进数据库后续做时间序列的情感变化趋势分析才有数据支撑。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价