news 2026/10/21 23:17:16

基于Python的网易云排行榜数据分析系统:从数据采集到可视化全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python的网易云排行榜数据分析系统:从数据采集到可视化全流程

做数据分析这些年,我一直有个观点:管你模型调得再好、图表画得再漂亮,数据源头一旦不干净,后面全是白费。这套基于Python的网易云排行榜数据分析系统,就是围绕“数据从哪来、怎么存、怎么洗、怎么用”这条主线做的。榜单数据量不大,但结构半标准化、带时间演化、有歌手和歌曲维度,拿来练手做分析系统刚刚好。这篇文章把完整的思路、采数、清洗、分析、可视化、工程组织都过一遍,适合正在学数据分析、想从“对着现成CSV跑几个函数”往上走一步的朋友参考。

1. 从榜单页面到分析结论:先理清这套系统到底要做什么

1.1 为什么拿排行榜作为数据分析切入点

排行榜数据有两个天然优势。第一是公开性,榜单页面正常访问就能看到,接口数据结构相对规整,做采集不需要折腾复杂加密协议;第二是时间维度,榜单每天更新,同一批歌曲的排名、热度会持续变化,天然就是一份带时间序列属性的数据。

很多朋友一上来就奔着机器学习、预测模型去,但我更建议先做描述性分析。排行榜能回答的问题其实很接地气:哪首歌最火、火到什么程度、火多久、哪个歌手霸榜能力强、新歌从上榜到登顶一般要多久。这些问题不需要复杂模型,Excel也能算,但要长期跟踪、自动更新、随时出图,就得靠一套系统把采集和分析串起来。

1.2 系统的功能边界和模块划分

我做的这个系统,功能边界很明确:不做推荐、不做用户画像,先聚焦在“榜单数据本身的分析价值”上。核心模块一共四块:

  • 采集端:按固定频率抓取多个榜单的歌曲快照数据;
  • 存储端:把每日快照存入数据库,保留历史记录;
  • 分析端:清洗数据、加工特征、计算榜单统计指标;
  • 展示端:生成可交互图表,输出分析结论。

这个划分不是拍脑袋,而是考虑到后续扩展。比如采集端抽象出“榜单数据源”接口,以后想加个新榜单,只要继承基类补上解析逻辑。分析端做的所有指标都基于统一的宽表,图表和报表都能复用。系统跑起来之后,每天只需执行一条命令,就能把当天数据采下来、算出指标、生成图表,不用人肉打开页面复制粘贴。

2. 采集层是地基:接口规律观察、请求模拟与数据落库

2.1 榜单接口的观测方法

采集层的核心是找到数据的真实来源。打开网易云音乐网页版,进入排行榜页面,按F12打开开发者工具,切到Network面板,刷新页面,能看到一系列请求。其中返回JSON、内容包含歌曲信息的那个请求,就是我们要用的接口。

请求里一般带几个关键参数:

参数作用示例值
id榜单ID,不同榜单对应不同值热歌榜3778678、新歌榜3779629
limit每页返回数量20
offset分页偏移量0、20、40……

我当时是把三个榜单的ID都抽出来做成配置文件,这样切换榜单不用改代码。接口返回的歌曲信息字段比较完整,除了歌名、歌手、专辑外,还有排名、播放量、评论数等,后面清洗和分析都用得上。

注意,这里讲的是观察公开页面请求、模拟浏览器请求获取公开数据,属于常规开发手段。实际落地时一定要控制合理频率,做学习研究用途,不要对服务造成压力,也要遵守平台的服务条款。

2.2 请求模拟的关键参数与频率控制

接口规律摸清之后,写请求代码就很快了。关键是要把请求头伪装成真实浏览器的样子,否则服务器可能返回异常。

import requests import time import random headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0 Safari/537.36", "Referer": "https://music.163.com/", "Accept": "application/json, text/plain, */*", } def fetch_track_list(playlist_id, total=100): items = [] for offset in range(0, total, 20): url = "https://music.163.com/api/v6/playlist/detail" params = {"id": playlist_id, "offset": offset, "limit": 20} resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() tracks = data.get("playlist", {}).get("tracks", []) if not tracks: break for rank, t in enumerate(tracks, start=offset + 1): items.append({ "rank": rank, "song_id": t.get("id"), "song_name": t.get("name"), "artist": "/".join(a["name"] for a in t.get("ar", [])), "album": t.get("al", {}).get("name"), "play_count": t.get("playCount"), }) # 控制请求间隔,避免对服务器造成压力 time.sleep(random.uniform(1.0, 2.5)) return items

几个细节说一下。第一,返回字段里歌手是一个数组,一首歌可能有多位歌手合作,我用斜杠拼接,后面做歌手分析时再拆开。第二,playCount字段不是所有榜单接口都返回,缺失时我留着None,交给清洗层处理。第三,time.sleep的随机间隔很重要,固定间隔容易被识别到规律,随机间隔更稳妥。

2.3 表结构设计与增量策略

采集到的数据要落库。我用SQLite起步,结构简单、零配置,数据量在万级以下完全够用。核心表是榜单每日快照表:

字段名类型说明
idINTEGER PRIMARY KEY自增主键
list_typeTEXT所属榜单,如hot/new/soar
song_idINTEGER歌曲ID
song_nameTEXT歌名
artistTEXT歌手
albumTEXT专辑
rankINTEGER当日排名
play_countINTEGER播放量,可能为空
track_dateTEXT榜单日期,格式YYYY-MM-DD

索引上,我建了一个联合唯一索引(list_type, song_id, track_date),保证同一首歌在同一天同一个榜单只记录一次。用INSERT OR REPLACE写入,天然支持重复执行不产生脏数据。

增量策略很简单粗暴:每天存一份全量快照。榜单数据本身不大,三个榜单各100首,一天不到300条,一年也就10万条左右。保存全量快照的好处是分析时能随时回溯任意一天的数据,不用依赖“增量变更表”去倒推当天状态。真正做增量合并反而容易漏数据。

3. 让数据“变干净”:清洗规则与特征加工的落地实践

3.1 脏数据来源和清洗策略

数据采下来不等于能用。我跑了三天之后就发现,原始数据里有几类典型问题:

  • 歌名不统一:有的歌名带“(Live)”“(Remix)”“(伴奏)”,同一首歌的现场版和录音室版要区分还是要合并,取决于分析口径;
  • 歌手分隔不一致:有的字段用“/”分隔多个歌手,有的用“、”,还有的带“feat.”,需要统一格式;
  • 字段缺失:play_count为空,album为空;
  • 重复记录:同一首歌在热歌榜和飙升榜同时出现,或者不同日期同一首歌的写法有细微差异。

我定了几条清洗规则,基本能覆盖以上情况:

def clean_track(row): name = str(row["song_name"] or "").strip() # 去掉版本标记,统一为原曲 name = re.sub(r"[((].*?(Live|Remix|伴奏|钢琴|Acoustic).*?[))]", "", name, flags=re.I) # 歌手字段统一用斜杠分隔 artist = str(row["artist"] or "").replace("、", "/").replace("&", "/") artist = re.sub(r"(?i)feat\..*?$", "", artist).strip("/ ") # 播放量空值用0填充,保留原始标记后续可选过滤 play_count = row["play_count"] or 0 return (name.strip(), artist.strip(), play_count)

这里有个判断给读者参考:是否要把现场版和原版合并,取决于分析目的。做“歌手热度”时我会合并,因为听众记住的是歌名;做“歌曲生命周期”时会保留版本差异,因为现场版上榜单很少,混进来反而干扰曲线。这个规则每次跑分析前在配置里切换,而不是写死在代码里。

3.2 特征工程:热度分级、歌手聚合、榜单演化字段

清洗完之后,还要加工一批衍生特征,不然分析时每次都要写重复逻辑。我做三个层面的特征:

  • 歌曲层面:播放量分桶,分成 0-1w、1w-10w、10w-100w、100w-1000w、1000w+ 五档;
  • 歌手层面:把一个歌手的多首上榜歌曲聚合成一行,统计上榜歌曲数、最高排名、平均排名;
  • 日期层面:计算出每首歌相对于前一天的排名变化值,正值代表上升,负值代表下降。

特征计算最好放在存储层和分析层之间,输出一张标准特征宽表。这样后面画任何图都用同一套数据,不会出现“这次用的清洗规则和上次不一样”的情况。

df["play_bucket"] = pd.cut( df["play_count"], bins=[0, 1e4, 1e5, 1e6, 1e7, float("inf")], labels=["0-1w", "1w-10w", "10w-100w", "100w-1000w", "1000w+"] ) df["rank_change"] = df.groupby(["list_type", "song_id"])["rank"].diff(-1) * -1

3.3 多日数据合并的时间序列结构

数据跑多天之后,分析要面对的核心结构是“歌曲 × 日期”的演化矩阵。我做了两种形态:

长表:一行为“某首歌在某天的排名”,适合统计和筛选;宽表:一行为“某首歌”,列是每天的排名,适合画折线图看趋势。

pivot = df.pivot_table( index="song_id", columns="track_date", values="rank", aggfunc="first" )

这一步的坑在于:某首歌在某一天可能不在榜单上,透视后会出现NaN。这个NaN有业务含义,代表歌曲掉榜了,分析时不能直接用fillna(0)糊弄过去,比如算霸榜天数时NaN和排名第100名语义完全不同。我的做法是单独生成一个on_list_days字段,记录歌曲出现在榜单的天数,这样后续统计口径就清楚了。

4. 分析模块怎么落地:从播放量分布到榜单演化

4.1 播放量长尾分布:头部集中效应

第一个分析点是播放量分布。音乐平台的播放量几乎必然符合长尾分布,头部的十几首歌占据绝大部分播放量。用pandas算起来很简单:

top1 = df[df["list_type"] == "hot"].nlargest(1, "play_count") play_sum = df["play_count"].sum() top10_sum = df.nlargest(10, "play_count")["play_count"].sum() print("Top10播放量占比: {:.2f}%".format(top10_sum / play_sum * 100))

我第一次跑出Top10占比高达70%多,比预想的还要夸张。这个数字说明榜单内部依然存在明显的头部效应,排在末尾的歌虽然上榜,但实际热度和头部差距极大。这个现象如果只看排名是看不出来的,排名只告诉你顺序,播放量才告诉你差距有多大。

4.2 歌手霸榜能力:谁在持续输出

第二个分析是歌手维度。把歌手字段拆开之后,统计每个歌手在观察周期内的上榜歌曲数、总上榜天数、最高排名、平均排名。

def expand_artist(df): return df.assign(artist=df["artist"].str.split("/")).explode("artist") artist_df = expand_artist(df) summary = artist_df.groupby("artist").agg( song_count=("song_id", "nunique"), max_rank=("rank", "min"), avg_rank=("rank", "mean"), total_days=("track_date", "nunique") ).sort_values("song_count", ascending=False)

值得留意的是,“上榜歌曲数多”和“平均排名高”是两种不同能力。有的歌手靠一两首爆款稳居前五,有的歌手发一张专辑七八首歌全部进榜但排名分散。我的图表里放了两张图:一张看歌曲数,一张看平均排名,两张图对照才不至于误判。

4.3 新歌上榜周期:从首现到峰值要多久

第三块分析我比较感兴趣:新歌上榜后的排名演化。取出每首歌首次出现在榜单的日期,再看它后续几天的排名变化,计算从首现到峰值(最高排名)经过的天数。

这个指标的洞察是:榜单不是一个纯靠播放量排名的机制,新歌往往有推新期,排名会在短时间内迅速拉升,随后回落。我用一个多小时跑出了几十首歌的演化曲线,整体规律是:大部分新歌在首现后2到4天内达到峰值,少数老歌靠长尾播放慢慢爬升。这种现象用折线图叠加展示非常直观。

4.4 可视化选型:pyecharts 还是 matplotlib

可视化我两个库都试过,简单对比一下:

需求pyechartsmatplotlib
交互式图表(鼠标悬停、缩放)强弱
长线趋势图一般强
与pandas直接结合需要转换原生支持好
部署到HTML报告方便需要另存图片

我最终的取舍是:日常探索分析用matplotlib,出图快、改起来顺手;给团队演示或生成HTML报告时用pyecharts,交互效果好。两套代码都封装在visual模块里,参数统一,切换成本很低。

from pyecharts.charts import Bar, Line from pyecharts import options as opts def render_bar(data_frame, x, y, title, output_path): bar = Bar() bar.add_xaxis(data_frame[x].tolist()) bar.add_yaxis("播放量", data_frame[y].tolist()) bar.set_global_opts(title_opts=opts.TitleOpts(title=title)) bar.render(output_path)

5. 代码组织:不是一次性脚本,而是一个能跑的小系统

5.1 分层目录结构

前面这些内容单独拉出来都能用,但组合成“系统”就得有工程结构。我按职责分成四个目录,每个目录只干一件事:

project/ ├── config/ │ └── settings.yaml ├── collector/ │ ├── api_client.py │ └──榜单配置.py ├── storage/ │ ├── database.py │ └── models.py ├── analysis/ │ ├── cleaner.py │ ├── features.py │ └── metrics.py ├── visual/ │ ├── chart_render.py │ └── report.py └── main.py

这个结构的好处是:改了采集逻辑不用动分析代码,改了图表样式不用动采集代码。对我这种一个人维护的项目来说,模块边界清晰比什么都重要。

5.2 配置管理与调度更新

榜单ID、请求间隔、数据库路径、每日采集榜单列表,这些参数全部放在config/settings.yaml里。比如:

playlists: hot: 3778678 new: 3779629 soar: 19723756 request_interval: min: 1.0 max: 2.5 database_path: "data/music_rank.db" analysis: top_n: 50 play_bins: [0, 10000, 100000, 1000000, 10000000]

日常更新我用APScheduler做定时任务,每天凌晨1点自动执行一次采集流程。

from apscheduler.schedulers.blocking import BlockingScheduler def job(): ingest_daily() process_data() generate_report() scheduler = BlockingScheduler() scheduler.add_job(job, "cron", hour=1, minute=0) scheduler.start()

为什么选凌晨1点?因为排行榜一般在午夜前后更新,等到1点数据已经稳定,避免采到更新到一半的脏数据。这个时间点调整过两次,实际跑下来凌晨1点最稳。

5.3 异常重试与日志

上线一周后我发现一个尴尬情况:某天凌晨采集时第三方接口超时,任务中断,那天就没有数据。为了不再出现这种情况,加了两个基础设施。

第一个是异常重试装饰器,网络请求失败自动退避重试:

import functools import time def retry_on_failure(max_retries=3, delay=5): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception: if attempt == max_retries - 1: raise time.sleep(delay * (attempt + 1)) return wrapper return decorator

第二个是日志,采集结果、清洗条数、异常堆栈全部写入文件。遇到哪天数据不对,翻日志就能定位,不用靠猜。

6. 实测中踩过的坑和完整排查链路

6.1 Cookie失效导致采集中断

现象:跑了四五天后,某天早晨发现采集任务失败,错误信息显示返回的不是JSON而是畸形数据。

我当时的排查链路是这样的:先看日志,发现失败请求全部集中在某个时间点之后;然后手动用浏览器访问榜单接口,返回正常;再用脚本请求同样接口,返回异常。这说明不是网络问题,是脚本请求被拦截了。再对比浏览器请求头和脚本请求头的差异,发现浏览器请求中带了Cookie字段,脚本里只带了UA和Referer。更新Cookie后恢复正常。

教训是:公开接口也会有基础风控,长时间不带Cookie高频请求容易被拦。解决方式是在配置里维护一组有效Cookie,定期更新;同时保持合理的请求间隔,不要顶到边界频率。

6.2 时区问题导致按天分区错位

现象:数据库里某天的数据数量明显偏少,部分日期甚至只有几十条。

排查过程:先看采集日志,当天任务确实把三个榜单都跑完了,每条请求都有成功返回;再看数据库,发现写入的track_date有部分是前一天日期。原因出在服务器的系统时区是UTC,而榜单更新时间和业务日期都用北京时间,凌晨1点执行任务时UTC时间是前一天17点,导致跨日数据被写进了错误的日期分区。

解决办法:在写入前强制把日期格式化为北京时间字符串,不使用系统本地时间。

from datetime import datetime, timezone, timedelta BJ_TIME = timezone(timedelta(hours=8)) def get_bj_date(): return datetime.now(BJ_TIME).strftime("%Y-%m-%d")

6.3 重复歌曲记录的合并陷阱

现象:分析歌手热度时发现某歌手的数据翻了一倍,肉眼可见不对。

排查过程:我用歌手名做分组统计之后,先抽查了几个歌手的明细,发现同一首歌在热歌榜和新歌榜都有记录,而我没有区分榜单类型,直接全表聚合,导致一首歌被算了两遍。

教训是:跨榜单聚合前必须想清楚“歌”的粒度。同一首歌在不同榜单出现是正常的业务现象,但统计“歌手有几首歌上榜”时应按榜单粒度说明,或者明确口径为“单日任意榜单出现的去重歌曲数”。正确写法是:

song_in_any_list = df.groupby(["track_date", "song_id"]).first().reset_index() song_count = expand_artist(song_in_any_list).groupby("artist")["song_id"].nunique()

6.4 接口字段变更的兼容处理

现象:某天起,部分歌曲的album字段返回为空,导致后续清洗时报错。

排查发现不是数据源坏了,而是平台返回字段偶尔缺失。程序里直接用t["al"]["name"]这种硬取字段的方式,一旦字段缺失就抛异常。

修复方式是把所有接口字段解析改成.get()带默认值,同时增加字段兼容层,遇到未知结构时记录日志而不是中断任务:

def safe_get(data, *keys, default=None): cur = data for key in keys: if not isinstance(cur, dict): return default cur = cur.get(key) if cur is None: return default return cur

这个改动看起来小,但避免了整个系统因为单条脏数据崩溃。工程上有个原则:采集环节永远不要让一条脏数据毁掉整批任务,把坏数据隔离出来、记下来、继续跑,等回头统一处理。

7. 从这套系统还能延伸出什么

最后聊点个人体会。这套榜单分析系统本身技术难度不大,真正的价值在于把“数据生产”这条链路跑通。我自己的感觉是,做完这个项目之后,再去看其他数据分析需求,思路会清晰很多,因为大部分分析工作的时间其实都花在采集、清洗、口径统一上。

想继续扩展的话,有几个方向我觉得很自然:一是接入评论数据,对热门歌曲的评论做文本挖掘,看听众讨论的关键词和情感倾向;二是做实时榜单监控,对排名突变超过一定阈值的歌曲做告警,研究“爆款什么时候开始爆”;三是把多日数据和节假日对齐,看榜单波动和事件之间的关联。不管往哪个方向走,前面搭好的采集层、存储层和特征层都能接着用,这也是当初做分层设计的回报。

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

Python五子棋实战:状态机设计与鲁棒性实现

简介:本资源是一份面向计算机专业本科生的Python毕业设计完整实践材料,聚焦五子棋游戏开发全流程,帮助学习者掌握GUI编程、游戏逻辑设计与软件工程规范。压缩包含1个253KB的Word文档(.doc),完整涵盖摘要、需…

作者头像 李华
网站建设 2026/10/21 23:15:33

S7-1200与V20变频器MODBUS RTU通讯调试核心:控制字切换与物理层校准

简介:本资源是一份面向工业自动化工程师与PLC初学者的实操型技术文档,聚焦S7-1200 PLC通过外部端子控制V20变频器启停,并基于MODBUS RTU协议实现频率读写的核心方法。内容覆盖硬件接线规范(CB1241模块与V20端子的屏蔽双绞线连接&a…

作者头像 李华
网站建设 2026/10/21 23:15:05

独立开发三年实战:需求验证、MVP边界与技术选型的避坑指南

如果把独立开发看作一场长跑,我恰好跑到了第三年。这三年里,我一个人走完了从需求、设计、开发到上线运营的整个闭环,经历过第一批用户带来的兴奋,也经历过连续好几个月没有增长、甚至想放弃的时刻。今天这篇内容不聊某个具体框架…

作者头像 李华
网站建设 2026/10/21 23:14:57

从零搭建技能地图:让团队能力盘点真正驱动用人决策

说实话,最初接到这个内部项目时,我心里是有点犯嘀咕的。项目代号就叫“skills”,目标很宽泛:把团队里每个人的技能底数摸清楚。听起来简单,不就是发个表让大家填一下“会什么、熟不熟”吗?但我见过的技能盘…

作者头像 李华
网站建设 2026/10/21 23:12:50

PaddlePaddle手写数字识别实战:从CNN原理到可复现训练

1. 这不是“抄作业”,而是用PaddlePaddle亲手搭出第一个能认图的神经网络你搜到“paddlepaddle卷积神经网络作业”这个标题,大概率正被某门人工智能导论、机器学习基础或深度学习实践课的期末大作业压着——老师布置了用飞桨(PaddlePaddle&am…

作者头像 李华
网站建设 2026/10/21 23:12:02

算法治理的“悬鉴”视角:养护人叙事环如何决定系统走向

“悬鉴”这个词,我一开始是从一篇讲传统治理思想的笔记里读到的,原意是挂一面镜子在高处,让所有经过它的人都下意识地照见自己。后来做算法治理相关工作久了,越来越觉得这个词被低估了。我们讨论算法治理,张口闭口往往…

作者头像 李华