每年到毕设季,总有人拿着一串关键字来问我:“我想做个数据分析类的系统,最好能自动抓数据,又能出图表,还能有个网页展示,工作量看起来也够大……”我一般先问一句:数据源定了没有?如果没定,网易云音乐排行榜数据分析系统,其实是相当稳的一个选择。
网易云音乐排行榜数据分析系统,这个题目我帮不少人梳理过技术方案,自己也踩过不少坑。它不单是“爬点数据再画几个图表”那么简单,而是一条从采集、清洗、分析到可视化展示的完整数据链路。用Django做Web框架,Python做数据处理,把链路跑通之后,无论拿去当毕业设计、课程项目,还是写成简历上的亮点项目,都很能打。
这篇文章不会给你贴网盘链接,而是把这个项目从选题逻辑、系统设计,到爬虫、入库、分析算法、Django页面实现,再到常见的坑和答辩准备,完整讲一遍。适合正在选毕设题目的你、写代码写到一半卡住的你,也想帮还没想清楚答辩怎么讲的人提前捋一捋思路。
1. 选题思路与系统整体设计
1.1 这个题目为什么值得做
先说选题。毕设最怕两件事:一是题目太简单,工作量撑不起论文;二是题目太硬核,做到一半发现自己搞不定。网易云音乐排行榜数据分析系统恰好卡在一个很舒服的位置。
数据源是公开的,排行榜页面也是大家每天都会用的功能,评阅老师不用费劲理解“你做的这个东西到底是干嘛的”。技术栈覆盖面很广:爬虫、数据库、数据清洗、数据分析、Web开发、可视化,每一个环节都能写出实质内容,论文的每一章都有东西可写。同时难度又可控,单机开发就能完成,不需要分布式集群或者GPU这类资源。
但要注意一点:如果只是“爬虫爬下来,ECharts画几张图”,那这个项目很容易被当成花架子。评阅老师看的是逻辑完整性和工程化程度,而不只是看页面漂不漂亮。所以我在设计这个题目时,刻意把系统做成了一个闭环:每天自动定时采集、数据入库、清洗分析、结果落表、Web页面展示。这样整个系统有一个自洽的故事线,答辩时不会一被追问就露馅。
1.2 系统模块划分与数据流
项目整体划分成四个模块:
- 数据采集模块:定时请求排行榜接口,解析返回的JSON,把歌曲、排名、热度、评论等数据写入数据库。
- 数据存储模块:用MySQL存储原始数据和中间结果,保留每天的历史排名记录,后续分析都从这里取数。
- 数据处理与分析模块:对原始数据做去重、补缺、格式清洗,然后计算热度指数、歌手排名、歌曲趋势、评论关键词等指标。
- Web展示与管理模块:Django从数据库读取分析结果,通过JSON接口输出到前端页面,使用ECharts渲染成排行榜、趋势图、占比图等。
这条链路一定不能断。很多人做了一个能跑的爬虫,也觉得做了个能打开的页面,但中间的分析和落表环节是空的,数据直接写死在前端。这属于自欺欺人,答辩时一问“指标是怎么算出来的”,整个人就卡住了。
数据流梳理成一条线之后,后面不管改哪个模块,心里都有底。这个项目的核心不在于单一技术多深,而在于数据从起点到终点的流动过程是否完整、合理。
1.3 技术选型:不要一上来就堆重型组件
我第一次做类似项目时,第一反应是“要用就用一套大而全的架构”,结果给自己找了一堆麻烦。现在回头想,技术选型的核心原则是:够用、能讲清楚、不出幺蛾子。
| 方向 | 推荐选择 | 不推荐选择 | 理由 |
|---|---|---|---|
| Web框架 | Django | Flask | Django自带ORM、Admin后台、迁移工具,不用自己拼一堆库 |
| 爬虫 | requests | Scrapy | 单榜单场景请求量不大,requests足够,Scrapy反而增加学习成本 |
| 数据库 | MySQL | MongoDB | 排行榜、歌曲、评论之间有明确关系,MySQL更容易让评阅老师看懂 |
| 可视化 | ECharts | Matplotlib | Web页面展示时ECharts交互性强,图表类型丰富 |
| 定时任务 | APScheduler | Celery | Celery需要额外引入消息队列,太重;APScheduler随Django进程运行就够 |
当然,Scrapy不是不能用,如果项目要爬很多歌单、很多页,再用Scrapy也合理。但在毕设场景里,requests加一个定义良好的Spider类反而更直观,也更好调试。
2. 数据采集:把稳定的数据源握在手里
2.1 榜单接口与关键字段分析
做爬虫第一步不是写代码,而是打开浏览器开发者工具,找到排行榜页面背后的数据接口。网易云音乐榜单页面的数据是以JSON格式返回的,里面有歌曲列表。字段大概包含这几类:
| 字段 | 含义 | 备注 |
|---|---|---|
| song_id | 歌曲唯一ID | 去重和关联的唯一依据 |
| name | 歌名 | 展示用 |
| artists | 歌手列表 | 可能有多位歌手,需要拆分 |
| album | 专辑信息 | 包含专辑名和封面地址 |
| duration | 时长 | 单位毫秒,展示时换算 |
| play_count | 播放量 | 热度计算的关键指标 |
| comment_count | 评论数 | 热度计算的关键指标 |
| share_count | 分享数 | 热度计算的关键指标 |
网上能找到的接口地址经常失效,因为这类接口普遍带签名参数,一段时间可能就变了。所以我的习惯是:把工具打开,自己抓一次当前可用的接口,再根据实际返回结构写解析代码。抄旧代码的成本往往比想象中高。
一个基础版本爬虫大概长这样:
import requests from datetime import date import time class MusicRankSpider: def __init__(self): self.session = requests.Session() self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://example.com/", }) def fetch_rank(self, rank_id): url = "https://example.com/rank/list" params = {"id": rank_id, "limit": 50} resp = self.session.get(url, params=params, timeout=10) resp.raise_for_status() data = resp.json() return data.get("songs", []) def parse_and_save(self, rank_id): songs = self.fetch_rank(rank_id) today = date.today().isoformat() for index, song in enumerate(songs, start=1): record = { "song_id": song["id"], "name": song["name"], "artists": ",".join(song["artists"]), "album": song.get("album", {}).get("name", ""), "duration": song.get("duration", 0), "rank": index, "play_count": song.get("play_count", 0), "comment_count": song.get("comment_count", 0), "share_count": song.get("share_count", 0), "rank_date": today, } # 这里调用数据库入库函数 save_rank_record(record) time.sleep(2)请求头里的Referer和User-Agent很重要。现在很多接口会校验请求来源,拿到一个空列表或者403状态码时,优先检查这两个字段是不是齐全。登录态的Cookie有时也会影响接口返回的数据范围,但排行榜数据一般不需要登录也能拿。
2.2 采集工程化:增量策略、入库设计与反封经验
数据不能只抓一次,否则没法做趋势分析。排行榜本身是日更的,所以采集策略按天增量就够了。
每个榜单建立一个rank_record表,每天写入当天50首歌的排名和热度数据。这样既能看“今天谁上榜了”,也能看“某首歌从上周到今天排名是怎么变化的”。我见过有人用一张song表直接覆盖热度值,这种做法会丢掉历史记录,等于把趋势分析这条路堵死了。
建表SQL参考如下:
CREATE TABLE song ( song_id BIGINT PRIMARY KEY, name VARCHAR(200) NOT NULL, artists VARCHAR(500) DEFAULT '', album VARCHAR(200) DEFAULT '', duration INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE rank_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, song_id BIGINT NOT NULL, rank INT NOT NULL, play_count BIGINT DEFAULT 0, comment_count INT DEFAULT 0, share_count INT DEFAULT 0, rank_date DATE NOT NULL, UNIQUE KEY uk_song_date (song_id, rank_date), KEY idx_date (rank_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意几个细节:
- 歌曲表用utf8mb4,而不是utf8。歌曲名和评论内容里经常出现特殊字符,utf8mb4才能完整支持。
- 播放量、分享数这类字段要用BIGINT。排行榜头部歌曲的播放量动不动百万、千万,INT很容易溢出。
- 加唯一键uk_song_date。同一首歌同一天的数据重复写入时,直接用INSERT INTO ... ON DUPLICATE KEY UPDATE做覆盖,避免重复。
反封方面,我的经验是:不要一次性并发请求几十个接口,主动限速比被封之后再折腾代理省心得多。每次请求之间随机延迟1到3秒,失败时做指数退避重试,连续失败超过5次就停止当天采集并记录日志。这套做法对毕设场景完全够用,也足够稳妥。
3. 数据清洗与排行榜分析算法
3.1 数据清洗:去重、补缺、统一格式
爬下来的数据不会干干净净。常见问题有三类:重复、缺失、格式不一致。
重复问题比较好理解,榜单接口偶尔会把同一首歌返回两次,或者今天抓完失败重跑了一遍。用唯一键就能兜住,但入库前最好在内存里做一次去重,减少无效数据库写入。
缺失值处理需要按字段区分。歌名为空或者song_id缺失的记录直接删掉,这些行没有分析价值。热度字段如果有个别缺失,可以按同一首歌前一天的数值填充,也可以用当天歌曲热度均值填充。具体用哪种,我建议在代码里写好注释,因为论文里要写清楚“为什么这么填”。
格式统一是很多人忽略的点。日期要统一成YYYY-MM-DD格式,歌手列表要拼接成方便分拆的字符串,时长从毫秒换算成秒或者分钟展示。用pandas处理起来是这样的:
import pandas as pd df = pd.read_sql("SELECT * FROM rank_record", engine) # 去重 df = df.drop_duplicates(subset=["song_id", "rank_date"]) # 歌名缺失直接删除 df = df.dropna(subset=["name"]) # 播放量缺失用前值填充,按歌ID分组 df["play_count"] = df.groupby("song_id")["play_count"].ffill().fillna(0) # 日期统一 df["rank_date"] = pd.to_datetime(df["rank_date"]).dt.strftime("%Y-%m-%d")这一段虽然看起来简单,却是分析结果可信度的基础。数据都没洗干净,后面算出来的所谓“歌手Top10”就没有说服力。
3.2 热度指数与榜单分析算法
排行榜页面给的是一个直接排名,但页面里没有告诉你这个排名是怎么算出来的,也没有综合分数。很多排序算法也不会公开权重。所以项目里要自己做一套热度指数,作为分析的抓手。
直接用播放量排序太粗暴,因为评论数、分享数同样是歌曲热度的重要信号。但三个指标的量纲不一样,直接相加的话,播放量10万和评论量100的数值完全不在一个量级。所以我习惯先取对数再加权:
score = 0.5 * log10(play_count + 1) + 0.3 * log10(comment_count + 1) + 0.2 * log10(share_count + 1)
举例说明:假设某首歌播放量100万,评论数2000,分享数5000,那么:
- 播放量部分:0.5 * log10(1000001) = 0.5 * 6 = 3.0
- 评论数部分:0.3 * log10(2001) = 0.3 * 3.301 = 0.990
- 分享数部分:0.2 * log10(5001) = 0.2 * 3.699 = 0.740
最终得分是4.73左右。而另一首歌播放量50万,评论数8000,分享数2000,计算得到3.27分。两组数据对比,能看到权重设计如何影响排名。这个公式的优点是可以自己讲明白“为什么这么设计”:取对数是为了压缩极端值,权重体现三个指标的重要程度差异。
清洗完的数据通过pandas算好分数,写入analysis_result表:
CREATE TABLE analysis_result ( id BIGINT PRIMARY KEY AUTO_INCREMENT, song_id BIGINT NOT NULL, score DECIMAL(10, 4) NOT NULL, rank_date DATE NOT NULL, trend VARCHAR(10) DEFAULT 'flat', KEY idx_date_score (rank_date, score) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;trend字段记录这首歌当天相比昨天排名的变化,可以用up、down、flat表示。
3.3 歌曲趋势、歌手榜与评论关键词分析
有了连续多天的rank_record数据,趋势分析就顺理成章了。取某一首歌按时间排序的排名序列,画一条折线,能直观看到歌曲上榜后的变化曲线。很多热门新歌会有一个快速上升、然后缓慢下降的过程,这个现象写进论文里非常容易出彩。
歌手榜分析需要把歌曲表里的artists字段拆分出来。一首歌可能有多位歌手,清洗时用逗号分隔存储,分析时用pandas做拆分再分组聚合:
df_singer = df.assign(artist=df["artists"].str.split(",")).explode("artist") df_singer = df_singer.groupby("artist")["score"].sum().reset_index() df_singer = df_singer.sort_values("score", ascending=False)这样可以得到一个“歌手热度榜”,体现的不只是单个歌手有没有上榜歌曲,而是上榜歌曲的综合表现。如果某位歌手有三首歌都在榜单上,他的排名大概率会靠前。
评论关键词分析是可选的加分项。用jieba对每首歌的高赞评论做分词,去掉停用词后统计词频,把Top20关键词展示在歌曲详情页里。这一步不算复杂,但能让项目的“数据分析”含量更高,评阅老师也会觉得你不是只会算平均数。
import jieba from collections import Counter def extract_keywords(comments, top_n=20): words = [] stop_words = {"的", "了", "是", "我", "你", "他", "在", "也", "都"} # 根据实际补充 for comment in comments: words += [w for w in jieba.lcut(comment) if w.strip() and w not in stop_words] return Counter(words).most_common(top_n)4. Django Web 实现:从数据库到可视化页面
4.1 ORM模型设计与管理后台
Django项目的地基是models.py。把前面的表结构翻译成ORM模型:
from django.db import models class Song(models.Model): song_id = models.BigIntegerField(primary_key=True) name = models.CharField(max_length=200) artists = models.CharField(max_length=500) album = models.CharField(max_length=200, blank=True) duration = models.IntegerField(default=0) class RankRecord(models.Model): song = models.ForeignKey(Song, on_delete=models.CASCADE) rank = models.IntegerField() play_count = models.BigIntegerField(default=0) comment_count = models.IntegerField(default=0) share_count = models.IntegerField(default=0) rank_date = models.DateField() score = models.DecimalField(max_digits=10, decimal_places=4, default=0) class Meta: unique_together = ("song", "rank_date")这里把score放到了RankRecord里,而不单独再建一张analysis_result表,是为了查询方便。Web页面展示排行榜时,直接按日期倒序取数据就行,不用再做多表关联。
把这个模型注册进Django Admin后台:
from django.contrib import admin from .models import Song, RankRecord @admin.register(Song) class SongAdmin(admin.ModelAdmin): list_display = ("song_id", "name", "artists") search_fields = ("name", "artists") @admin.register(RankRecord) class RankRecordAdmin(admin.ModelAdmin): list_display = ("rank", "song", "rank_date", "score") list_filter = ("rank_date",)Admin后台不只是为了摆个样子,日常检查数据是否正常入库时非常方便。爬虫跑完,打开后台看一眼今天的榜单有没有写进去,比查数据库省事很多。
4.2 数据接口与ECharts可视化
页面展示采用前后端不分离的方案,Django渲染HTML模板,页面里的图表数据通过JSON接口异步获取。这样做的好处是图表可以局部刷新,数据更新时不用整页刷新。
一个获取排行榜数据的接口可以写成这样:
from django.http import JsonResponse from .models import RankRecord def ranking_api(request): rank_date = request.GET.get("date", "") records = ( RankRecord.objects.filter(rank_date=rank_date) .select_related("song") .order_by("rank")[:50] ) data = [ { "rank": r.rank, "name": r.song.name, "artists": r.song.artists, "score": float(r.score), } for r in records ] return JsonResponse({"code": 0, "data": data})前端页面用ECharts的话,可以在HTML里引入CDN资源:
<div id="rankChart" style="width:100%;height:500px;"></div> <script src="https://cdn.example.com/echarts.min.js"></script> <script> fetch("/api/ranking?date=2025-01-01") .then(res => res.json()) .then(res => { const chart = echarts.init(document.getElementById("rankChart")); chart.setOption({ title: { text: "排行榜Top30" }, tooltip: {}, xAxis: { type: "category", data: res.data.map(item => item.name) }, yAxis: { type: "value" }, series: [{ type: "bar", data: res.data.map(item => item.score) }] }); }); </script>同一个页面里可以放多个图表,除了柱状图,还能放歌手Top10的横向条形图、歌曲类型占比的饼图、单首歌曲排名变化的折线图。数据都从同一个JSON接口家族里拿,ECharts的option配置按图表类型做区分。
开发时一个常见问题是跨域。如果Django直接渲染页面,接口是同源的,不存在跨域问题,也就不需要配CORS头。只有当Django只做后端API、前端单独部署时才要处理,毕设项目建议直接走同源方案,省掉一堆麻烦。
4.3 定时调度与后台任务
数据不能每天都手动跑一遍爬虫。用APScheduler加上Django启动时的配置,可以实现每天凌晨自动执行采集和分析。
关键点是APScheduler的job定义:
from apscheduler.schedulers.background import BackgroundScheduler scheduler = BackgroundScheduler(timezone="Asia/Shanghai") def daily_task(): spider = MusicRankSpider() spider.run_all_ranks() run_analysis_pipeline() scheduler.add_job(daily_task, "cron", hour=1, minute=30) scheduler.start()注意两件事:timezone要显式设置,否则可能按服务器默认时区跑,你睡醒了它还没跑;另外Django开发服务器如果开启了自动重载,scheduler可能会被初始化两次,导致任务重复执行,部署时用gunicorn之类的常驻进程更稳妥。
后台任务跑完之后,最好能留下日志。爬虫每次抓了多少条、清洗后剩多少条、分析任务是否成功,都写进日志文件。出了问题,第一眼就能看出是采集环节挂掉还是分析脚本报错,不用从头猜。
5. 常见问题、避坑经验与答辩建议
5.1 问题排查速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 中文乱码 | 数据库字符集不是utf8mb4 | 建库时指定DEFAULT CHARSET=utf8mb4,连接字符串加charset参数 |
| 页面图表不显示 | 接口返回空数据或字段名不一致 | 先直接访问API地址看返回JSON,再检查前端取数据时字段拼写 |
| 爬虫偶尔拿不到数据 | 接口签名变化或请求头缺失 | 重新打开开发者工具抓接口,比对请求头差异 |
| 播放量数字看起来不对 | INT字段溢出变成负数 | 把字段改成BIGINT |
| 定时任务不执行 | 时区问题或scheduler重复初始化 | 配置Asia/Shanghai时区,部署时使用常驻进程 |
| 部署后静态文件404 | DEBUG=False时静态文件没有收集 | 配置STATIC_ROOT并执行collectstatic |
| 分析脚本报错 | 数据库有脏数据 | 先跑数据清洗,再跑分析,不要跳过清洗步骤 |
这些坑基本都是我实际遇到过的。最烦的是中文乱码,一旦数据入库时选错字符集,前面几十天的数据都要重新抓。所以数据库建库脚本里一定要写明utf8mb4,初始化环境时先检查这一步。
5.2 远程调试流程与日志定位
远程调试这个需求在毕设时期特别多。拿到一套源码,在本地跑不起来,抱怨最多的就是把代码发过去之后,对方跑半天没有输出。
我在帮人排查时,习惯按照这条路径来走:
- 先看服务能不能启动。执行python manage.py runserver之后,报错信息贴出来。大多数问题是缺少依赖,或者Python版本不匹配。
- 再看依赖。用requirements.txt安装所有第三方库,如果版本冲突,优先处理版本冲突;建议虚拟环境部署。
- 接着测试数据库连接。迁移跑不跑得通、后台能不能看到数据,如果连数据库都没连上,页面必然白屏。
- 最后跑一个单独脚本测试爬虫,确认网络和接口是否正常。很多“部署后爬虫不工作”的问题,其实是服务器所在的网络环境无法访问目标站点,和代码无关。
排查时不要凭感觉改代码。日志是唯一的线索来源。Django的DEBUG输出、爬虫自己写的日志、系统日志,都看一遍,才能定位到底卡在哪一环。
5.3 答辩演示的关键准备
演示环节我见过太多翻车现场,共通问题就是“太依赖现场网络”。爬虫接口一旦抽风,ECharts就画不出图,这时候PPT讲得再好也顶不住。
我的建议是:答辩前把关键分析结果导出成静态JSON,前端代码里加一个“离线模式”。当接口请求失败时,页面自动读取本地JSON文件展示数据。平时开发用动态接口,答辩演示强制走离线模式。这个备胎方案能救回一整个演示环节。
讲解主线最好按数据流走,不要跳着讲。先讲数据从哪来、怎么采集,再讲怎么清洗入库,然后讲分析算法为什么这么设计,最后展示页面。整个过程就是一个闭环,评阅老师顺着你的思路理解,比看着一屏代码讲故事更容易跟住。
有些高频问题可以提前想好回答:
- 为什么权重取0.5、0.3、0.2?答:参考业务逻辑,播放量直观反映受众规模,评论和分享代表互动深度,取对数消除量纲差异。
- 为什么不实时抓取?答:排行榜本身日更,日级采集足够支撑分析,也避免给目标站点造成不必要的压力。
- 数据量有多大?答:一个榜每天约50条记录,连续采集半年约9000条,单机MySQL完全可以承载,不需要分布式方案。
5.4 后续扩展:从毕设到可以写进简历的项目
如果毕业设计做完还想要更好看,或者打算把这个项目延伸到简历项目、论文方向,有三条路可以走。
第一是情感分析。把评论内容做正负向判定,给每首热门歌曲加一个“评论区情绪画像”。这个扩展用现有的自然语言处理库就能做,数据量也不大,足够出结果。
第二是个性化推荐。基于用户收藏和歌曲热度数据,做一个简单的协同过滤推荐模块。这会引入用户维度数据,让系统从“看榜单”变成“猜你喜欢”,项目档次明显不一样。
第三是热度预测。用时间序列方法预测未来几天榜单走势。实现起来有一定难度,但作为扩展点写在论文展望里,评阅老师会觉得你对方向有思考,而不是只完成了基本功能。
最后分享一个我自己的习惯。这种“数据采集到展示”的项目,最容易被忽视的就是数据完整性验证。你辛辛苦苦跑了一个月定时任务,结果某天凌晨服务器内存不足,任务挂了三天没跑,漏了几天的榜单数据。后面的趋势分析就会出现缺口。我后来在定时任务里加了一道检查:每天任务执行完后,统计一下当天实际入库条数,如果和预期数量不一致就发一条提醒。这个习惯帮我提前发现过好几次采集异常,比等分析时发现数据不对再回查日志高效得多。做数据类项目,让数据链路“可观察、可追踪”,比单纯堆功能更有价值,这也是这个项目做完之后最大的收获。