简介:这是一套基于Python与Django开发的网易云音乐数据可视化分析系统,面向计算机专业本科生、毕业设计学生及数据分析初学者,聚焦用户画像构建与播放行为深度分析两大核心问题,可直接用于毕设、课程设计或教学演示。资源包共54个文件,含24个核心Python源码(覆盖Scrapy爬虫、Django后端、ECharts前端集成)、4个CSV原始数据集、3份Markdown说明文档、1份PDF设计报告及7张PNG可视化图表,整体10.57MB,结构清晰、模块解耦,便于理解技术链路与二次开发。已有62人学习下载,资源提供完整可运行代码、详细设计文档、环境配置指南及远程技术支持,尤其适合希望从数据采集(热歌榜爬取)、MySQL存储、到Django大屏展示全流程实践的学习者,是少有的涵盖“爬—存—析—展”闭环的轻量级教学级项目。
1. 为什么用 Django 做音乐行为大屏,而不是 Flask 或 Streamlit?
这不是一个“Python 可视化项目”那么简单——它要解决的是:如何让一份毕设/课设在答辩现场,30 秒内让导师看懂你干了什么、数据从哪来、结论靠不靠谱。我带过三届某高校计算机系的课程设计指导,最常看到的翻车现场是:学生花两周爬完数据,用 Matplotlib 画了 8 张图,答辩时被问“这张热力图的横轴单位是什么?原始数据清洗逻辑在哪?”,当场卡壳。而这个网易云音乐可视化项目,从一开始就把“可解释性”和“可交付性”钉死在架构里:用 Scrapy 做强可控的定向爬虫(不是 requests + BeautifulSoup 那种玄学翻页),MySQL 存结构化行为日志(播放时间戳、歌单收藏动作、用户标签权重),Django 后端暴露 RESTful 接口供大屏调用(不是把所有逻辑塞进前端 JS),再用 ECharts 封装成响应式大屏页面。它不追求“实时千万级并发”,但保证每一行代码都能对应到毕业论文里的“第三章 系统设计”;它不堆砌 AI 模型,但用户画像模块用的是真实可复现的 RFM+标签聚类(最近一次播放、频率、总时长 + 歌单偏好关键词 TF-IDF),不是“根据喜好推荐”的黑匣子话术。适合两类人:一是需要交一份有完整数据链路、有文档、能跑通、能讲清技术选型理由的毕设/课设的学生;二是想快速搭一个带用户分群、行为路径、热度趋势三要素的分析原型的从业者。如果你的课题是“基于 Web 的音乐平台数据分析系统”,这份资源就是你答辩 PPT 里那张“系统架构图”的实体版本。
2. 总体架构设计:四层结构与五个核心模块的职责边界
这个项目不是“爬虫 + 画图”的缝合怪,而是按工业级数据产品思路拆解为四层:数据采集层 → 数据存储层 → 业务逻辑层 → 可视化呈现层。每层之间通过明确接口契约通信,避免后期改一个图表就全盘崩坏。下面直接给出我在某跨平台系统中实际落地的模块划分逻辑,和本项目完全对齐:
2.1 数据采集层:Scrapy 爬虫的可控性设计
项目里CloudMusic-Crawler目录下的爬虫不是简单抓热歌榜。它做了三件事:
- 反爬绕过策略收敛:网易云音乐对未登录用户限制榜单访问频次,爬虫通过
scrapy-rotating-proxies轮换 User-Agent 和 Referer(非 IP 代理),并在settings.py中配置DOWNLOAD_DELAY = 3+RANDOMIZE_DOWNLOAD_DELAY = True,模拟真实用户节奏; - 结构化字段提取:不只抓歌名、歌手,还解析
<span class="txt">里的播放量文本,用正则r'(\d+\.?\d*)\s*(万|亿)'统一转为数值型;歌单页额外提取“创建者 ID”“标签列表”“收藏数”,为后续用户画像打基础; - 增量更新机制:每次运行前检查 MySQL 中
song_list表的update_time字段,只爬取last_update < NOW() - INTERVAL 1 DAY的榜单,避免重复入库。
提示:
requirements.txt里scrapy==2.6.2是关键版本——新版 Scrapy 3.x 对CrawlSpider的 rule 解析逻辑有变更,会导致部分规则失效。若你本地环境是 Python 3.10+,务必先pip install scrapy==2.6.2再启动爬虫。
2.2 数据存储层:MySQL 表结构与字段语义设计
项目用 MySQL 而非 SQLite 或 CSV,是因为用户行为分析必须支持多表关联查询(比如“找出近7天收藏某类歌单的用户中,播放周杰伦歌曲频次最高的 Top10”)。以下是核心三张表的实际 DDL(来自设计报告.pdf第 4.2 节):
-- 用户基础表(非网易云真实账号,是爬虫生成的虚拟ID) CREATE TABLE `user_profile` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` VARCHAR(32) NOT NULL COMMENT '虚拟用户唯一标识', `gender` TINYINT DEFAULT 0 COMMENT '0:未知,1:男,2:女', `age_group` VARCHAR(10) DEFAULT 'unknown' COMMENT '年龄段分组', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 行为日志表(关键!记录每一次可量化动作) CREATE TABLE `user_behavior_log` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` VARCHAR(32) NOT NULL, `song_id` VARCHAR(32) NOT NULL, `behavior_type` ENUM('play','collect','share','comment') NOT NULL, `timestamp` DATETIME NOT NULL, `duration_sec` INT DEFAULT 0 COMMENT '播放时长(秒)', `song_popularity` INT DEFAULT 0 COMMENT '歌曲热度值(归一化后)', INDEX idx_user_time (`user_id`, `timestamp`), INDEX idx_song_time (`song_id`, `timestamp`) ); -- 歌单元数据表(支撑“歌单分析可视化系统”) CREATE TABLE `playlist_info` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `playlist_id` VARCHAR(32) NOT NULL, `title` VARCHAR(255) NOT NULL, `creator_id` VARCHAR(32) NOT NULL, `tag_list` JSON COMMENT 'JSON数组,如["古风","治愈"]', `collect_count` INT DEFAULT 0, `play_count` INT DEFAULT 0, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP );注意user_behavior_log表的两个复合索引:idx_user_time支撑“某用户近N天行为路径”查询,idx_song_time支撑“某歌曲在各时段播放热度”统计。没有这两个索引,Django Admin 里查一周数据可能卡顿 20 秒以上。
2.3 业务逻辑层:Django 的 ViewSet 与数据聚合策略
Django 不是用来写 HTML 的,而是做数据管道的编排中枢。项目中NeteaseCloudMusicDataAnalysis目录下的views.py定义了 5 个核心 API 端点,全部继承viewsets.ModelViewSet,但关键在get_queryset()的重写逻辑:
# api/views.py from django.db.models import Count, Avg, Sum, Q from rest_framework import viewsets from .models import UserBehaviorLog, UserProfile, PlaylistInfo class UserPortraitViewSet(viewsets.ModelViewSet): serializer_class = UserPortraitSerializer def get_queryset(self): # RFM 模型计算:Recency(最近播放)、Frequency(频次)、Monetary(播放时长总和) today = timezone.now().date() return UserProfile.objects.annotate( recency=Max('userbehaviorlog__timestamp', filter=Q(userbehaviorlog__behavior_type='play')), frequency=Count('userbehaviorlog', filter=Q(userbehaviorlog__behavior_type='play')), monetary=Sum('userbehaviorlog__duration_sec', filter=Q(userbehaviorlog__behavior_type='play')) ).filter( recency__gte=today - timedelta(days=7) # 近7天活跃用户 )这里没用 Pandas 做聚合,因为:1)Django ORM 的annotate+filter在百万级数据下比pandas.read_sql()快 3 倍以上;2)所有计算逻辑可直接映射到论文“第四章 数据分析方法”中的公式。UserPortraitSerializer则负责把recency转为“距今X天”,frequency分档为“高频/中频/低频”,这才是用户画像能落地的关键——不是输出一堆数字,而是输出可运营的标签。
2.4 可视化呈现层:ECharts 大屏的响应式适配方案
Django大屏目录下的templates/dashboard/index.html不是静态 HTML。它通过{% url 'api:user-portrait-list' %}动态获取 API 数据,并用 ECharts 的setOption()实现按需渲染。重点在于两点适配:
- 分辨率自适应:监听
window.onresize事件,但不用myChart.resize()简单粗暴调用,而是先clear()再setOption(option, {notMerge: true}),避免图表元素错位; - 数据懒加载:大屏首次加载只请求“今日播放热度 TOP10”和“用户地域分布”,点击“详情”按钮才触发
fetch('/api/behavior-trend/?days=30')加载 30 天趋势数据,防止首屏白屏超 3 秒。
项目里static/js/charts.js的initSongHeatMap()函数,用echarts.init(dom, null, {renderer: 'canvas'})显式指定 canvas 渲染器——这是为了兼容老旧浏览器,实测在 IE11 下仍能显示基础热力图(虽然不支持动画)。
3. 核心功能实现:用户画像建模与播放行为路径还原
用户画像不是给用户打“喜欢周杰伦”这种标签,而是构建可验证的行为特征向量。本项目采用RFM + 标签权重加权法,所有计算逻辑都在 Django Model 层完成,确保论文里“用户画像构建流程图”的每一步都有代码对应。
3.1 RFM 模型的 Django 实现与阈值设定
RFM 中的 M(Monetary)在音乐场景下不能直接用消费金额,而是用有效播放时长替代。项目定义“有效播放”为:duration_sec >= 60(播放满1分钟)。UserProfile模型中新增三个字段:
# models.py class UserProfile(models.Model): # ... 其他字段 r_score = models.SmallIntegerField(default=0) # Recency 得分(1-5) f_score = models.SmallIntegerField(default=0) # Frequency 得分(1-5) m_score = models.SmallIntegerField(default=0) # Monetary 得分(1-5) rfm_segment = models.CharField(max_length=20, default='other') # 最终分群标签 def calculate_rfm(self): """在 save() 前调用,更新 RFM 得分""" from django.db.models import Max, Count, Sum, Q from datetime import timedelta # R:最近一次播放距今天数(越小得分越高) last_play = self.userbehaviorlog_set.filter( behavior_type='play' ).aggregate(last=Max('timestamp'))['last'] if last_play: days_since = (timezone.now().date() - last_play.date()).days self.r_score = max(1, 5 - min(days_since // 3, 4)) # 0-2天=5分,3-5天=4分... # F:近30天播放次数 play_count = self.userbehaviorlog_set.filter( behavior_type='play', timestamp__gte=timezone.now() - timedelta(days=30) ).count() self.f_score = 1 if play_count == 0 else min(5, (play_count // 10) + 1) # 0次=1分,10-19次=2分... # M:近30天有效播放总时长(秒) total_duration = self.userbehaviorlog_set.filter( behavior_type='play', duration_sec__gte=60, timestamp__gte=timezone.now() - timedelta(days=30) ).aggregate(total=Sum('duration_sec'))['total'] or 0 self.m_score = 1 if total_duration == 0 else min(5, (total_duration // 3600) + 1) # 每小时=1分 # 综合分群:RFM 三维度组合(共125种,但项目只保留8类高价值分群) self.rfm_segment = self._get_segment_label() def _get_segment_label(self): """根据 RFM 得分返回业务可理解的标签""" if self.r_score >= 4 and self.f_score >= 4 and self.m_score >= 4: return 'high_value' elif self.r_score >= 4 and self.f_score < 3: return 'at_risk' elif self.r_score < 3 and self.f_score >= 4: return 'hibernating' # ... 其他逻辑(详见 design_report.pdf 第 5.3 节) return 'other'注意:
calculate_rfm()方法在UserProfile.save()时自动触发(通过pre_save信号或重写save()),确保每次用户行为更新后画像实时刷新。这比定时任务更精准,也避免答辩时被问“画像多久更新一次”。
3.2 播放行为路径的图谱化还原
行为路径不是简单画“播放→收藏→分享”流程图,而是用Neo4j 图数据库思维在 MySQL 中模拟。项目在user_behavior_log表上建立“行为序列”视图:
-- 创建行为序列视图(MySQL 5.7+) CREATE VIEW user_behavior_sequence AS SELECT a.user_id, a.song_id as current_song, b.song_id as next_song, a.timestamp as current_time, b.timestamp as next_time, TIMESTAMPDIFF(SECOND, a.timestamp, b.timestamp) as time_gap_sec FROM user_behavior_log a JOIN user_behavior_log b ON a.user_id = b.user_id AND a.timestamp < b.timestamp AND b.timestamp <= DATE_ADD(a.timestamp, INTERVAL 1 HOUR) WHERE a.behavior_type = 'play' AND b.behavior_type = 'play' ORDER BY a.user_id, a.timestamp;这个视图的意义在于:它把用户播放行为抽象为(user_id, current_song, next_song, time_gap)四元组,为后续分析“听周杰伦后 30 分钟内最常跳转到哪位歌手”提供原子数据。Django 中通过raw()查询调用该视图:
# analytics/utils.py def get_transition_matrix(song_id, hours=1): """获取某歌曲后的跳转概率矩阵""" with connection.cursor() as cursor: cursor.execute(""" SELECT next_song, COUNT(*) as cnt FROM user_behavior_sequence WHERE current_song = %s AND time_gap_sec <= %s*3600 GROUP BY next_song ORDER BY cnt DESC LIMIT 10 """, [song_id, hours]) return cursor.fetchall()这就是答辩时能说清的“行为路径分析方法”:不是靠感觉,而是用时间窗口约束的跳转频次统计。
3.3 歌单标签体系的 TF-IDF 构建
歌单标题和描述文本稀疏,直接用关键词匹配不准。项目用sklearn.feature_extraction.text.TfidfVectorizer构建轻量级标签模型:
# analytics/tfidf_builder.py from sklearn.feature_extraction.text import TfidfVectorizer import jieba def build_playlist_tfidf(): # 从 playlist_info 表读取所有歌单标题+标签字符串 playlists = PlaylistInfo.objects.values_list('title', 'tag_list') corpus = [] for title, tag_json in playlists: tags = json.loads(tag_json) if tag_json else [] # 拼接标题+标签,用结巴分词 text = title + ' ' + ' '.join(tags) words = jieba.lcut(text) corpus.append(' '.join(words)) # 构建 TF-IDF 矩阵(max_features=1000,避免维度爆炸) vectorizer = TfidfVectorizer(max_features=1000, ngram_range=(1,2)) tfidf_matrix = vectorizer.fit_transform(corpus) # 保存向量器和特征名,供在线预测用 joblib.dump(vectorizer, 'tfidf_vectorizer.pkl') return vectorizer.get_feature_names_out() # 使用示例:给新歌单打标签 def predict_playlist_tags(new_title, new_tags): vectorizer = joblib.load('tfidf_vectorizer.pkl') text = new_title + ' ' + ' '.join(new_tags) words = jieba.lcut(text) vec = vectorizer.transform([' '.join(words)]) # 返回 TF-IDF 值最高的前3个词 feature_array = vectorizer.get_feature_names_out() tfidf_sort = vec.toarray()[0].argsort()[-3:][::-1] return [feature_array[i] for i in tfidf_sort]这个模块的价值在于:它让“歌单分析可视化系统”里的“相似歌单推荐”有据可依——不是随机推荐,而是基于文本向量余弦相似度。design_report.pdf第 6.1 节的“标签体系构建流程图”,就是这段代码的图形化表达。
4. 关键避坑指南:部署与调试中最容易翻车的 4 个点
这套方案在某高校实验室部署过 12 次,学生踩过的坑我都记在本子上。以下 4 条是答辩前夜最高频的崩溃现场,按“现象→原因→解决”给出可立即执行的方案:
4.1 现象:Scrapy 爬虫运行后 MySQL 里只有空表,日志显示200 OK但无数据入库
原因:网易云音乐热歌榜 HTML 结构在 2023 年底更新,原爬虫 XPath//ul[@class="f-hide"]/li/a不再匹配新 DOM;同时settings.py中ROBOTSTXT_OBEY = True导致爬虫遵守 robots.txt 被拒。
解决:
- 用 Chrome 打开
https://music.163.com/#/discover/toplist?id=3778678,F12 查看新榜单<li>的 class 名(实测为j-flag); - 修改
spiders/toplist_spider.py中的parse()方法:
# 原代码(失效) # songs = response.xpath('//ul[@class="f-hide"]/li/a/@href').getall() # 新代码(2024年实测有效) songs = response.xpath('//div[@class="j-flag"]//a[contains(@href,"/song?id=")]/@href').getall()- 在
settings.py中设ROBOTSTXT_OBEY = False,并添加DEFAULT_REQUEST_HEADERS模拟浏览器:
DEFAULT_REQUEST_HEADERS = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }4.2 现象:Django 启动时报错django.core.exceptions.ImproperlyConfigured: Requested setting DATABASES, but settings are not configured
原因:.env文件未创建,或settings.py中DATABASES配置依赖os.getenv()读取环境变量失败;常见于 Windows 系统未安装python-decouple库。
解决:
- 在项目根目录创建
.env文件,内容为:
DB_NAME=cloudmusic_db DB_USER=root DB_PASSWORD=your_password DB_HOST=127.0.0.1 DB_PORT=3306- 确保
requirements.txt包含python-decouple==3.8,并执行pip install -r requirements.txt; - 检查
settings.py开头是否有:
from decouple import config DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': config('DB_NAME'), 'USER': config('DB_USER'), 'PASSWORD': config('DB_PASSWORD'), 'HOST': config('DB_HOST', default='127.0.0.1'), 'PORT': config('DB_PORT', default='3306'), } }4.3 现象:大屏页面加载后图表空白,浏览器控制台报Uncaught ReferenceError: echarts is not defined
原因:static/js/echarts.min.js文件路径错误,或index.html中<script>标签顺序不对(ECharts 必须在charts.js之前加载)。
解决:
- 确认
static/js/目录下存在echarts.min.js(项目包里是echarts@5.4.3版本); - 检查
templates/dashboard/index.html中 script 标签顺序:
<!-- 必须第一 --> <script src="{% static 'js/echarts.min.js' %}"></script> <!-- 必须第二 --> <script src="{% static 'js/charts.js' %}"></script> <!-- 不能颠倒! -->- 若用 Nginx 部署,确认
nginx.conf中location /static/指向正确路径,且alias末尾有/:
location /static/ { alias /path/to/your/project/static/; }4.4 现象:用户画像 API 返回空数据,但 MySQL 中user_behavior_log表有百万条记录
原因:Django 默认开启ATOMIC_REQUESTS,当user_behavior_log表数据量过大(>50 万行)时,annotate()查询会因锁表超时;同时UserProfile表未建立user_id索引,导致 JOIN 效率骤降。
解决:
- 在
models.py中为UserProfile.user_id添加数据库索引:
class UserProfile(models.Model): user_id = models.CharField(max_length=32, db_index=True) # 关键! # ... 其他字段- 运行
python manage.py makemigrations && python manage.py migrate生效; - 在
settings.py中调整数据库连接超时:
DATABASES = { 'default': { # ... 其他配置 'OPTIONS': { 'init_command': "SET SESSION wait_timeout=28800;", }, 'CONN_MAX_AGE': 600, # 连接池最大存活时间(秒) } }5. 毕业答辩实战:如何用 5 分钟讲清你的技术深度
答辩不是代码朗诵,而是用评委能听懂的语言,把技术决策背后的思考链展示出来。我带过的 A 同学去年用这个项目答辩,被问“为什么不用 Elasticsearch 做行为日志检索”,他没背概念,而是打开design_report.pdf第 3.2 节,指着一张对比表格说:“老师,我试过 ES,但发现两个问题:第一,我们日志写入是批量 INSERT,ES 的 bulk API 在千条/秒写入时 CPU 占用 90%,而 MySQL 批量插入稳定在 30%;第二,ES 的聚合查询结果无法像 Django ORM 那样直接映射到 RFM 模型的annotate()字段,得额外写一层转换逻辑——这会让论文‘第四章 系统实现’的代码截图失去连贯性。” 评委当场点头。这就是你要的效果。
5.1 答辩 PPT 的三页黄金结构
不要超过 12 页 PPT,核心是这三页:
| 页码 | 内容要点 | 设计逻辑 |
|---|---|---|
| 第 3 页:系统架构图 | 用 Visio 或 draw.io 重绘四层架构,在每层旁手写标注 1 个技术选型理由: - 采集层: Scrapy→ “Rule 语法天然支持多级页面跳转”- 存储层: MySQL→ “事务一致性保障 RFM 计算原子性”- 逻辑层: Django REST→ “Serializer 自动校验字段类型,避免前端传参错误”- 呈现层: ECharts→ “Canvas 渲染器兼容答辩教室老旧投影仪” | 评委扫一眼就知道你不是抄的,每个箭头都有思考 |
| 第 6 页:数据链路截图 | 截三张图拼成一行: 1. scrapy crawl toplist -o data.json控制台输出(证明爬到了)2. mysql> SELECT COUNT(*) FROM user_behavior_log;返回128432(证明存进了)3. http://localhost:8000/api/user-portrait/返回 JSON 中rfm_segment: "high_value"(证明算对了) | 用证据链代替口头承诺,比说一百遍“已实现”都管用 |
| 第 9 页:创新点提炼表 | 表格列:传统做法 / 本项目做法 / 优势 例: “用户画像标签” → “人工定义关键词” / “TF-IDF 自动提取歌单文本特征” / “标签覆盖率提升 37%(见附录测试报告)” | 把详细说明文档.md里的测试数据转化成对比语言 |
5.2 被问“你这个系统怎么保证数据真实性”的应答脚本
别慌,这个问题本质是问“你有没有伪造数据”。直接打开data_apply/README.md,指向其中一段:
“所有原始数据均来自网易云音乐公开榜单(URL: https://music.163.com/#/discover/toplist?id=3778678),爬虫日志已存档于
data/raw_logs/20240512_toplist.log。为验证数据有效性,我们抽样比对了 100 首歌曲的播放量:爬虫抓取值与网页显示值误差 ≤ 0.3%,符合学术研究容错范围。”
然后补充一句:“老师,如果您需要,我可以现场演示爬虫重新运行一次,从榜单抓取到入库再到大屏刷新,全程 3 分钟。” —— 这句话的杀伤力,远超背十页技术原理。
5.3 答辩前必做的三件事清单
- 环境复位测试:在一台全新安装的 Windows 10 虚拟机中,从
requirements.txt开始重装所有依赖,执行python manage.py runserver,确认大屏能打开、API 能返回数据、爬虫能成功入库。这是防止答辩当天因环境差异翻车的后悔药。 - 文档交叉核对:打开
设计报告.pdf,找到“第五章 系统测试”中的测试用例表,随机挑 3 个,用 Postman 调用对应 API,截图保存。例如测试用例 TC-07:“输入用户 ID,返回其近 7 天播放歌曲 TOP5”,你就调GET /api/user-behavior/?user_id=U12345&days=7,截图响应体。 - 代码注释强化:在
views.py的UserPortraitViewSet.get_queryset()方法上方,加一段中文注释:
# 【答辩重点】RFM 模型实现说明: # R(Recency):取 user_behavior_log 表中 behavior_type='play' 的最大 timestamp # F(Frequency):统计近30天 behavior_type='play' 的记录数 # M(Monetary):求和近30天 duration_sec>=60 的记录 duration_sec 字段 # 评分标准见 design_report.pdf 第 5.2 节表 5-1这样评委翻你代码时,一眼就能定位到核心逻辑,不会在几百行里迷失。
从那以后我每次带学生做毕设,都强制他们在答辩前一周,把这三件事做完并邮件发给我确认。不是怕他们做不好,而是怕他们把时间花在美化 PPT 上,却忘了最该加固的是数据链路的真实性。希望帮到你。
本文还有配套的精品资源,点击获取