
简介基于Java实现的网易云音乐数据抓取爬虫项目面向正在学习网络爬虫与数据采集的初中级开发者也适合作为数据分析场景下的数据获取参考。项目采用Maven标准结构包含歌曲、评论等信息的抓取逻辑演示了从页面请求、内容解析到数据库持久化的完整流程并附有数据库初始化脚本与使用说明能帮助读者快速搭建环境理解爬虫工程化写法。压缩包共23个文件核心为18个Java源文件另有2个XML配置、1个Properties配置文件、1个数据库SQL脚本和1份Markdown说明整体仅23KB代码量适中目录结构清晰便于逐文件研读与二次开发。读者既可以参考其抓取思路改造为自己的采集工具也可以学习如何管理请求参数、处理响应数据并写入数据库。目前已有90人学习下载适合作为爬虫实战入门案例也可为后续扩展其他音乐平台或站点采集提供基础。 做爬虫这几年我其实很少遇到像网易云音乐这样“既好抓又难抓”的数据源。说好抓是因为它的Web端API非常丰富歌单、歌曲、评论、用户信息几乎都有现成接口说难抓是因为接口签名更新频繁、限流策略越来越严格如果不做好规划很容易爬到一半就被封了IP。项目《_NetEaseMusicSpider.zip》是我去年做音乐数据分析课题时写的一个完整工程定位很简单用Python抓取网易云音乐的歌单、歌曲详情和评论数据清洗后统一存入SQLite供后续做用户偏好分析和歌曲传播路径研究。这篇文章就从设计思路、核心实现到避坑经验完整复盘一遍希望能给正在研究爬虫技术或者对音乐数据挖掘感兴趣的朋友一点参考。1. 项目定位与整体设计思路1.1 先想清楚抓什么数据再想怎么抓很多新手拿到一个爬虫项目第一反应是“怎么绕过反爬”但我的习惯是反过来先把数据模型定义清楚再决定技术方案。这个项目的目标数据最终要服务两类分析场景——歌曲热度趋势分析和评论情感词频统计所以我把数据需求拆成了四个维度歌单基础信息歌单ID、名称、创建者、播放量、标签、收藏数。歌曲基础信息歌曲ID、名称、歌手、专辑、时长、发行时间。评论数据评论内容、点赞数、评论时间、用户昵称。用户信息附带抓取用户ID、用户等级、性别、创建时间。字段明确以后爬虫才有“完成”的标准。我之前见过不少项目爬虫写完了却不知道字段是否完整也没办法验证数据质量最后分析时才发现关键字段缺失只能返工。这里也建议你做任何爬虫项目前先用文档列出目标表结构再动代码。1.2 技术选型逻辑这个项目我没有用Scrapy而是选择了requestslxmlSQLite的轻量组合原因很直接Scrapy虽然功能强大但对于单机万级数据量的抓取任务来说框架本身的学习和配置成本偏高。requests配合Session管理Cookie非常灵活网易云音乐的接口需要通过Cookie维持登录态用Session天然合适。数据量在十万条以内时SQLite的单文件数据库读写效率足够且不需要额外部署数据库服务。解析层用lxml做XPath提取部分接口返回的是JSON直接用json模块处理不需要引入重型解析库。考虑到后续可能需要扩展到百万级数据量我在存储层做了一层抽象定义统一的DatabaseWriter基类SQLite和MySQL各写一个实现切换数据源时只需要改配置项。这个设计后面抓评论数据时帮我省了不少事因为评论量大到一定程度SQLite频繁写入会出现锁竞争。2. 网易云音乐API的调用细节与请求构造2.1 请求头和Session的构造是第一步网易云音乐的Web端API拼接方式比较统一大部分接口都遵循/api/xxx的模式请求头需要携带一些关键信息才能拿到完整数据。我实际使用的请求头构造如下import requests session requests.Session() session.headers.update({ 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: https://music.163.com/, Origin: https://music.163.com, Accept: application/json, text/plain, */*, Content-Type: application/x-www-form-urlencoded, Cookie: 换成你自己的Cookie })这里有个关键点User-Agent和Referer是基础的“身份标识”但真正影响数据完整性的是Cookie。网易云音乐的很多接口未登录状态和登录状态返回的数据字段差很多——比如歌单的creator信息、评论区的用户信息、歌曲的fee字段是否付费都需要登录态才能完整拿到。2.2 歌单列表接口的请求与响应解析这个项目里我主要使用了两个核心接口第一个是歌单分类接口用于获取某个标签下的歌单ID列表POST /api/playlist/list 参数cat分类标签、order排序方式、offset偏移量、limit每页数量第二个是歌单详情接口用于获取歌单内歌曲的完整列表POST /api/playlist/detail 参数id歌单ID重点说一下/api/playlist/list接口的参数细节。cat参数控制分类比如“华语”“流行”“民谣”“摇滚”通过不同标签的遍历覆盖可以建立比较完整的歌单样本池。order参数我测试下来hot最热和new最新返回的数据质量差异很大hot很多是运营推荐歌单刷量痕迹明显new是用户新建歌单数据更真实但质量参差不齐。我最后的抓取策略是hot和new各抓一部分按歌单维度做去重。响应结构里有一个需要特别注意的点歌单详情接口返回的JSON中trackIds字段只包含歌曲ID列表而playlist.tracks字段才是完整歌曲对象。但如果歌单内歌曲数量超过1000首接口默认只返回前1000首需要在代码里判断trackCount字段是否大于返回的tracks数量再决定是否用歌曲ID列表逐个补齐。2.3 加密接口与Cookie的边界说明网易云音乐Web端的部分接口比如评论区的完整接口、部分用户详情接口采用了名为weapi的加密调用方式本质上是AES对称加密和RSA非对称加密的混合方案。实现思路大家可以在公开资料里找到核心是生成一个params加密参数和一个encSecKey加密密钥然后以表单形式POST到/weapi/xxx路径。这里我想坦诚地说明我的做法项目初期我尝试过weapi的全部接口但后来重新评估了合规性和必要性最终只在代码里保留了明文接口的抓取逻辑只对/weapi机制做了技术验证没有在工程里落地使用。原因是网易云音乐的《用户协议》明确禁止未经授权的爬取行为作为技术学习者我对“加密绕过”始终保留限度——验证技术可行性是一回事把绕过方案做成产品是另一回事。市面上有不少开源方案但直接使用可能带来法律风险这点务必自行评估。对于评论这类需要weapi才能拿到完整数据接口的需求我的替代方案是使用网易云音乐的开放平台API如果目标字段有覆盖或者使用移动端eapi接口的部分明文参数做有限度的抓取。如果只是做数据分析课题歌单和歌曲基础数据已经足够撑起大部分研究场景了。3. 核心模块实现与数据落地3.1 从歌单ID到歌曲列表的串联抓取流程整个爬虫的抓取流程我用一个状态机来控制简化后的核心代码如下import time import json import sqlite3 from typing import List, Dict def fetch_playlist_ids(session, cat: str, offset: int, limit: int 30) - List[str]: url https://music.163.com/api/playlist/list payload { cat: cat, order: hot, offset: offset, limit: limit, total: True } try: resp session.post(url, datapayload, timeout10) resp.raise_for_status() data resp.json() except Exception as e: print(f[ERROR] 获取歌单列表失败: {e}) return [] playlists data.get(playlists, []) ids [str(item[id]) for item in playlists if item.get(id)] return ids def fetch_playlist_detail(session, playlist_id: str) - Dict: url https://music.163.com/api/playlist/detail payload {id: playlist_id} resp session.post(url, datapayload, timeout10) data resp.json() playlist data.get(playlist, {}) track_list playlist.get(tracks, []) track_count playlist.get(trackCount, 0) # 如果返回的 tracks 数量小于 trackCount说明歌曲被截断 # 此时需要用 trackIds 字段逐个补齐歌曲详情 if len(track_list) track_count: track_ids [item[id] for item in playlist.get(trackIds, [])] track_list [fetch_song_detail(session, tid) for tid in track_ids[:track_count]] return { playlist_id: playlist_id, name: playlist.get(name), tracks: track_list, play_count: playlist.get(playCount), tags: ,.join(playlist.get(tags, [])) }看到这里你会发现抓取逻辑本身并不复杂真正的复杂度在于“异常处理和状态转移”。比如遇到网络抖动导致某个请求超时是直接跳过还是重试如果歌单详情抓了一半程序崩溃了怎么断点续传这些问题都需要在代码里考虑清楚。3.2 歌曲详情与评论数据的二次请求歌曲详情接口相对简单POST /api/song/detail 参数c[{id: 歌曲ID}]这个接口支持批量查询一次最多传1000个歌曲ID返回的songs数组按请求顺序排列。批量请求是提升效率的关键我实测从单曲逐个请求改为批量100个请求后抓取速度提升了6倍以上。评论数据这个项目里我主要抓了热门评论接口为POST /api/comment/playlist?idxxxpage1pageSize20实际测试发现这个明文接口返回的是热门评论且分页参数offset的有效范围有限超过一定深度会被限流。如果要对全部评论做深度抓取就需要weapi体系里的/weapi/comment/resource/接口了这个我没有落地原因前面已经说过。3.3 数据清洗与SQLite落库数据落地这一层我设计了一个统一的清洗函数。因为接口返回的数据并不直接适合入库举例来说playCount字段是数字但偶尔会有null值需要做默认值填充。歌曲的duration字段单位是毫秒展示层面需要转为mm:ss格式。评论的time字段是Unix时间戳入库前要转为datetime格式便于后续分析。歌手字段artists是一个数组需要提取歌手名拼接为字符串。清洗逻辑示例def clean_track_info(track: Dict) - Dict: artists [a.get(name, ) for a in track.get(artists, [])] album track.get(album, {}) or {} return { song_id: track.get(id), name: track.get(name), artists: / .join(artists), album: album.get(name), duration_ms: track.get(duration, 0), duration_text: f{track.get(duration, 0) // 60000}:{(track.get(duration, 0) % 60000) // 1000:02d}, publish_time: album.get(publishTime), fee: track.get(fee, 0) }表结构我设计得比较轻量核心三张表playlists、tracks、playlist_tracks。这里我特意没有建comments表因为后续需要更完整的数据会把评论单独做成一个爬虫任务不混在基础数据抓取流程里。建表SQL和插入逻辑这里不贴全代码了核心思路是使用INSERT OR IGNORE避免重复插入。关键字段加唯一索引如歌曲ID、歌单ID。每批次提交使用事务避免大量小事务导致SQLite锁竞争。数据库设计看起来不起眼但前期把索引和唯一约束设计好能省下后面大量清洗去重的功夫。4. 反爬应对与性能优化实践4.1 做一个“懂事”的爬虫“懂事”这个词是我在实际项目里总结出来的爬虫不是越快越好而是要懂得克制懂得动态调度。网易云音乐的限流策略我摸下来大概有这几条规律同一个IP的请求频率如果长期超过每秒3次触发封禁的概率会急剧上升。单接口高频访问比多接口轮流访问更容易触发风控。深夜时段凌晨2点到5点的请求更容易被标记为异常行为。所以我在爬虫里加了一个Ticker限速器核心逻辑是滑动窗口限流class Ticker: def __init__(self, max_calls: int, period: float 1.0): self.max_calls max_calls self.period period self.calls [] def wait_if_needed(self): import time now time.time() # 清理超出时间窗口的调用记录 self.calls [t for t in self.calls if now - t self.period] if len(self.calls) self.max_calls: sleep_time self.period - (now - self.calls[0]) if sleep_time 0: time.sleep(sleep_time) self.calls.append(time.time())这个简单的滑动窗口限流器比time.sleep(1)这种方式高明在它能在保证平均请求频率的同时允许短时间内的微突刺整体请求节奏更自然也更不容易被风控识别为机器行为。4.2 异常重试与断点续抓抓取过程中网络错误和服务端错误是不可避免的。我的重试策略是对超时错误requests.exceptions.Timeout做指数退避重试最多重试3次。对HTTP 5xx错误服务器错误做固定间隔重试最多重试2次。对HTTP 4xx错误尤其是403不重试直接记录日志并跳过。断点续抓这块我维护了一个游标文件state { current_cat: 华语, offset: 120, failed_playlists: [], last_update: 2024-01-15 22:30:00 } # 每次抓完一个分类及时更新状态文件 with open(crawler_state.json, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2)这样做的好处是即使程序因为断电或其他原因中断重启后也能从上次的位置继续抓取不需要从头来过。这个设计对于长时间爬取任务几乎是必备的。4.3 多线程并发抓取的取舍项目初期我用单线程跑抓取一万个歌单大概需要4小时。后来优化为多线程但一开始直接上20个线程结果没过10分钟就被限流了。后来我改成线程池 限速器组合方案8个线程每个线程共享同一个限速器把整体请求频率控制在每秒不多于3次。这个策略实测下来稳定性好了很多from concurrent.futures import ThreadPoolExecutor, as_completed ticker Ticker(max_calls3, period1.0) def worker(playlist_id: str): ticker.wait_if_needed() return fetch_playlist_detail(session, playlist_id) with ThreadPoolExecutor(max_workers8) as executor: futures {executor.submit(worker, pid): pid for pid in playlist_ids} for future in as_completed(futures): result future.result() if result: write_to_db(result)这里有个容易被忽略的细节requests.Session不是线程安全的。多线程场景下要么为每个线程创建独立的Session要么给Session加锁。我实测下来为每个线程创建独立Session是更稳妥的做法因为Session内部持有的连接池存在状态并发写同一Session可能引发不可预期的错误。5. 常见问题与排查技巧实录5.1 高频报错汇总与解决办法我把项目调试期间的典型报错整理成了一个表格这里结合排查思路一起说明报错现象可能原因解决方案requests.exceptions.ConnectionError网络不稳定或者IP被临时封禁等待一段时间后重试或切换网络出口HTTP 403Cookie失效或者User-Agent被识别更新Cookie检查请求头是否完整JSONDecodeError: Expecting value接口返回了HTML而不是JSON可能是被跳转到了验证页检查响应状态码和响应内容判断是否被风控SQLitedatabase is locked多线程同时写数据库改为单写线程或使用WAL模式返回数据中歌曲列表为空歌单ID无效或歌单被设置成不可见跳过该歌单并记录原因排查这类问题的一个高效思路是先用Postman手动请求一遍接口确认接口本身没有问题再回到代码里查问题。很多新手一上来就调试代码其实如果接口本身返回了异常响应代码层面再优化也解决不了。5.2 数据完整性校验数据抓取完之后完整性校验往往被忽略但这恰恰是影响后续分析质量的关键一环。我常用的三个校验手段数量校验对比歌单表的trackCount字段和playlist_tracks表中对应歌单的歌曲数量不一致则说明漏抓了。字段非空校验统计关键字段歌曲名、歌手名的空值比例如果超过5%需要回头排查清洗逻辑。时间分布校验看歌曲的发行时间分布是否符合真实规律。如果某个时间段的数据明显畸高或畸低可能说明抓取过程中存在系统性偏差。我这里举一个真实案例第一版爬虫抓完以后我发现1970年发行的歌曲占比高达30%排查后发现问题出在清洗环节——接口中部分歌曲的publishTime字段为0我没有填充默认值导致Unix时间戳0被转成了1970年1月1日。这种细节bug如果不做时间分布校验几乎发现不了。5.3 关于合规使用的一些思考最后想认真聊一下爬虫的“边界”问题。这个是很多爬虫项目容易被忽略、但真正重要的部分。写爬虫的人往往聚焦于技术实现但“能爬”和“该爬”是两回事。我在这个项目中的实践是只抓取公开接口可获取的数据不通过逆向手段获取加密接口的核心密钥。控制请求频率不对目标服务造成压力。抓取数据仅用于个人学习研究不用于商业用途不在任何平台传播原始数据。在代码仓库的README里明确写明使用条款和数据来源声明。爬虫技术本身是中性的但使用场景决定了它的边界。网易云音乐作为平台方有权利保护自己的数据资产作为技术学习者我们更应该理解并尊重这种保护——学习的目的是提升技术能力而不是钻空子。如果你也在做类似的数据抓取和分析项目我建议你在项目最开始就定义好“哪些数据不抓”的红线这样后续做技术选型和架构设计时会有更清晰的判断依据。写在最后这个项目还能往哪里扩展从《_NetEaseMusicSpider.zip》这个项目出发能扩展的方向其实不少。如果你对数据分析感兴趣可以基于抓取的歌单和歌曲数据做用户听歌偏好聚类或者分析不同风格歌曲的传播路径如果你对爬虫技术本身感兴趣可以尝试把数据源扩展到电台、MV、歌单广场等模块如果你对后端工程感兴趣可以把存储层从SQLite平滑迁移到MySQL或PostgreSQL再加上一个定时调度任务来增量更新。我个人在完成这个项目后最大的收获其实不是爬虫技术本身而是养成了一套“先把数据模型想清楚再动手”的做事习惯。爬虫只是数据获取的手段真正的价值在于后续的分析、挖掘和业务价值转化。希望这篇文章能帮你少踩一些我踩过的坑也期待看到你基于这些数据做出更有意思的场景应用。本文还有配套的精品资源点击获取