Python爬虫实战:网易云音乐数据采集与结构化存储

发布时间:2026/10/17 5:48:07
Python爬虫实战:网易云音乐数据采集与结构化存储
1. 这个项目到底在做什么先把话说在前头标题写的是“爬取网易云音乐源码”但真正做过这类项目的人都知道我们爬的从来不是“源码”而是页面结构、接口返回的数据、以及前端渲染所依赖的那套资源组织方式。源码是人家服务器上的东西你拿不到也不该拿你能拿到的是浏览器或客户端请求出去、又返回来的那些东西。这个区别必须先掰扯清楚不然方向一开始就歪了。我做的是一个音乐平台公开页面的数据采集与结构分析项目目标很明确把某个歌单页面、某位歌手的作品列表、某首歌的基础信息歌名、歌手、专辑、时长、封面地址稳定地抓下来存成结构化数据顺便把前端是怎么把这些数据拼成页面的逻辑摸一遍。它解决的问题是当你需要批量整理自己的歌单、做音乐数据统计、或者研究前端渲染方案时手动一条条复制粘贴效率太低而且容易出错。适合谁来参考有一定编程基础、懂点网络请求、想入门数据采集的开发者也适合做前端、想搞清楚“页面数据到底从哪来”的同学。完全零基础也能看我会把每一步为什么这么做讲透。但我要提前说一句任何采集行为都要遵守目标平台的服务条款和robots协议控制频率只取公开数据不碰用户隐私不做商业分发。这是底线不是客套话。下面我按“整体设计思路 → 核心细节 → 实操落地 → 问题排查”这条线把整个项目从头到尾拆一遍。中间会穿插我自己踩过的坑和实测有效的参数能直接抄作业的地方我会标出来。2. 整体设计与技术选型思路2.1 为什么先分析再动手而不是上来就写请求新手最容易犯的错就是打开编辑器直接requests.get(url)然后发现返回的HTML里啥都没有或者拿到一堆乱码。我早期也这么干过浪费了大半天。后来我养成了一个习惯任何采集项目先花20分钟做“侦察”再花2小时写代码。侦察做得好后面写代码就是体力活侦察没做后面全是玄学调试。侦察要搞清楚三件事数据是通过什么方式加载的服务端直出还是前端异步请求、请求带了哪些关键参数、返回的数据是什么格式。音乐平台的页面大多是前端异步渲染的也就是说你右键查看网页源代码看到的往往是一个空壳真正的数据是页面加载后由JavaScript再发一次请求拿回来的。这就决定了你不能只抓HTML得去抓那个“第二次请求”。2.2 采集方案的三条路线对比我把常见的三条路线列出来你可以根据自己的需求和基础选方案原理优点缺点适用场景直接解析HTML请求页面后解析DOM实现简单异步渲染页面拿不到数据服务端直出的老式页面抓接口返回找到前端调用的数据接口直接请求数据干净、结构清晰、速度快需要分析请求参数和加密逻辑绝大多数现代前端页面浏览器自动化用无头浏览器模拟真实操作能拿到最终渲染结果慢、资源占用高、易被识别接口加密复杂或必须交互时这个项目我选的是第二条路线抓接口返回。原因很直接——接口返回的是JSON字段清晰不用去解析一堆嵌套的HTML标签维护成本低。浏览器自动化我留作备选只在接口实在搞不定的时候才上。这个取舍逻辑很重要能用接口就别用浏览器自动化后者是杀鸡用牛刀还容易把简单问题复杂化。2.3 数据存储与去重设计抓下来的数据往哪放我一开始用CSV后来数据量上来了换成SQLite。理由CSV适合一次性导出但做去重、增量更新很麻烦SQLite单文件、零配置、支持SQL对个人项目来说刚刚好。表结构我设计得很简单CREATE TABLE songs ( song_id TEXT PRIMARY KEY, name TEXT NOT NULL, artist TEXT, album TEXT, duration INTEGER, cover_url TEXT, collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );song_id做主键天然去重。每次采集用INSERT OR IGNORE重复的自动跳过不用自己写比对逻辑。这个设计省了我大量时间强烈推荐。3. 核心细节解析与实操要点3.1 怎么找到真正的数据接口打开目标页面按F12进开发者工具切到Network面板筛选Fetch/XHR然后刷新页面。这时候你会看到一串请求重点看返回类型是JSON、体积比较大、名字里带list或detail的那种。点进去看Response如果里面有你想要的歌名、歌手恭喜找到了。这里有个细节请求列表可能很多怎么快速定位我的经验是按响应大小排序数据接口通常返回体最大。另外看请求的Initiator列能追溯到是哪个JS文件发起的顺着这个线索能理解整个数据流。注意不同页面歌单页、歌手页、搜索页调用的接口不一样参数结构也不同。别指望一个接口打天下要针对每种页面分别分析。3.2 请求参数里的门道找到接口后别急着复制URL就请求。仔细看它的请求参数通常分三类业务参数比如id歌单ID、limit每页数量、offset偏移量。这些是你能控制的也是分页采集的关键。固定参数比如一些版本号、客户端类型标识。这些一般照抄就行但要注意它们可能随平台更新而变化。签名/加密参数这是最麻烦的。有些接口会带一个根据其他参数计算出来的签名你直接改参数请求就会失败。关于第三类我的处理原则是如果签名逻辑能通过分析前端JS搞清楚就本地复现如果太复杂就退而求其次用浏览器自动化。我不建议去硬啃混淆过的加密代码投入产出比太低。这个判断很重要很多新手卡在加密上出不来其实换个思路就绕过去了。3.3 请求头里必须带的字段接口能通请求头占一半功劳。以下是我实测必须关注的几个User-Agent伪装成正常浏览器别用默认的python-requests。Referer很多接口会校验来源页不带就返回403。Cookie部分接口需要登录态公开数据一般不需要但带上更稳。Accept告诉服务器你要JSON写application/json。我一般会从浏览器里把整个请求头复制出来然后精简掉无关的逐个测试哪些是必需的。这个过程叫“最小化请求头”能减少被识别为异常请求的概率。3.4 分页与限速策略分页靠offset和limit两个参数配合。比如limit50第一页offset0第二页offset50以此类推。但要注意有些接口的offset有上限超过某个值就返回空这时候得换用其他分页方式比如基于上一页最后一条的ID做游标。限速是必须的。我的做法是每次请求之间sleep一个随机时间范围1到3秒。为什么用随机而不是固定固定间隔的请求模式太规律容易被识别。随机间隔更接近真人操作。另外遇到返回429请求过多就指数退避等2秒、4秒、8秒再重试。4. 完整实操流程与关键环节4.1 环境准备与依赖安装我用的是Python依赖就三个pip install requests pip install beautifulsoup4 pip install lxmlrequests负责发请求beautifulsoup4和lxml负责解析HTML虽然主走接口但偶尔要解析页面拿一些接口里没有的信息。不需要装selenium除非你走浏览器自动化路线。4.2 第一步抓取歌单基础信息先写一个函数根据歌单ID拿到歌单的元信息名称、创建者、歌曲总数import requests import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/, Accept: application/json } def fetch_playlist_meta(playlist_id): url https://example.com/api/playlist/detail params {id: playlist_id} resp requests.get(url, headersHEADERS, paramsparams, timeout10) resp.raise_for_status() data resp.json() return { name: data[result][name], creator: data[result][creator][nickname], track_count: data[result][trackCount] }注意timeout10一定要加不然网络卡住程序就挂那了。raise_for_status()让非200的状态码直接抛异常方便定位问题。4.3 第二步分页抓取歌曲列表这是核心环节。根据总歌曲数算出需要请求几页循环抓取def fetch_all_tracks(playlist_id, total): all_tracks [] limit 50 offset 0 while offset total: url https://example.com/api/playlist/track/all params {id: playlist_id, limit: limit, offset: offset} resp requests.get(url, headersHEADERS, paramsparams, timeout10) if resp.status_code 429: wait 2 ** (offset // limit % 4) time.sleep(wait) continue data resp.json() songs data.get(songs, []) if not songs: break all_tracks.extend(songs) offset limit time.sleep(random.uniform(1, 3)) return all_tracks这里的2 ** (offset // limit % 4)是简单的指数退避最多等8秒。random.uniform(1, 3)是随机限速。实测下来这个节奏跑几百首歌不会被拦。4.4 第三步数据清洗与入库接口返回的原始数据字段很多我只保留需要的然后写进SQLiteimport sqlite3 def save_tracks(tracks): conn sqlite3.connect(music.db) cursor conn.cursor() for t in tracks: cursor.execute( INSERT OR IGNORE INTO songs (song_id, name, artist, album, duration, cover_url) VALUES (?, ?, ?, ?, ?, ?), ( str(t[id]), t[name], , .join(a[name] for a in t[ar]), t[al][name], t[dt], t[al].get(picUrl, ) ) ) conn.commit() conn.close(), .join(...)处理多歌手的情况把列表拼成字符串。INSERT OR IGNORE配合主键实现自动去重。这套组合拳下来重复跑也不会产生脏数据。4.5 第四步验证采集结果采完别急着庆祝先验证。我一般查三个东西总数对不对、有没有空字段、有没有重复。用SQL一查便知SELECT COUNT(*) FROM songs; SELECT COUNT(*) FROM songs WHERE name IS NULL OR name ; SELECT song_id, COUNT(*) FROM songs GROUP BY song_id HAVING COUNT(*) 1;第一条对总数第二条查空值第三条查重复。三条都正常这次采集才算成功。这个验证习惯帮我提前发现过好几次接口字段变更的问题。5. 常见问题与排查技巧实录5.1 返回数据为空或字段缺失这是最常见的问题。排查顺序先看状态码是不是200再看返回体是不是JSON最后看JSON里有没有你预期的字段。如果状态码200但数据为空大概率是参数不对或请求头缺失。我遇到过一次是因为Referer写错了域名改对之后立刻正常。还有一种情况是接口返回的字段名变了。平台改版时经常干这事比如把songs改成tracks。这时候你的代码不会报错但会静默返回空列表。所以每次采集后都要验证数据量别等入库了才发现全是空的。5.2 请求被拒绝403/429403通常是请求头问题重点检查User-Agent和Referer。429是频率问题加大sleep间隔或者换IP但个人项目一般不需要降速就够了。我实测把间隔从1秒提到2到3秒随机429基本消失。提示不要用多线程猛冲。个人项目单线程加限速完全够用多线程只会让你更快被封。速度不是这个项目的目标稳定才是。5.3 编码乱码问题有时候返回的中文是乱码这是因为resp.text的编码判断错了。解决办法是手动指定resp.encoding utf-8或者直接用resp.json()它内部会正确处理。我一般优先用resp.json()省心。5.4 常见问题速查表现象可能原因解决方向返回HTML空壳数据是异步加载的改抓接口别解析HTML403 Forbidden请求头缺失或被识别补全User-Agent、Referer429 Too Many Requests请求太快加大随机间隔指数退避字段缺失接口改版重新分析接口更新字段映射中文乱码编码判断错误手动设utf-8或用json()数据重复没做去重主键INSERT OR IGNORE分页拿不全offset有上限改用游标分页5.5 几个我踩过的坑第一个坑以为接口参数是固定的。有次我硬编码了一个版本号参数结果平台更新后这个参数变了接口直接返回错误。后来我改成从页面里动态提取这个参数就再没出过问题。教训是能动态获取的参数就别硬编码。第二个坑忽略了数据的时间戳。我早期采集没记录采集时间后来想分析数据变化时发现无从下手。加上collected_at字段后就能做增量对比了。这个字段成本极低价值极高。第三个坑没有做异常重试。网络抖动是常态一次失败就放弃太可惜。我给每个请求包了一层重试逻辑失败重试3次每次间隔递增。加上之后采集成功率从80%提到了接近100%。6. 关于合规与边界的个人体会做这类项目技术只是一半另一半是边界感。我给自己定了三条规矩只采公开数据、控制频率不影响对方服务、数据只用于个人学习分析不对外分发。这三条不是束缚反而是保护——它让你的项目能长期稳定地跑下去而不是做一次就被封。另外我建议把采集逻辑和数据分析逻辑分开。采集模块只负责拿数据存库分析模块从库里读数据做统计。这样即使采集部分因为平台变化需要调整也不会影响你已经积累的数据和分析代码。这个解耦设计在我后续好几个项目里都复用了非常值。如果你也想动手做类似的东西我的建议是先拿一个小歌单练手把整条链路跑通再考虑扩大规模。别一上来就想着抓几万首先把“请求→解析→入库→验证”这个闭环走顺后面就是复制粘贴的事。真正花时间的从来不是写代码而是理解数据是怎么流动的。