简介面向Java开发者的网易云音乐数据抓取爬虫项目适合对爬虫技术、音乐数据采集感兴趣的初学者与进阶开发者重点解决如何通过代码批量获取歌曲、评论等公开数据的实际问题。压缩包共23个文件其中18个Java源码文件构成抓取核心逻辑搭配Maven工程配置文件与属性配置文件管理依赖和运行参数另附SQL脚本用于建表存储数据以及说明文档帮助快速上手整体包体仅23KB轻量易分析。项目包含完整的抓取流程实现如请求构造、响应解析、数据清洗与入库环节读者可据此学习Java爬虫的常见写法理解如何从网页或接口提取目标字段并通过SQL脚本存储于本地数据库同时可直接修改配置与SQL适配自己的采集需求也能借鉴其中的异常处理与工程结构设计。目前已有90人学习下载适合用作课程设计参考或个人爬虫入门练习更是了解真实爬虫项目目录结构的便捷样例。1. 抓网易云音乐数据从“能抓到”到“抓得干净”隔着多少层第一次拿到“抓取网易云音乐的数据_NetEaseMusicSpider.zip”这个标题时我脑子里冒出来的不是“装好环境就能跑”而是三个反问能抓到、抓得全、抓得干净这三件事之间到底隔着多少层。网易云音乐的数据抓取最容易被低估的点在于发一个请求拿回 JSON 谁都会但请求频率稍微快一点就触发验证码接口换一个版本就少几个字段本地跑得好好的脚本放到服务器上却一片超时。这些都是实际会遇到的场景不是玄学。这篇笔记从接口边界讲起逐步落到可复现的代码骨架、参数取舍和避坑经验。适合的人群很明确做歌单分析、音乐推荐研究、榜单追踪或刚接触网页接口爬虫的从业者。我默认你已经会用 Python 和 requests但没写过完整的抓取工程。读完你至少能得到一个能跑通的最小实现以及一套“跑挂了之后怎么排查”的方法。2. 先分清楚抓什么接口边界、数据层级与抓取策略2.1 把需求拆到接口粒度搜索、歌单、歌曲详情与评论的四种抓取场景做网易云音乐数据抓取第一件事不是写代码而是把需求拆到“接口粒度”。绝大多数从业者并不是想爬全站他们想要的其实是下面四类数据中的一类或几类。搜索数据是最常见的入口给定一个关键词拿到歌曲列表。典型需求是“我不知道歌名全称只记得几个字”或者“我想批量研究某个歌手的热门歌”。搜索接口返回的是歌曲摘要包含歌曲 id、歌名、歌手数组、专辑名、时长等。这里的重点是搜索结果是摘要不是详情。歌单数据是另一个高价值场景。歌单相当于已经被人整理好的“音乐知识库”一首歌会出现在哪些歌单里歌单的作者是谁歌单的收藏数多少这些信息比单首歌曲本身更有分析价值。抓歌单时通常先拿歌单列表再拿歌单详情详情里会带上多首歌曲 id剩下的再逐首补详情。歌曲详情和评论数据则是深度分析才会用到的。歌曲详情里带版权状态、音质信息、专辑图片评论数据则适合做文本情感分析或热度排序研究。这两个接口的请求量通常很大因为它们需要按歌曲 id 逐个请求没法批量带出。我一般会建议先画一张数据层级图再动手搜索 → 歌曲id → 歌曲详情 评论歌单 → 歌单内歌曲id → 逐首补详情。这样你能清楚知道哪些数据是一次性拿到的哪些是必须二次请求的也方便预估总请求量。总请求量一旦超过几百次频率控制就不再是“可选项”而是“必选项”。2.2 抓取策略提前定请求频率、断点续抓与异常重试的默认值策略层面的三个默认参数我建议在写脚本前就定下来请求间隔、重试次数、断点记录方式。请求间隔方面不要用固定间隔要用带随机抖动的间隔。固定 0.5 秒一个请求很容易被识别成自动化流量固定 1 秒又太慢。常见做法是让间隔在 0.5 到 1.5 秒之间随机浮动既保证速度又让请求节奏看起来像人工操作。如果你抓的是评论这类大头数据间隔还要再放宽到 1 到 2 秒。异常重试的默认值建议是 3 次第一次失败后等 1 秒第二次失败后等 2 秒第三次失败后等 4 秒退避时间指数递增。超过 3 次就放弃当前请求把这条记录写到失败队列里而不是无限重试。无限重试是新手最容易犯的错它会让你在接口已经限流的情况下继续狂打最后从“限流”变成“封禁”。断点续抓是所有抓取脚本都必须有的能力。最简单的实现是维护一个“已抓取 id 集合”每成功抓完一条就写入本地文件。这样脚本中断后重启不用从头再跑只是把未完成的 id 继续抓完。不要小看这个设计网易云音乐的歌单详情和评论接口都只接受逐 id 请求一旦抓了几万条之后中断没有断点记录会让人非常崩溃。2.3 三种抓取路线选型接口直连、模拟客户端与页面解析围绕网易云音乐主流的抓取路线有三条。我先把它们的适用场景说清楚。接口直连是最推荐的方式。它直接请求网易云音乐的数据接口拿到的是结构化 JSON字段干净、解析成本低。这条路线要求你正确携带请求头和参数有一部分场景还需要处理加密参数。优点是效率高、数据完整、后续分析成本低。模拟客户端是第二条路线。它通过模拟 App 或客户端的请求签名方式拿到与客户端一样的数据。这个方案的成功率最高字段最完整但实现难度也最大因为它要处理签名、随机数、时间戳等一堆参数而且参数生成逻辑会随客户端版本变化。对多数分析类需求来说这条路线的投入产出比不划算。页面解析是最不建议的路线。虽然它看起来不需要“懂接口”直接抓 HTML 再解析 dom 就行但网页端的数据是经过 JS 动态渲染的HTML 里经常拿不到完整 JSON需要额外执行渲染脚本速度和稳定性都不如接口直连。页面一旦改版解析代码立刻失效维护成本极高。我给你的建议很简单优先走接口直连先跑通最小闭环遇到接口参数校验变严时再考虑模拟客户端。不要一上来就抓 HTML。3. 把 NetEaseMusicSpider 的最小骨架搭起来环境、目录与可跑脚本3.1 先搭干净环境虚拟环境、项目目录与依赖动手的起点是建一个不污染全局 Python 环境的虚拟目录。我用 Python 3 的 venv 模块依赖只需要 requests 这一个核心库解析 JSON 用标准库 json断点记录用标准库 os 和 time。mkdir NetEaseMusicSpider cd NetEaseMusicSpider python3 -m venv .venv source .venv/bin/activate pip install requests这里的目录名我直接沿用了标题里的叫法方便你理解。虚拟环境这一步的意义在于抓取脚本长期运行后往往要加 pandas、openpyxl、sqlite3 这类库如果全部装进全局环境不同项目之间的依赖版本会打架。先隔离环境后续不管加什么依赖都不影响其他脚本。参数说明python3 -m venv .venv表示在项目目录下创建虚拟环境.venv是环境目录名可以随意改但建议统一命名为 .venv 并写进.gitignore。source .venv/bin/activate是激活虚拟环境Windows 下对应的命令是.venv\Scripts\activate。requests 库是这个抓取器的唯一强依赖。3.2 统一请求入口会话管理、超时与重试写抓取脚本最忌讳每个函数各写一套 requests 调用后面想统一改请求头时会改到怀疑人生。我习惯先封装一个客户类把超时、重试、公共请求头收敛到一处。# netease_client.py import random import time import requests UA ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36 ) class NeteaseClient: def __init__(self, timeout10, max_retries3): self.timeout timeout self.max_retries max_retries self.session requests.Session() self.session.headers.update( { User-Agent: UA, Referer: https://music.163.com, Accept: application/json, text/plain, */*, } ) def _sleep_before_retry(self, attempt): time.sleep((2 ** attempt) random.uniform(0, 0.5)) def get_json(self, url, paramsNone): for attempt in range(self.max_retries): try: resp self.session.get( url, paramsparams, timeoutself.timeout ) if resp.status_code 200: return resp.json() if resp.status_code 403: time.sleep(10) except requests.RequestException: pass self._sleep_before_retry(attempt) return None逻辑说明这里用 Session 而不是直接 requests.get是因为 Session 会自动维持 Cookie连续请求时不用每次手动带上一次返回的 Cookie。这在网易云音乐场景里很重要很多接口对未带 Cookie 的请求会直接返回异常状态码。请求头里的 Referer 统一指向音乐首页能减少一部分基础反爬误伤。参数说明timeout10表示单次请求 10 秒没响应就超时不要设成 None否则脚本会卡死。max_retries3表示只重试 3 次超过就放弃。_sleep_before_retry采用指数退避加随机抖动第一次失败等约 1 秒第二次约 4 秒第三次约 8 秒避免重试节奏被识别。403 状态码单独处理先停 10 秒再进入退避循环这是网易云音乐限流的典型信号。3.3 搜索与歌单抓取的最小实现核心代码与字段映射基础请求封装好之后就可以写第一个可跑通的抓取函数了。先做搜索再做歌单详情这两个函数足够覆盖“找歌”和“按歌单批量抓取”两类常见需求。# core.py def search_song(client, keyword, limit20): url https://music.163.com/api/search/get params {s: keyword, type: 1, limit: limit, offset: 0} data client.get_json(url, params) if not data or result not in data: return [] songs data[result].get(songs, []) or [] return [ { song_id: item.get(id), name: item.get(name), artists: [a.get(name) for a in item.get(artists, [])], album: item.get(album, {}).get(name, ), duration_ms: item.get(duration), } for item in songs ] def playlist_detail(client, playlist_id): url fhttps://music.163.com/api/v6/playlist/detail params {id: playlist_id, n: 1000} data client.get_json(url, params) if not data or playlist not in data: return [] tracks data[playlist].get(tracks, []) or [] return [ { song_id: track.get(id), name: track.get(name), artists: [a.get(name) for a in track.get(ar, [])], album: track.get(al, {}).get(name, ), } for track in tracks ]逻辑说明搜索函数把用户输入的关键词拼进参数返回前几个匹配的歌曲。这里的字段映射我刻意做成了“目标字典”而不是直接返回原始 JSON因为原始 JSON 的字段名对后续分析不友好而且不同接口里“歌曲是否付费”这类字段名不一致先收敛到统一结构能省很多麻烦。参数说明limit20控制单次返回条数调试时用 5 就能验证链路。type1表示搜索类型为歌曲如果搜索歌单需要改成 1000这是网易云音乐搜索接口的类型约定。playlist_detail里的n1000表示歌单内最多取 1000 首注意这不是所有歌单都能取全部分歌单超过 1000 首时接口行为会不一样后面避坑章节再展开。关于接口地址需要说一句网易云音乐的接口体系分新旧多套/api/search/get属于历史接口字段语义简单非常适合验证链路。实际使用时如果你发现它返回空或者报错通常是因为请求需要带上更完整的 Cookie而不是你的代码有 bug。3.4 结果落地JSON 行存储与后续转表抓到的数据不能只活在内存里否则脚本一断前面全白干。我推荐用“JSON 行存储”作为落地格式也就是每一行是一条完整 JSON文件后缀用.jsonl。# storage.py import json import os class JsonlStorage: def __init__(self, path): self.path path if not os.path.exists(path): open(path, w, encodingutf-8).close() def append(self, record: dict): with open(self.path, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n)逻辑说明每抓到一个完整对象就追加一行即使后续脚本崩溃已经写入的行不会丢处理程序重启后只需要从文件末尾继续读。相比一次性把全部数据 dump 成一个大 JSON 文件JSON 行存储的优势体现在断点场景单行写入可随时中断而大 JSON 一旦中途写失败整个文件可能损坏。后续用 pandas 读取时也只要pd.read_json(path, linesTrue)。参数说明ensure_asciiFalse是中文最容易踩的坑不设这个参数中文会被转成\uXXXX转义序列肉眼没法排查问题后续导入数据库也不太方便。追加模式用a而不是w这是防止每次运行覆盖旧数据的关键很多初学者在这里翻车跑第二次发现文件里只剩新数据。4. 数据校验与增量抓取别让脏数据毁掉后续分析4.1 字段校验先行拿到 JSON 后先检查这六项抓取脚本跑起来之后第一个要验证的不是“拿到了多少条”而是“拿到的每条是不是我要的数据”。网易云音乐接口在不同时间段返回的字段完整性差异很大有的歌曲详情里没有专辑字段有的评论接口里没有点赞数字段如果不做校验直接落库后续分析时会出现大面积空值。# validate.py REQUIRED_FIELDS [song_id, name] def validate_song(record: dict) - bool: if not record: return False for field in REQUIRED_FIELDS: if record.get(field) is None: return False if record.get(song_id) in (None, 0, ): return False return True逻辑说明校验函数只做两件事检查必填字段是否存在检查歌曲 id 是否合法。把校验单独拆成函数而不是写在抓取函数里是因为后续接评论数据、歌单数据时同样需要复用这套检查逻辑。校验不通过的数据不要直接丢弃建议单独写进invalid.jsonl方便事后判断是接口变化还是字段映射写错。我的经验是必填字段越少越好。很多人会想把所有字段都校验一遍结果因为某个非关键字段在接口异常时缺失导致整条有效数据被丢弃。正确做法是只校验分析时无论如何都离不开的字段比如歌曲 id 和名称其余字段允许为空。4.2 增量与去重已抓 id 集合做断点续抓增量抓取的核心是“记住已经抓过什么”。最简单的方案是维护一个已抓 id 文件每成功处理一条就往里面追加一行。脚本启动时读入这个文件在发起请求前先判断 id 是否已存在。# dedup.py import os def load_done_ids(pathdone_ids.txt): done set() if not os.path.exists(path): return done with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if line: done.add(int(line)) return done def mark_done(path, song_id): with open(path, a, encodingutf-8) as f: f.write(str(song_id) \n)逻辑说明这里用 set 存储已抓 id判断复杂度是 O(1)几百万条数据也不会慢。文件追加写入和 set 配合起来就是最朴素的断点续抓方案。脚本中断后重新执行时先 load 一遍已经抓过的 id 全部跳过未抓的继续处理。这里有一个容易踩坑的点load_done_ids在初跑后会变得很大比如 10 万个 id 也就是 10 万行文本Python set 加载需要一点时间这是正常的。但如果你的抓取量级到千万级建议换 sqlite用表结构存储已抓 id不要再用纯文本否则加载时间会变成不可忽略的开销。常见做法是 100 万条以内用文本文件超过就上 sqlite。4.3 线程池提速有限并发下的安全写法抓取速度慢是另一个高频痛点。串行抓一万首个要两三个小时很多人第一反应就是上多线程。网易云音乐抓取用多线程不是不行但并发数要控制得很保守。# crawl.py from concurrent.futures import ThreadPoolExecutor, as_completed def batch_search(client, keywords, storage, max_workers4): with ThreadPoolExecutor(max_workersmax_workers) as pool: future_map { pool.submit(search_song, client, kw): kw for kw in keywords } for future in as_completed(future_map): keyword future_map[future] try: songs future.result() for song in songs: if validate_song(song): storage.append(song) mark_done(done_ids.txt, song[song_id]) except Exception as exc: print(fkeyword failed: {keyword}, err{exc})逻辑说明ThreadPoolExecutor是 Python 标准库自带的线程池不用额外装依赖。max_workers4是我在网易云音乐场景下的安全上限不建议超过 6。原因在于网易云音乐的限流机制是全局性的不是你换一个用户代理就能绕过。单线程跑 1 秒一个请求已经很克制4 个线程同时跑仍然可能触发验证码所以要给每个线程内部再设随机间隔。参数说明max_workers4表示同时最多 4 个请求在飞。as_completed是遍历结果的关键它会在某个线程完成时立即返回配合 try-except 可以避免某一个请求失败导致整个循环终止。注意这里每个子任务都用同一个 client 实例因为 Session 内部有自己的连接池同一个 Session 的并发请求不会重复建立 TCP 连接能明显降低资源占用。5. 网易云音乐抓取避坑请求失败、数据错乱与限流的判断标准5.1 频繁触发验证码现象脚本稳定跑几分钟后返回结果不再是 JSON而是一段包含验证码页面的 HTML或者直接返回 403。原因请求频率超过了服务端的容忍阈值。绝大多数情况下不是因为单个请求有问题而是因为单位时间内请求总次数太多触发了基于 IP 的访问频率统计。解决先把并发数降回 1把请求间隔调整到 1.5 到 2 秒跑 30 分钟观察是否还触发。然后把验证码响应单独做异常判断一旦检测到页面类响应立即停止抓取等待 3 到 5 分钟再继续而不是继续硬闯。最后建议每抓完一个接口模块就换一批 Cookie 或重新登录一下让会话状态保持新鲜。5.2 同一个歌曲 id 对不上现象搜索时拿到的 song_id在请求歌曲详情时返回的不是同一首歌或者歌单详情里的歌曲列表和实际歌单内容不一致。原因新旧接口对歌曲对象的唯一标识定义不一致。历史接口里id是歌曲的纯数字 id而新接口在部分场景下会返回带前缀的混合字符串或者id字段对应的是专辑 id 而不是歌曲 id。还有一种情况是歌单详情接口做了截断超过 1000 首的歌单只返回前 1000 首。解决在字段映射时明确锁定“歌曲 id”的字段来源不要把id、songId、trackId混用。写一个审计函数随机抽样 20 首歌用歌曲名称加歌手名去搜索接口反查 id看结果是否一致。歌单超过 1000 首的场景改为先拿歌单内歌曲 id 列表接口再逐首补详情不要依赖单次大返回。5.3 JSON 字段大面积缺失现象接口返回 200但 JSON 里album、duration、artists经常为空或者某些歌曲的字段结构和第一条数据不一样。原因网易云音乐不同类型的数据命中不同的返回模板。比如无版权歌曲只返回基础字段付费歌曲会多出privilege字段纯音频类内容可能没有专辑信息。如果代码里用item[album][name]这种硬下标方式取值遇到缺失字段就直接抛异常。解决把所有字段取值统一改成.get()加默认值嵌套字段也要逐层 get。不要偷懒。然后在字段校验里按业务要求区分“核心字段”和“扩展字段”核心字段缺失记为失败扩展字段缺失只记为警告。最后建议在落库前做一个字段覆盖率统计看每个字段的非空比例及时发现接口变化。5.4 换网络环境后请求超时或全部 403现象本地跑得好好的脚本部署到云服务器或者公司内网后第一个请求就超时或者连续返回 403。原因大多数情况下不是代码问题而是请求出口 IP 被服务端标记为高风险。数据中心 IP 段或共享出口 IP 的请求信任度天然比家庭宽带低服务端会在同一套频率规则上追加更严格的拦截。解决部署到服务器后先把请求间隔放大一倍从 1 秒调到 2 秒观察稳定后再逐步试探性缩短。其次确认服务器本地时间与网络时间同步时间偏差超过 30 秒会导致带时间签名的请求全部校验失败给我的经验是网易云音乐对时间戳较敏感这是踩坑里最容易忽略的一条。最后建议在服务器上先用 50 条数据做小规模验证不要直接上全量任务避免触发封禁后整个出口 IP 被拉黑。5.5 抓取中断后重复数据堆叠现象脚本因为网络闪断或手动终止后重启同一批数据被重复写入最终统计量虚高。原因断点记录写入的顺序不对。很多脚本是“先抓数据后标记 done”在数据写入了但还没标记完成的间隙脚本崩溃重启后这条数据被重复抓取。另一类原因是 mark_done 使用追加模式写文件但脚本每次都全量加载没有把全量加载后仍然重复追加的问题暴露出来。解决先标记 done 再写业务数据或者用事务式写法先标记、再落库、最后确认成功回滚标记。更稳妥的方案是在落库前用内存里的已抓 set 做实时判断命中重复就直接跳过。数据入库后还要跑一个分组统计按 song_id 查重复条数让重复问题在源头和事后都能看得到。6. 进阶技巧把抓到的数据用起来之前先做这三件事6.1 先做字段标准化时长、歌手、版权字段统一口径抓回来一堆 JSON 不等于能直接做分析因为网易云音乐的字段命名和单位并不是分析场景原生友好的。我会用一个小脚本统一口径时长从毫秒转成秒或者“分:秒”歌手从数组拼成逗号分隔字符串版权字段映射成 0/1 两个取值专辑信息里缺失的用“未知专辑”填充。这一步是分析质量的分水岭不做标准化后面画图、建模都会在处理脏数据上浪费大量时间。# normalize.py def normalize_song(record): duration_ms record.get(duration_ms) or 0 seconds duration_ms // 1000 artists record.get(artists) or [] return { song_id: record[song_id], name: record.get(name, ), artist: / .join(artists), album: record.get(album) or 未知专辑, duration_sec: seconds, fee: 1 if record.get(fee) else 0, }这段代码很简单但它解决的问题很真实不统一口径后续 join 两张表时要么类型不匹配要么字段名冲突要么单位对不上。我希望你把这个标准化逻辑放在抓取完成后的第一个处理步骤而不是最后可视化之前。顺序反了坑就多了。6.2 用歌单做交叉关联补全缺失字段并估算覆盖率第二件事是拿歌单数据交叉补全。很多歌曲在搜索结果里只带基础字段但它在某个歌单里出现时接口会多返回一些补充信息比如歌曲别名、关联艺人、收录时间。常见做法是把歌单歌曲表和搜索歌曲表按 song_id 合并用歌单表的字段填补搜索表的空值然后统计每个字段的填充率。如果填充率低于九成说明你的数据链路还有接口没覆盖到需要回去补抓。6.3 建立小规模抽检习惯把抓取器变成可再生工具最后一件事也是我自己吃过亏才养成的习惯每次调整代码后先抽 50 条数据做全链路验证确认字段完整性和 id 正确性再跑全量任务。全量任务跑完后定时抽检已落库数据的条数和关键字段非空率一旦发现异常立刻停掉增量任务排查接口变化。这样你的抓取器就不再是一次性脚本而是一个可持续运行的数据管线。说句心里话网易云音乐数据抓取这个方向真正难的不是写请求代码而是建立一种“先想清楚边界再动手”的工程习惯。我第一次做这个项目时就是没做断点续抓跑了两天发现断在三分之一处只能从头重跑那种感觉至今记得。后来把所有环节都补上标准化、校验、去重之后脚本跑一周都没有一次人工干预。希望这些踩坑换来的经验能帮你少走一段弯路。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?