news 2026/10/17 5:47:40

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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两个参数配合。比如limit=50,第一页offset=0,第二页offset=50,以此类推。但要注意:有些接口的offset有上限,超过某个值就返回空,这时候得换用其他分页方式(比如基于上一页最后一条的ID做游标)。

限速是必须的。我的做法是每次请求之间sleep一个随机时间,范围1到3秒。为什么用随机而不是固定?固定间隔的请求模式太规律,容易被识别。随机间隔更接近真人操作。另外,遇到返回429(请求过多)就指数退避,等2秒、4秒、8秒再重试。

4. 完整实操流程与关键环节

4.1 环境准备与依赖安装

我用的是Python,依赖就三个:

pip install requests pip install beautifulsoup4 pip install lxml

requests负责发请求,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, headers=HEADERS, params=params, timeout=10) resp.raise_for_status() data = resp.json() return { "name": data["result"]["name"], "creator": data["result"]["creator"]["nickname"], "track_count": data["result"]["trackCount"] }

注意timeout=10一定要加,不然网络卡住程序就挂那了。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, headers=HEADERS, params=params, timeout=10) 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 第三步:数据清洗与入库

接口返回的原始数据字段很多,我只保留需要的,然后写进SQLite:

import 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/429)

403通常是请求头问题,重点检查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空壳数据是异步加载的改抓接口,别解析HTML
403 Forbidden请求头缺失或被识别补全User-Agent、Referer
429 Too Many Requests请求太快加大随机间隔,指数退避
字段缺失接口改版重新分析接口,更新字段映射
中文乱码编码判断错误手动设utf-8或用json()
数据重复没做去重主键+INSERT OR IGNORE
分页拿不全offset有上限改用游标分页

5.5 几个我踩过的坑

第一个坑:以为接口参数是固定的。有次我硬编码了一个版本号参数,结果平台更新后这个参数变了,接口直接返回错误。后来我改成从页面里动态提取这个参数,就再没出过问题。教训是:能动态获取的参数就别硬编码。

第二个坑:忽略了数据的时间戳。我早期采集没记录采集时间,后来想分析数据变化时发现无从下手。加上collected_at字段后,就能做增量对比了。这个字段成本极低,价值极高。

第三个坑:没有做异常重试。网络抖动是常态,一次失败就放弃太可惜。我给每个请求包了一层重试逻辑,失败重试3次,每次间隔递增。加上之后采集成功率从80%提到了接近100%。

6. 关于合规与边界的个人体会

做这类项目,技术只是一半,另一半是边界感。我给自己定了三条规矩:只采公开数据、控制频率不影响对方服务、数据只用于个人学习分析不对外分发。这三条不是束缚,反而是保护——它让你的项目能长期稳定地跑下去,而不是做一次就被封。

另外,我建议把采集逻辑和数据分析逻辑分开。采集模块只负责拿数据存库,分析模块从库里读数据做统计。这样即使采集部分因为平台变化需要调整,也不会影响你已经积累的数据和分析代码。这个解耦设计在我后续好几个项目里都复用了,非常值。

如果你也想动手做类似的东西,我的建议是:先拿一个小歌单练手,把整条链路跑通,再考虑扩大规模。别一上来就想着抓几万首,先把“请求→解析→入库→验证”这个闭环走顺,后面就是复制粘贴的事。真正花时间的从来不是写代码,而是理解数据是怎么流动的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 5:47:25

音频事件检测全链路实战:从特征工程到工程部署

你有没有过这样的时刻&#xff1a;在嘈杂的车间里突然分辨出某台设备的“不对劲”声音&#xff0c;在安静的家中听到一声清脆的玻璃破碎声&#xff0c;或者在走廊尽头听到一阵异常的急促脚步&#xff1f;声音里的信息远比我们想象中丰富&#xff0c;而音频事件检测就是让机器学…

作者头像 李华
网站建设 2026/10/17 5:46:38

AI辅助编程新手避坑:从应援APP开发总结的实战经验

看到这个标题&#xff0c;你可能有点意外&#xff1a;追星和AI开发&#xff0c;怎么凑到一起了&#xff1f;但恰恰是这种看起来“业余”的项目&#xff0c;最容易让新手把AI辅助开发这条路走明白。我最早接触AI写代码&#xff0c;不是在公司项目里&#xff0c;而是因为想给喜欢…

作者头像 李华
网站建设 2026/10/17 5:46:09

流程可视化追踪:BPMN高亮映射与引擎数据组装实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 5:46:03

模板系统决定Claude Code开发效率:claude-code-templates实战指南

接手过不少用 Claude Code 做日常开发的项目&#xff0c;也帮团队搭过好几套claude-code-templates的模板库。说实话&#xff0c;Claude Code 这东西&#xff0c;入门门槛不高&#xff0c;但真正决定它能不能“用得顺手”的&#xff0c;往往不是模型本身&#xff0c;而是你喂给…

作者头像 李华
网站建设 2026/10/17 5:44:56

运算符与表达式练习:从陷阱到综合刷题指南

写运算符和表达式的练习&#xff0c;这事听起来特别基础&#xff0c;但说句实话&#xff0c;我带过不少初学者&#xff0c;也看过很多工作两三年的开发者在代码评审里翻车&#xff0c;问题往往就出在这块。不管是C、C、Java还是Python&#xff0c;运算符和表达式永远是语言的第…

作者头像 李华