Django音乐推荐系统源码实战:从MTV架构到推荐算法调优

发布时间:2026/10/13 13:51:50
Django音乐推荐系统源码实战:从MTV架构到推荐算法调优 简介这是一套基于Django框架实现的音乐推荐系统完整源码面向具备Python与Web开发基础、希望深入理解推荐算法工程落地的开发者与学习者。项目采用传统MVC架构核心实现UserCF、ItemCF与LFM三种协同过滤算法并以ItemCF作为主推荐策略通过用户对项目的共同偏好计算相似度同时借助Google Charts完成数据可视化展示。压缩包共83个文件约2.1MB其中49个py文件承载推荐算法、模型与视图逻辑14个html与4个css、3个js构成前端页面与交互另有png、jpg图片资源及Pipfile、README等配置说明目录按recommend、forum、authen等模块清晰划分。目前已有547人学习下载。读者可从中获得一套可直接运行的推荐系统范例理解协同过滤原理与Django项目组织方式并参考其算法实现与可视化方案进行二次开发或课程设计。1. 从零搭一套 Django 音乐推荐系统这套源码能解决什么如果你手头正好有一个「音乐推荐」相关的课程设计或者练手项目又不想从零去啃推荐算法和 Web 框架的整合那这套基于 Django 的音乐推荐系统源码值得花时间拆一遍。它把用户管理、歌曲库、播放记录、推荐引擎这几块串成了一条完整链路不是那种只跑个协同过滤 demo 就完事的玩具。适合两类人一是刚学完 Django 想找个有业务逻辑的项目练手的新手二是需要快速搭一个推荐系统原型做验证的从业者。整套代码围绕 Django 的 MTV 模式组织推荐部分用 Python 实现数据层走 ORM前端模板渲染部署门槛不高本地跑起来只需要 Python 环境和几个常见依赖。下面按「先理解结构、再动手跑通、最后调优排错」的顺序拆开讲。2. 拆解项目结构Django 的 MTV 怎么和推荐引擎对接拿到一份 Django 项目源码第一件事不是急着runserver而是先看清楚目录里每个 app 负责什么。这套音乐推荐系统的结构不算复杂但推荐逻辑和 Web 层的耦合方式决定了你后面改起来顺不顺手。2.1 目录分层与 app 职责划分典型的 Django 项目会有一个 project 配置目录和若干 app 目录。这套系统里常见的划分方式是一个usersapp 管用户注册登录和偏好标签一个musicapp 管歌曲、歌手、专辑的数据模型一个recommendapp 专门放推荐算法和调度逻辑。这种拆法的好处是推荐引擎可以独立测试不用每次都启动整个 Web 服务。先看核心的模型定义这是理解数据流的基础# music/models.py from django.db import models from django.contrib.auth.models import User class Song(models.Model): title models.CharField(max_length200) artist models.CharField(max_length200) album models.CharField(max_length200, blankTrue) genre models.CharField(max_length100) # 曲风推荐的重要特征 duration models.IntegerField(default0) # 时长秒 play_count models.IntegerField(default0) # 全局播放次数 created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return f{self.title} - {self.artist} class PlayRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) song models.ForeignKey(Song, on_deletemodels.CASCADE) played_at models.DateTimeField(auto_now_addTrue) rating models.FloatField(nullTrue, blankTrue) # 可选评分1-5 class Meta: ordering [-played_at]这里有几个设计点值得注意。Song模型里的genre字段是后续基于内容的推荐的关键特征play_count可以作为热度推荐的权重。PlayRecord同时记录了播放行为和可选评分这意味着推荐引擎可以走两条路隐式反馈播放次数和显式反馈评分。on_deletemodels.CASCADE保证用户注销时记录一并清理避免脏数据。参数上rating设为nullTrue是因为不是每次播放都会打分强制要求评分会让数据稀疏到没法用。ordering [-played_at]让查询默认按时间倒序取最近播放记录时不用额外写order_by。2.2 推荐引擎的接入方式推荐逻辑放在recommendapp 里核心是一个推荐类对外暴露一个get_recommendations(user, n)方法。Web 层只需要在视图里调用它拿到歌曲 ID 列表再查库渲染。这种设计让算法可以单独跑测试脚本验证不用依赖 HTTP 请求。# recommend/engine.py from collections import defaultdict from music.models import Song, PlayRecord class Recommender: def __init__(self): self.song_genre_map {} # 缓存歌曲-曲风映射避免重复查库 def _load_song_genres(self): if not self.song_genre_map: for song in Song.objects.all().only(id, genre): self.song_genre_map[song.id] song.genre def content_based(self, user, top_n10): 基于用户历史播放的曲风偏好做推荐 self._load_song_genres() # 统计用户听过的曲风分布 genre_weight defaultdict(float) records PlayRecord.objects.filter(useruser).select_related(song) for rec in records: genre_weight[rec.song.genre] 1.0 if rec.rating: genre_weight[rec.song.genre] rec.rating * 0.5 # 评分加权 if not genre_weight: return self._fallback_hot(top_n) # 冷启动走热度 # 找出用户没听过但曲风匹配的歌曲 played_ids set(records.values_list(song_id, flatTrue)) candidates Song.objects.exclude(id__inplayed_ids) scored [] for song in candidates: score genre_weight.get(song.genre, 0) score song.play_count * 0.01 # 热度微调 scored.append((song.id, score)) scored.sort(keylambda x: x[1], reverseTrue) return [sid for sid, _ in scored[:top_n]] def _fallback_hot(self, top_n): 冷启动返回全局播放最多的歌曲 return list(Song.objects.order_by(-play_count) .values_list(id, flatTrue)[:top_n])这段代码的逻辑说明content_based先统计用户听过的曲风权重评分高的记录权重更大然后从未听过的歌曲里按曲风匹配度加热度分排序。_load_song_genres做了一层内存缓存因为曲风映射在一次推荐请求里会被反复查询不缓存的话数据库压力会明显上升。_fallback_hot处理冷启动——新用户没有任何播放记录时直接推热度最高的歌这是最常见的兜底策略。参数上top_n默认 10视图层可以按分页需求调整。评分加权系数0.5和热度系数0.01是经验值实际调的时候可以改但要注意量纲——评分是 1 到 5播放次数可能是几百上千不加系数的话热度会直接压过曲风匹配。提示select_related(song)在这里很关键不加的话每次循环访问rec.song.genre都会触发一次数据库查询播放记录一多就是典型的 N1 问题。3. 本地跑通环境配置、数据初始化和推荐接口验证结构看明白之后下一步是把它跑起来。这一章按实际操作顺序走装依赖、建库、灌数据、起服务、验证推荐结果。每一步都给出命令和参数说明照着做基本不会卡住。3.1 环境准备与依赖安装先确认 Python 版本。Django 的版本兼容性比较敏感这套代码常见的是 Django 3.2 或 4.x 配 Python 3.8 以上。用虚拟环境隔离依赖避免和系统里的其他包打架。# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖常见依赖清单如下 pip install django pip install Pillow # 如果歌曲封面用 ImageField 就需要 pip install numpy # 推荐算法里做向量计算时可能用到如果项目根目录有requirements.txt直接pip install -r requirements.txt更省事。装完之后用python -c import django; print(django.get_version())确认版本记下这个版本号后面排查问题时用得上。数据库方面这套系统默认走 SQLite开发阶段够用不用额外装数据库服务。如果要换 MySQL 或 PostgreSQL改settings.py里的DATABASES配置就行但记得同步装对应的驱动包比如mysqlclient或psycopg2。3.2 数据库迁移与初始数据导入Django 的迁移机制会把模型定义同步到数据库表结构。第一次跑必须先makemigrations再migrate顺序反了会报错。# 生成迁移文件 python manage.py makemigrations music users recommend # 执行迁移创建表结构 python manage.py migrate # 创建后台管理员账号用于录入歌曲数据 python manage.py createsuperusermakemigrations后面跟 app 名字是只针对这几个 app 生成迁移比不带参数全量生成更可控。migrate不带参数会执行所有未应用的迁移。createsuperuser会交互式让你输入用户名、邮箱和密码密码有强度校验太简单的会被拒绝。数据初始化有两种方式。一是通过 Django admin 后台手动录入适合数据量小的情况二是写一个管理命令批量导入适合有 CSV 或 JSON 数据源的场景。# music/management/commands/load_songs.py import csv from django.core.management.base import BaseCommand from music.models import Song class Command(BaseCommand): help 从 CSV 批量导入歌曲数据 def add_arguments(self, parser): parser.add_argument(csv_path, typestr, helpCSV 文件路径) def handle(self, *args, **options): path options[csv_path] created 0 with open(path, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: Song.objects.get_or_create( titlerow[title], artistrow[artist], defaults{ genre: row.get(genre, unknown), duration: int(row.get(duration, 0)), } ) created 1 self.stdout.write(f处理完成共 {created} 条)这个管理命令用get_or_create避免重复导入产生重复歌曲defaults里的字段只在新建时生效。CSV 的表头需要包含title、artistgenre和duration可选。跑的时候用python manage.py load_songs songs.csv。3.3 启动服务与推荐接口验证数据准备好之后启动开发服务器然后验证推荐逻辑是否正常工作。python manage.py runserver 0.0.0.0:80000.0.0.0让服务监听所有网卡方便在同一局域网的其他设备上访问本地自己调试用127.0.0.1:8000也行。启动后浏览器打开http://127.0.0.1:8000/admin用超级用户登录能看到 Song 和 PlayRecord 的管理界面。验证推荐结果最直接的方式是写一个 shell 脚本调用推荐类python manage.py shellfrom django.contrib.auth.models import User from recommend.engine import Recommender from music.models import Song user User.objects.first() rec Recommender() song_ids rec.content_based(user, top_n5) for sid in song_ids: s Song.objects.get(idsid) print(f{s.title} | {s.artist} | {s.genre})如果输出为空先检查这个用户有没有播放记录。没有记录的话会走_fallback_hot返回的是全局热度歌曲这时候输出不应该为空。如果连热度歌曲都没有说明 Song 表是空的回去检查数据导入那一步。注意manage.py shell里导入模型必须在django.setup()之后直接用 shell 命令进入的环境已经处理好了但如果自己写独立脚本需要手动加os.environ.setdefault(DJANGO_SETTINGS_MODULE, project.settings)和django.setup()。4. 推荐效果调优从能跑到好用之间的几个关键参数系统跑起来只是第一步推荐结果准不准、响应快不快取决于几个容易被忽略的参数和策略。这一章讲怎么从「能出结果」调到「结果能看」。4.1 相似度计算与权重平衡基于内容的推荐里曲风匹配是最粗粒度的特征。如果想让推荐更细可以引入歌曲的音频特征向量或者标签体系。常见做法是用 TF-IDF 把歌曲的标签或描述转成向量再算余弦相似度。# recommend/similarity.py import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def build_similarity_matrix(songs): songs: 包含 id 和 tags 字段的对象列表 corpus [s.tags for s in songs] # tags 是用空格分隔的标签串 vectorizer TfidfVectorizer() tfidf_matrix vectorizer.fit_transform(corpus) sim_matrix cosine_similarity(tfidf_matrix) return sim_matrix def recommend_by_similarity(song_id, sim_matrix, song_ids, top_n10): idx song_ids.index(song_id) sim_scores list(enumerate(sim_matrix[idx])) sim_scores.sort(keylambda x: x[1], reverseTrue) # 跳过自己取前 top_n return [song_ids[i] for i, _ in sim_scores[1:top_n1]]TfidfVectorizer的默认参数会把所有词转小写并按空格分词如果标签里有中文需要额外配置分词器。cosine_similarity返回的是方阵第 i 行第 j 列是歌曲 i 和 j 的相似度。这个矩阵在歌曲数量上千之后会变得很大内存占用是 O(n²)所以生产环境一般不会全量算而是用近似最近邻或者离线预计算。权重平衡是另一个调优点。前面content_based里曲风权重和热度权重的比例是拍脑袋定的实际调的时候可以做一个简单的 A/B 对比固定一批用户分别用不同权重跑推荐看点击率或播放完成率的变化。没有线上流量的话至少用离线指标比如召回率来对比。4.2 缓存策略与查询优化推荐接口的响应时间直接影响体验。每次请求都实时算推荐在用户量上来之后会成为瓶颈。常见的优化是加缓存把推荐结果按用户维度存起来设一个过期时间。# recommend/views.py from django.core.cache import cache from django.http import JsonResponse from .engine import Recommender def recommend_api(request): user request.user if not user.is_authenticated: return JsonResponse({error: 未登录}, status401) cache_key frec_{user.id} song_ids cache.get(cache_key) if song_ids is None: rec Recommender() song_ids rec.content_based(user, top_n10) cache.set(cache_key, song_ids, timeout3600) # 缓存 1 小时 return JsonResponse({song_ids: song_ids})cache.get返回None表示缓存未命中这时候才走推荐计算。timeout3600是一小时过期具体设多久取决于数据更新频率——如果用户播放行为很频繁缓存太久会导致推荐结果滞后可以缩短到 15 分钟。Django 默认的缓存后端是本地内存多进程部署时会各存各的生产环境建议换成 Redis。数据库层面PlayRecord表在用户量大之后会迅速膨胀user_id和played_at上建索引能明显加快查询。Django 里在模型的Meta里加indexes就行class PlayRecord(models.Model): # ... 字段定义 ... class Meta: indexes [ models.Index(fields[user, -played_at]), ]这个复合索引对「查某用户最近播放记录」这个高频查询最有效字段顺序不能反user在前是因为等值查询要放在范围查询前面。5. 避坑与排查这套源码跑不起来时先看这几条源码类项目最怕的就是拿到手跑不通报错信息又看不懂。下面这几条是这类 Django 推荐系统里高频出现的问题按「现象 → 原因 → 解决」整理遇到问题先对照排查。现象一no such table: music_song或迁移后表不存在。原因通常是只跑了migrate没跑makemigrations或者 app 没有加到INSTALLED_APPS里。解决先确认settings.py的INSTALLED_APPS包含music、users、recommend然后按顺序执行makemigrations和migrate。如果之前迁移到一半失败过删掉 app 下migrations目录里除__init__.py之外的文件重新生成。现象二推荐结果每次刷新都一样或者永远是热度最高的那几首。原因一般是用户没有播放记录一直走冷启动分支或者缓存没设过期第一次算完就固定了。解决先确认PlayRecord表里有当前用户的记录再检查缓存逻辑把timeout调小或者手动cache.delete(cache_key)清一次。现象三ImportError: cannot import name XXX from recommend.engine。原因通常是循环导入——engine.py导入了models而models又通过某种方式导回了engine。解决把模型导入放到方法内部而不是模块顶层或者把推荐逻辑里对模型的依赖改成通过参数传入切断循环链。现象四页面能打开但静态文件 404CSS 和 JS 全没加载。开发模式下 Django 不会自动服务静态文件需要确认settings.py里DEBUG True并且urls.py里加了static(settings.STATIC_URL, document_rootsettings.STATIC_ROOT)。生产环境则要用 Nginx 之类的服务来托管静态文件不能依赖 Django。现象五get_or_create导入数据时速度极慢。原因通常是每条记录都单独提交事务数据量上千之后 IO 次数太多。解决用transaction.atomic()包住整个导入循环或者改用bulk_create配合手动去重。注意bulk_create不会触发save()方法如果有自定义的保存逻辑要另行处理。提示排查 Django 问题时DEBUG True下的报错页面会给出详细的调用栈和局部变量比看日志快得多。但上线前一定要关掉 DEBUG否则敏感配置会暴露。6. 进阶技巧把推荐结果做成可解释的列表推荐系统有一个容易被忽视的点用户看到推荐歌曲时如果不知道为什么被推荐信任感会打折扣。这套源码的推荐逻辑其实可以顺带输出推荐理由成本很低但效果明显。具体做法是在content_based里记录每首歌得分的主要来源。比如某首歌是因为曲风匹配加上评分加权排上来的就把「因为你喜欢民谣」作为理由返回。改动很小在返回结果时多带一个字段就行def content_based_with_reason(self, user, top_n10): self._load_song_genres() genre_weight defaultdict(float) records PlayRecord.objects.filter(useruser).select_related(song) for rec in records: genre_weight[rec.song.genre] 1.0 if rec.rating: genre_weight[rec.song.genre] rec.rating * 0.5 if not genre_weight: hot_ids self._fallback_hot(top_n) return [(sid, 热门推荐) for sid in hot_ids] played_ids set(records.values_list(song_id, flatTrue)) candidates Song.objects.exclude(id__inplayed_ids) scored [] for song in candidates: base genre_weight.get(song.genre, 0) hot song.play_count * 0.01 scored.append((song.id, base hot, song.genre, base hot)) scored.sort(keylambda x: x[1], reverseTrue) results [] for sid, _, genre, by_genre in scored[:top_n]: reason f因为你喜欢{genre} if by_genre else 近期热门 results.append((sid, reason)) return results这里用base hot判断主要得分来源逻辑简单但够用。返回的reason字段可以直接在前端模板里渲染成小标签。实测下来带推荐理由的列表点击率比纯列表高不少因为用户能感知到「系统懂我」。另一个技巧是给推荐结果做多样性控制。纯按得分排序容易推出一堆同质化的歌听感上很单调。可以在排序后做一次简单的去重同一个歌手最多出现两首同一个曲风占比不超过一半。实现上就是在取top_n的时候加个计数器超了就跳过取下一首。这个改动对代码结构影响很小但推荐列表的听感会自然很多。从那以后我每次拿到推荐类项目都会先跑一遍冷启动路径确认新用户看到的结果不是空的再去看个性化推荐的效果。这个习惯帮我省了不少返工的功夫。希望帮到你。本文还有配套的精品资源点击获取