简介:面向计算机相关专业毕业设计和课程设计的Django网易云音乐数据分析可视化大屏项目,基于Scrapy抓取网易云音乐数据,经Django后端处理,通过ECharts构建可视化大屏页面,可帮助学习者掌握从数据采集、存储建模到前端展示的完整流程。资源包共60个文件,压缩后约622KB,核心是23个Python源码文件,覆盖Django模型、视图、URL路由、admin后台、迁移脚本及项目配置;前端由HTML、CSS和JavaScript组成大屏页面,并使用ECharts、jQuery、flexible等脚本实现图表绘制与屏幕适配;另含CSV数据文件、TTF字体、图片素材、Dockerfile、uwsgi.ini等部署配置,以及设计报告docx和说明文档,目录结构清晰。目前已有58人学习下载。项目代码完整可运行,参照说明文档与设计报告即可理解Scrapy爬虫、Django MTV架构和大屏可视化的实现细节;需要二次开发时,可替换数据源或调整图表模块,将项目迁移至其他音乐、电商数据分析场景,适合作为毕业设计模板,也适合新手进阶练习。
1. Django网易云毕业设计这条链路,真正难的是中间那段数据
这套压缩包把毕业生要交的东西拆成了四份:Django 写后台、数据分析整理网易云数据、可视化大屏做图表、设计报告应付答辩。多数人拿到标题先开 Django 工程、先把 ECharts 跑起来,最后发现画不出图,问题全出在中间那段数据上——原始数据有多少列、空值和"1.2万"这种字符串怎么处理、表之间怎么关联。这篇按"数据清洗→Django建模→聚合API→大屏渲染→交付部署"的顺序把整条链路走一遍,从能跑的代码讲到参数边界和已知的坑。适合做 Python 毕业设计的学生,也适合想快速搭一套"后台+统计接口+大屏"参考实现的工程师。
2. 数据分析第一步:把网易云数据清洗成 Django 能用的表
2.1 网易云数据分析的对象:歌单、歌曲、评论
网易云音乐的数据面很大,歌曲、歌单、歌手、专辑、评论、用户动态任意组合都能出题。毕业设计不建议全做,三张表最稳:歌单表、歌曲表、评论表。歌单是入口,歌曲挂在歌单下,评论挂在歌曲下,外键只有两层,查询不绕,画大屏时按歌单聚合、按歌曲排序、按时间看趋势都够用。
这组关系最终会落地成 Playlist 到 Song 再到 Comment 的两层一对多:
| 数据面 | 典型字段 | 大屏里的用途 |
|---|---|---|
| 歌单 | 名称、标签、播放量、收藏数 | 首页指标卡与排行 |
| 歌曲 | 歌名、歌手、专辑、播放量、时长 | 柱状图 / 折线图 |
| 评论 | 用户、内容、点赞数、发布时间 | 词云与热评表格 |
选定这三张表之后再决定有多少列。网易云接口返回的歌曲字段有二十多个,我只留歌名、歌手、专辑、时长、播放量这五列,其余字段对统计没帮助,还会让清洗脚本多写一堆判空。
2.2 用 pandas 清洗原始 CSV:空值、重复值和"万"字符
原始数据到手后不直接入库。常见做法是先导出成 CSV,再做三步清洗:重复歌曲按song_id去重;歌名为空的行直接删,因为后续所有聚合都依赖歌名;播放量如果出现"12.3万"这种字符串,需要转成整数,否则前端图表没法排序。
import pandas as pd # 读取网易云导出的原始数据 df = pd.read_csv("netease_songs_raw.csv", encoding="utf-8") # 按歌曲ID去重,保留第一次出现的记录 df = df.drop_duplicates(subset=["song_id"]) # 删除关键列空行,后续聚合依赖这三列 df = df.dropna(subset=["song_id", "song_name", "play_count"]) # 把 "12.3万" 转成 123000 def parse_wan(s): if not isinstance(s, str): return int(s or 0) if "万" in s: return int(float(s.replace("万", "")) * 10000) return int(float(s)) df["play_count"] = df["play_count"].apply(parse_wan) df["duration_sec"] = df["duration_sec"].astype(int) df.to_csv("netease_songs_clean.csv", index=False)drop_duplicates(subset=["song_id"])按歌曲 ID 去重,一首歌出现在多个歌单时是否保留由你的业务决定,这里只保留第一次出现。dropna(subset=[...])指定必须存在的三列,任何一个为空就整行删除。parse_wan里用字符串.replace("万","")配合float转换,比正则简单,也避免了"1.2万"被机械替换成"1.20000"的错误。astype(int)把时长这类数值列统一成整数,方便后面 ORM 的IntegerField接收。
提示:如果原始数据是从接口分段拉取的,CSV 里可能出现完全一致的两行,此时去重后行数明显减少,属于正常现象,不用怀疑脚本写错。
2.3 Django 模型设计:三张表,外键不要超过两层
数据清洗完成后再建模型。先用python manage.py startapp music建出应用,再按照清洗结果的字段形状写模型。
# music/models.py from django.db import models class Playlist(models.Model): name = models.CharField(max_length=128) tags = models.CharField(max_length=128, blank=True) play_count = models.IntegerField(default=0) class Meta: db_table = "playlist" class Song(models.Model): playlist = models.ForeignKey(Playlist, on_delete=models.CASCADE, related_name="songs") song_name = models.CharField(max_length=128, db_index=True) artist = models.CharField(max_length=64) album = models.CharField(max_length=128, blank=True) duration_sec = models.IntegerField(default=0) play_count = models.IntegerField(default=0, db_index=True) class Meta: db_table = "song" class Comment(models.Model): song = models.ForeignKey(Song, on_delete=models.CASCADE, related_name="comments") user_name = models.CharField(max_length=64) content = models.TextField() liked_count = models.IntegerField(default=0) posted_at = models.DateTimeField(db_index=True) class Meta: db_table = "comment"playlist和song两个外键的related_name一定要写。大屏接口里最常出现的查询是"取某歌单下播放量 Top 10",没有related_name只能写song_set,一旦嵌套两层以上可读性断崖下降。db_index=True加在song_name、play_count、posted_at上,这三个字段是高频过滤和排序条件,走索引能避免全表扫描。on_delete=models.CASCADE是刻意选的,评论跟着歌曲删、歌曲跟着歌单删,演示项目不需要纠结数据保留问题。建好模型后执行makemigrations和migrate生成表结构。
2.4 批量入库:用 bulk_create 而不是循环 save
几万行数据用循环save()也可以跑,但每条数据一次数据库往返,导入脚本要跑很久。常见做法是拼成对象列表后用bulk_create一次性提交。
# music/management/commands/import_music.py from django.core.management.base import BaseCommand from music.models import Playlist, Song class Command(BaseCommand): def handle(self, *args, **options): playlist = Playlist.objects.get(pk=1) # 从清洗后的CSV逐行读取并构造Song对象 songs = [ Song(playlist=playlist, song_name=row["song_name"], artist=row["artist"], play_count=row["play_count"]) for row in read_cleaned_rows() ] Song.objects.bulk_create(songs, batch_size=500)batch_size=500控制每条 INSERT 语句装载的行数,避免单条 SQL 过长。bulk_create不会触发模型save()方法,如果后续在save里加了校验或信号监听,导入脚本需要自己补这段逻辑。入库完成后在 Django shell 里执行Song.objects.count(),核对行数和 CSV 行数是否一致。
3. Django 聚合查询与接口设计:统计逻辑交给 ORM,而不是 Python
3.1 用 annotate 和 aggregate 一次取出统计值
大屏首页需要四类数字:歌曲总数、播放总量、平均播放量、评论总数。用 ORM 的聚合函数在数据库层算完,比把全表拉进 Python 再sum()快一个量级。
from django.db.models import Count, Sum, Avg from django.http import JsonResponse from music.models import Playlist, Song, Comment def overview(request): total_songs = Song.objects.count() total_play = Song.objects.aggregate(total=Sum("play_count"))["total"] or 0 avg_play = Song.objects.aggregate(avg=Avg("play_count"))["avg"] or 0 total_comments = Comment.objects.count() data = { "total_songs": total_songs, "total_play": total_play, "avg_play": round(avg_play, 1), "total_comments": total_comments, } return JsonResponse({"code": 0, "message": "ok", "data": data})aggregate返回的是字典,不是 QuerySet。空表上Sum和Avg返回None,所以加了or 0兜底。播放量均值保留一位小数,避免前端图表出现一长串浮点数。排行榜同样在数据库层处理:
top_songs = list( Song.objects.order_by("-play_count")[:10].values( "song_name", "artist", "play_count" ) )order_by的参数是模型字段名前加负号,表示倒序。[:10]是取前十条。values(...)让查询结果只包含三个列,后面转 JSON 时不会把整条对象都带出去。
3.2 大屏接口的数据结构约定
大屏页面需要的数据一般有三块:指标卡、排行榜、趋势折线。接口文档里最好固定一层统一壳子,前端只看code判断成功,再按 key 取业务数据:
{ "code": 0, "message": "ok", "data": { "overview": {}, "top_songs": [], "trend": [] } }趋势数据按发布时间分桶统计评论量:
trend = [ {"x": item["posted_at__date"], "y": item["cnt"]} for item in Comment.objects .values("posted_at__date") .annotate(cnt=Count("id")) .order_by("posted_at__date") ]values("posted_at__date")会按日期去重分组,annotate(cnt=Count("id"))给每组加上计数列,order_by("posted_at__date")让数据按时间升序排列,画折线图时不需要前端再排序。我把字段名直接转成了x和y,省掉前端一层 map。
| 操作 | 返回类型 | 空表行为 |
|---|---|---|
count() | int | 0 |
Sum("字段") | 字典 | None |
Avg("字段") | 字典 | None |
annotate(...) | QuerySet | 空 QuerySet |
3.3 查询性能:N+1 问题与索引使用
"取歌单列表,再在模板里循环取每首歌"是 Django 里最常见的 N+1 查询。模板每访问一次song.playlist就发起一条新 SQL,歌单一百个就是一百零一条查询。反向同理,取歌单后循环访问playlist.songs也是一样。
# 正向访问外键,用 select_related songs = Song.objects.select_related("playlist").filter(play_count__gt=10000) # 反向访问关联集合,用 prefetch_related playlists = Playlist.objects.prefetch_related("songs").all()select_related适合多对一和一对一,通过 JOIN 把外键表塞进同一条 SQL。prefetch_related适合一对多,它先查主表再查关联表,最后在 Python 里做分组,避免 JOIN 造成结果行膨胀。判断标准很简单:模板或序列化逻辑里访问了外键或反向外键,就加,代价只有一条额外查询。
索引方面,db_index=True对范围查询和排序同样有效。但注意在posted_at上做EXTRACT(YEAR FROM posted_at)这类日期函数时索引会失效,因为对字段做运算后无法命中 B-Tree。正确写法是用posted_at__year=2023这种 ORM 查询,把日期截取动作交给数据库的底层实现。
3.4 删除操作:先过滤再执行,确认影响行数
n = Song.objects.filter(play_count__lt=1).delete() print(n) # (2, {'music.Song': 2})delete()返回的元组包含删除总行数和分表数量。objects.all().delete()会清空全表,正常开发中没人这么写,但在不同项目里见过两次因为 filter 条件写错导致全表被清的情况。稳妥的做法是删除前先用count()预览影响行数,再执行delete()。
外键on_delete=CASCADE意味着删除歌单会把歌曲和评论一起删掉。如果哪天真要保留评论,就改成SET_NULL,同时把外键字段设为null=True。设计报告里如果对比了这两种删除策略,反而是个加分项。
4. 可视化大屏:ECharts 渲染与多屏适配的细节
4.1 从接口 JSON 到 echarts.setOption 的映射
大屏页面挂在 Django 的 template 下,常见做法是模板里放若干个图表容器,页面加载时统一请求一次/api/overview/,再按 key 分发给不同的渲染函数。
<!-- templates/dashboard.html --> <div id="screen"> <div id="chart-top"></div> <div id="chart-trend"></div> </div> <script src="{% static 'lib/echarts.min.js' %}"></script> <script> fetch("/api/overview/") .then((res) => res.json()) .then((res) => { if (res.code !== 0) return; renderTop(res.data.top_songs); renderTrend(res.data.trend); }); </script>fetch的 URL 用相对路径,不要写http://127.0.0.1:8000这种绝对地址,否则部署到服务器后还要改代码。echarts.min.js 建议下载到本地 static 目录,不要依赖 CDN,答辩现场的网速不可控。
function renderTop(songs) { const chart = echarts.init(document.getElementById("chart-top")); chart.setOption({ xAxis: { type: "category", data: songs.map((s) => s.song_name) }, yAxis: { type: "value", max: (value) => Math.ceil(value.max * 1.2), }, series: [{ type: "bar", data: songs.map((s) => s.play_count) }], }); }yAxis.max用函数动态设置,是让图表顶部留出 20% 空白,否则最高柱会顶满绘图区,视觉上很不协调。拿到接口数据先map成需要的维度,不要在setOption里做复杂遍历。
4.2 大屏适配:用 scale 方式而不是逐元素写死
可视化大屏适配是这个标题里最容易踩坑的部分。很多模板按 1920×1080 设计稿制作,拿到 1366 或 2560 的屏幕就乱。常见做法是整块#screen用绝对定位和transform: scale按浏览器视口缩放。
#screen { width: 1920px; height: 1080px; transform-origin: 0 0; }function fitScreen() { const scale = Math.min( window.innerWidth / 1920, window.innerHeight / 1080 ); document.getElementById("screen").style.transform = "scale(" + scale + ")"; } window.addEventListener("resize", fitScreen); fitScreen();transform-origin: 0 0保证缩放时从左上角开始,否则默认原点在中心,页面会偏移。用Math.min等比缩放,两边会留白,但图表和文字不会变形。如果要求完全铺满,可以改成scaleX和scaleY分开算,代价是图形被压扁,不推荐。
| 适配方式 | 优点 | 缺点 |
|---|---|---|
| flex / rem 流式布局 | 拉伸后文字不糊 | 不同分辨率下布局会变形 |
| transform: scale 整体缩放 | 严格还原设计稿 | 等比缩放时留边,非等比时变形 |
图表实例在缩放后要调用chart.resize(),因为容器尺寸没变时 ECharts 不会自动重绘。这个细节容易被忽略,实际效果是"缩放页面后图表大小正常,但图例和坐标轴错位"。
提示:答辩现场的电脑大概率没有外网,所有 JS 库放本地 static,这个细节经常被忽视。
4.3 3D 地区地图:新版 ECharts 不再内置中国地图
如果大屏需要"3D 地区地图"这种区域数据展示,别直接抄旧代码。新版 ECharts 的map类型不再内置中国地图 GeoJSON,需要自己准备china.json并注册。
fetch("/static/geo/china.json") .then((res) => res.json()) .then((geo) => { echarts.registerMap("china", geo); const chart = echarts.init(document.getElementById("map")); chart.setOption({ series: [{ type: "map3D", map: "china", itemStyle: { color: "#1e3c72" }, data: geoData, shading: "lambert" }], }); });map3D系列来自 echarts-gl 扩展包,要在echarts.min.js之后额外引入echarts-gl.min.js。地图数据需要自己准备或从公共数据源下载,注意坐标系要统一,如果业务经纬度和地图底图坐标系不一致,区域标记会错位。3D 地图观感好,但如果电脑显卡不支持 WebGL,页面会直接黑掉,所以一定要准备一张普通柱状图做降级方案。
5. 设计报告之外:交付前检查这四件事
5.1 用 Django admin 复核数据质量
在admin.py里注册模型,用后台快速检查排行榜数据是否合理:
# music/admin.py from django.contrib import admin from music.models import Song @admin.register(Song) class SongAdmin(admin.ModelAdmin): list_display = ("song_name", "artist", "play_count", "playlist") search_fields = ("song_name", "artist") list_filter = ("playlist",)list_display把歌名、歌手、播放量并列显示,导入的数据有没有空歌手、播放量有没有异常值一眼就能看出来。歌曲量到几万条时,admin 外键下拉框会卡,因为要查询全部歌曲。简单处理是把外键字段替换成搜索选择框,或者在列表页只读显示字段名。如果觉得原生 admin 样式不够好看,挂 simpleui 也简单,但数据量大时会更卡,界面美化不影响核心功能。
5.2 设计报告里通常被忽略的三块内容
报告骨架一般是摘要、绪论、需求分析、系统设计、数据库设计、功能实现、测试、总结。真正拉开差距的是三块:第一,ER 图要画对,Playlist-Song-Comment 的外键关系别画反;第二,测试部分要有真实数据截图,包括 admin 界面和接口返回 JSON 的证据;第三,异常处理要写,大屏接口在数据为空时返回什么、图表如何显示"暂无数据",比功能代码更值得写。
5.3 部署与打包:zip 里别装 venv 和缓存文件
宝塔部署 Django 的常见流程是:上传源码、在 Python 项目管理器创建虚拟环境、安装 requirements.txt、配置 gunicorn 和 Nginx 反向代理。打包交付压缩包之前检查下面几项:
| 检查项 | 原因 |
|---|---|
删除__pycache__和.pyc | 换机器部署时会干扰字节码缓存 |
| 不打包 venv | 虚拟环境路径写死,别人机器上用不了 |
requirements.txt用pip freeze生成 | 手动写依赖容易漏版本约束 |
| 数据库是否包含敏感数据 | 学习项目建议只留脱敏样本 |
gunicorn 启动命令通常是:
gunicorn project.wsgi:application -w 4 -b 127.0.0.1:8000-w 4表示 4 个 worker,服务器内存小于 1G 时改成 2 更稳妥。-b 127.0.0.1:8000只在本机监听,由 Nginx 从 80 端口转发进来。部署完成后,先curl http://127.0.0.1:8000/api/overview/验证 API 返回 JSON,再访问外网地址看大屏页面,这一步能把"代码能跑"和"部署成功"分开判断。
本文还有配套的精品资源,点击获取