news 2026/9/16 21:51:51

Django毕业设计:从网易云数据清洗到可视化大屏全链路实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django毕业设计:从网易云数据清洗到可视化大屏全链路实践

简介:面向计算机相关专业毕业设计和课程设计的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"

playlistsong两个外键的related_name一定要写。大屏接口里最常出现的查询是"取某歌单下播放量 Top 10",没有related_name只能写song_set,一旦嵌套两层以上可读性断崖下降。db_index=True加在song_nameplay_countposted_at上,这三个字段是高频过滤和排序条件,走索引能避免全表扫描。on_delete=models.CASCADE是刻意选的,评论跟着歌曲删、歌曲跟着歌单删,演示项目不需要纠结数据保留问题。建好模型后执行makemigrationsmigrate生成表结构。

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。空表上SumAvg返回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")让数据按时间升序排列,画折线图时不需要前端再排序。我把字段名直接转成了xy,省掉前端一层 map。

操作返回类型空表行为
count()int0
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等比缩放,两边会留白,但图表和文字不会变形。如果要求完全铺满,可以改成scaleXscaleY分开算,代价是图形被压扁,不推荐。

适配方式优点缺点
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.txtpip 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,再访问外网地址看大屏页面,这一步能把"代码能跑"和"部署成功"分开判断。

本文还有配套的精品资源,点击获取

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

同一首歌换6副耳机听感差异有多大?五维实测解析

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

作者头像 李华
网站建设 2026/9/16 21:47:11

浏览器里的AI视频剪辑管线:WebAssembly+WebGPU端侧推理实战

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

作者头像 李华
网站建设 2026/9/16 21:46:53

解决Oracle用户crontab的PAM configuration鉴权报错

凌晨两点被电话叫醒&#xff0c;值班的兄弟说Oracle的备份任务连续两晚没跑&#xff0c;登上服务器一看&#xff0c;oracle用户执行crontab -l直接甩出一行报错&#xff1a;You (oracle) are not allowed to access to (crontab) because of pam configuration。这不是Oracle自…

作者头像 李华
网站建设 2026/9/16 21:46:42

SSM文物管理系统实战:动态SQL、事务边界与MySQL优化

简介&#xff1a;本资源是一套基于SSM&#xff08;SpringSpringMVCMyBatis&#xff09;框架开发的B/S架构文物管理系统&#xff0c;面向Java Web初学者与课程设计实践者&#xff0c;解决中小型文博单位或高校实训中文物信息数字化管理、用户分权操作及交互式内容展示等核心需求…

作者头像 李华