news 2026/10/17 8:29:02

基于Django与MySQL的协同过滤电影推荐系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Django与MySQL的协同过滤电影推荐系统实战

简介:基于协同过滤的电影推荐系统毕业设计资源,适合计算机专业学生用于课程设计、毕业设计或推荐系统入门学习。系统完整实现了基于用户与基于物品两种协同过滤算法,后端使用Django框架搭配Python环境,支持MySQL与SQLite数据库切换,前端采用Bootstrap,并使用Movielens公开数据集完成训练与演示。压缩包共83个文件,大小约8.41MB,文件类型以Python源码、HTML模板、CSV评分数据为主,另含PDF论文、SQLite数据库、豆瓣爬虫脚本、依赖清单及详细readme文档,目录结构清晰,便于直接运行和二次开发。目前已有10610人学习下载,适合毕业设计参考及推荐系统实战练习。附带的爬虫和评分填充脚本可采集豆瓣电影数据、生成个性化输入,readme对运行环境、依赖安装、配置修改有详细说明,能帮助从零搭建可在线预览的电影推荐平台,并支持扩展算法比较或界面优化。

1. 为什么把“协同过滤电影推荐”落在 Django 2.2.1 上

A同学把毕设选题交上去时,某导师只问了一句:你打算怎么把它做出来?这句话问住了不少人。网上搜到的电影推荐系统,要么只讲算法不落代码,要么做了个网页但推荐逻辑是摆设。我建议他把“基于协同过滤的电影推荐系统”放在 python3.7 + django2.2.1 + mysql 这套固定组合上:算法用协同过滤,避开深度学习这种自己都讲不透的黑匣子;数据库用 MySQL 存用户、电影、评分三类数据;Django 负责把注册、打分、展示推荐结果的完整链路串起来。这套方案适合想在毕设或课设里真正跑通全流程的人——能运行、能演示、能讲清楚每一步,而不是交一堆没人看的代码。

2. 总体架构设计:Django 项目布局与四个核心模块的职责划分

2.1 MTV 分层与请求流转逻辑

Django 的 MTV(Model-Template-View)模式是这个项目的骨架。浏览器发起请求后,根路由 urls.py 先做分发,把地址交给对应应用的路由;路由再调用视图函数,视图里读写数据库、计算推荐算法,最后把结果塞给模板渲染成页面。链路不长,但模块边界一旦模糊,后期改任何功能都会连坐。

我搭这类项目时有个习惯:算法逻辑绝对不写在 views.py 里,单独放一个 algorithms.py。原因有两个,一是算法会被多个视图复用,比如推荐页用、电影详情页的“相似推荐”也用;二是答辩或给导师讲代码时,核心算法单独一个文件,远比“打开视图找一段缩进很深的循环”好讲得多。

2.2 四个核心模块的职责划分

按 Django 的 app 概念,把项目拆成 users、movies、ratings、recommend 四个应用,每个应用只管自己那一块。

应用核心功能涉及的模型/文件
users注册、登录、退出User 表、session
movies电影列表、详情、搜索Movie 表、模板渲染
ratings提交评分、我的评分记录Rating 表、表单处理
recommend相似度计算、推荐结果组装algorithms.py、推荐视图

划分原则是“谁的数据谁负责”。ratings 只负责评分的新增和查询,不做推荐;recommend 只读评分数据,不写数据。这样职责单一,出了问题直接定位到某个 app,不用在一个文件里上下翻。

2.3 项目目录结构与配置要点

movie_recommend/ ├── manage.py ├── movie_recommend/ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── users/ │ ├── models.py │ ├── views.py │ └── urls.py ├── movies/ │ ├── models.py │ ├── views.py │ └── urls.py ├── ratings/ │ ├── models.py │ ├── views.py │ └── urls.py └── recommend/ ├── algorithms.py ├── views.py └── urls.py

settings.py 里有两个配置最容易忽略。一是 INSTALLED_APPS 必须把四个 app 全部注册,漏掉一个就报No module named之类的错;二是数据库连接要写成 MySQL,不能保留默认的 sqlite:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'movie_db', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', } }

这段配置决定了 Django 的 orm 把模型映射到哪台数据库。NAME 对应 MySQL 里提前建好的库,如果没建库就直接 runserver,Django 只会报一个连接失败,不会帮你自动建库。通常的做法是先手动执行CREATE DATABASE movie_db DEFAULT CHARSET utf8mb4;再跑迁移,否则后续中文数据全是乱码。

3. MySQL 数据库设计:五张核心表与评分数据的建模方式

3.1 表结构设计与字段说明

这个项目不需要复杂的表,核心就三类信息:用户、电影、评分。按照“事实用行存”的原则,把每次打分作为一条记录放到评分表里,不要在一个用户字段里塞一长串电影 id,那是给内存用的,不是给关系型数据库用的。

表名字段类型说明
usersidINT 自增主键用户编号
usersusernameVARCHAR(50) 唯一登录名
userspasswordCHAR(64)存哈希值,不存明文
moviesidINT 自增主键电影编号
moviestitleVARCHAR(200)电影标题
moviesgenresVARCHAR(100)类型,多类型用竖线分隔
moviesrelease_yearSMALLINT上映年份
ratingsidINT 自增主键评分记录编号
ratingsuser_idINT 外键关联 users.id
ratingsmovie_idINT 外键关联 movies.id
ratingsscoreTINYINT评分值,范围 1–5
ratingscreated_atDATETIME评分时间

3.2 建表语句与约束设计

CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(64) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE movies ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, genres VARCHAR(100), release_year SMALLINT, poster_url VARCHAR(500) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE ratings ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, movie_id INT NOT NULL, score TINYINT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_movie (user_id, movie_id), CONSTRAINT fk_rating_user FOREIGN KEY (user_id) REFERENCES users(id), CONSTRAINT fk_rating_movie FOREIGN KEY (movie_id) REFERENCES movies(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有个细节值得在答辩时主动讲:UNIQUE KEY uk_user_movie是联合唯一约束,确保同一个用户对同一部电影只能有一条评分记录。用户反复点打分时,Django 侧可以先查再更新,也可以靠这个约束兜底,否则评分表会出现一条电影被同一个人刷 5 次的脏数据,最终污染相似度计算。

score 用 TINYINT 而不是 INT,是因为评分范围只有 1 到 5,TINYINT 占 1 字节,配合索引查询更快。外键约束保证了一点:删用户时评分记录不会变成孤儿数据。但要注意删用户的操作在 Web 端往往不开放,因为评分数据是推荐系统的原料,删了就没有可算的东西了。

3.3 公开数据集导入与数据校验

本地做测试时,通常的做法是导入公开电影数据集,比如 MovieLens 的 ml-latest-small 版本,里面大概有近万部电影和十万条左右评分,对毕设来说规模刚刚好。导入前先看原始 CSV 的表头,再写一次性脚本:

import csv import pymysql connection = pymysql.connect( host='127.0.0.1', user='root', password='your_password', database='movie_db', charset='utf8mb4' ) cursor = connection.cursor() with open('movies.csv', encoding='utf-8') as f: reader = csv.reader(f) next(reader) # 跳过表头 for row in reader: sql = "INSERT INTO movies (id, title, genres) VALUES (%s, %s, %s)" cursor.execute(sql, (int(row[0]), row[1], row[2])) connection.commit() cursor.close() connection.close()

为什么要保留原始数据集里的 id?因为 ratings.csv 里关联的 movie_id 和 movies.csv 是同一套编号。如果自己重新自增生成 id,评分数据的关联就全断了。导入完跑一条校验 SQL:SELECT COUNT(*) FROM ratings;,和原始数据行数做对比,差了就说明有重复键被跳过或者外键失败。

4. 协同过滤算法落地:余弦相似度与两种推荐策略的实现

4.1 从评分矩阵构建开始

协同过滤算法的输入是“用户-物品”评分矩阵。行是用户,列是电影,值是我们打的分数。矩阵在 Python 里的最直观做法是用 pandas 的透视表:

import pandas as pd import pymysql def load_rating_matrix(): connection = pymysql.connect( host='127.0.0.1', user='root', password='your_password', database='movie_db', charset='utf8mb4' ) sql = "SELECT user_id, movie_id, score FROM ratings" df = pd.read_sql(sql, connection) matrix = df.pivot_table( index='user_id', columns='movie_id', values='score' ) matrix = matrix.fillna(0) connection.close() return matrix

pivot_table会把缺失的评分变成 NaN,fillna(0)让矩阵变成稠密矩阵,方便算余弦相似度。这一步在设计上有个权衡:原始评分表是稀疏的,一个人一辈子看不了几千部电影,直接算矩阵会占内存。毕设级数据量这么干没问题,但要在答辩时说清楚“这是实验环境做法,生产环境会改用稀疏矩阵存储”。

4.2 基于用户的协同过滤(UserCF)

UserCF 的思路是“和你口味相似的人喜欢什么,你也大概率喜欢”。实现步骤分成三块:算用户间相似度、找最近邻、加权汇总候选电影得分。

import numpy as np from sklearn.metrics.pairwise import cosine_similarity def user_based_recommend(user_id, top_n=10, k=20): matrix = load_rating_matrix() # step1: 计算用户间余弦相似度 user_sim = cosine_similarity(matrix) user_sim_df = pd.DataFrame( user_sim, index=matrix.index, columns=matrix.index ) # step2: 取目标用户最相似的 k 个用户(排除自己) sim_users = user_sim_df.loc[user_id].sort_values( ascending=False ).iloc[1:k+1] # step3: 候选电影 = 邻居评过、目标用户没评过的,加权累计 user_rated = set(matrix.loc[user_id][matrix.loc[user_id] > 0].index) candidates = {} for other_user, sim in sim_users.items(): other_ratings = matrix.loc[other_user][matrix.loc[other_user] > 0] for movie_id, score in other_ratings.items(): if movie_id not in user_rated: candidates[movie_id] = candidates.get(movie_id, 0) + sim * score ranked = sorted(candidates.items(), key=lambda x: x[1], reverse=True) return ranked[:top_n]

注释里已经把三步标清楚了。k 是邻居数量,取 20 的意思是不管全局有多少用户,只参考最像的 20 个人。这个值不是越大越好,太大了容易混入相似度低的人,反而拉低推荐质量。候选电影的加权分直接用“相似度 × 邻居评分”累加,没有做归一化,因为我们要的是排序结果,不是精确的预测分值,不归一化不影响 top_n。

4.3 基于物品的协同过滤(ItemCF)

ItemCF 换了个角度:如果用户给《教父》打了高分,那么和《教父》最相似的《疤面煞星》也值得推荐。实现上把矩阵转置一下,让每行变成电影,每列变成用户:

def item_based_recommend(user_id, top_n=10): matrix = load_rating_matrix().T # 转置成 电影-用户 矩阵 item_sim = cosine_similarity(matrix) item_sim_df = pd.DataFrame( item_sim, index=matrix.index, columns=matrix.index ) user_rated = matrix[user_id][matrix[user_id] > 0] scores = {} for movie_id, score in user_rated.items(): # 与当前电影最相似的 10 部 sim_items = item_sim_df.loc[movie_id].sort_values( ascending=False ).iloc[1:11] for other_movie, sim in sim_items.items(): if other_movie not in user_rated: scores[other_movie] = scores.get(other_movie, 0) + sim * score ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return ranked[:top_n]

这段代码里的 “每部电影只看最相似的 10 部” 是一个实用主义剪枝。如果不剪枝,每次推荐要把矩阵里所有电影都遍历一遍,算得慢不说,低相似度的噪声还会混进来。ItemCF 在电影数量远小于用户数量时,性能通常比 UserCF 好,因为物品之间的相似度可以提前离线算好,用户访问推荐页时直接查结果。

4.4 混合策略与参数调整

两套算法单跑都有明显短板。UserCF 在用户量少时相似度不可靠,ItemCF 对冷门电影的覆盖不够。通常的做法是做一个简单的混合开关:默认走 ItemCF,当目标用户的评分数少于 5 条时走 UserCF,再少就直接给热门榜兜底。

def recommend_for_user(user_id, top_n=10): matrix = load_rating_matrix() user_rated_count = (matrix.loc[user_id] > 0).sum() if user_rated_count >= 5: return item_based_recommend(user_id, top_n) elif user_rated_count >= 1: return user_based_recommend(user_id, top_n) else: return hot_movies(top_n) # 按平均分和评分人数排序

这个分支逻辑在答辩时非常加分,因为老师最常问的就是“冷启动怎么办”。把冷启动策略写成代码,比嘴上说“我们用热门推荐兜底”有说服力得多。调参时,k、top_n、相似物品数量,建议在 settings.py 里定义成常量,方便后面做实验对比,不需要每次改代码里的魔法数字。

5. 常见问题排查:环境搭建、数据稀疏与冷启动的避坑经验

5.1 mysqlclient 编译失败:卡住毕设第一天的经典坑

现象:Windows 上执行pip install mysqlclient直接报错,末尾一行是error: Microsoft Visual C++ 14.0 is required。

原因:Python 3.7 时代 mysqlclient 经常没有现成的 wheel 包,需要本地编译,而编译依赖 VC++ 构建工具。很多同学在这一步就放弃了,误以为代码有问题,其实是环境问题。

解决:跑pip install pymysql,然后在 Django 项目的__init__.py里加两行:

import pymysql pymysql.install_as_MySQLdb()

pymysql 是纯 Python 实现,不需要编译,装完就能当 MySQLdb 用。这个手段适合项目快速落地,生产环境建议还是用 mysqlclient,但毕设阶段没必要在编译环境上耗时间。

5.2 中文乱码:建表时一个字符集决定成败

现象:电影标题里的中文在页面上显示成问号,或者直接变成一组乱码。检查 MySQL 命令行,发现数据本身存进去就是乱码。

原因:建库时没指定字符集,MySQL 用了默认的 latin1,存不了中文。等到发现再改,表里的数据已经坏了,删了重建又要重导一遍,属于典型的“前期偷懒后期翻车”。

解决:建库时执行CREATE DATABASE movie_db DEFAULT CHARSET utf8mb4;,同时把 Django 连接字符串里的 charset 也带上。命令行下还不行,就加一句SET NAMES utf8mb4;再操作。这套组合下来基本不会再看到乱码。

5.3 全量相似度计算内存溢出

现象:数据集导入后,跑item_based_recommend直接内存飙升,电脑风扇狂转,甚至进程被杀掉。

原因:余弦相似度对 n 部电影要生成 n×n 的相似度矩阵。电影数上万后,这个矩阵光存储就接近 GB 级,普通笔记本扛不住。

解决:不预先算全量矩阵,改成惰性计算,只算用户评分过的电影与其他电影的相似度,同时在算相似度时加上“只保留前 50 个近邻”的剪枝逻辑。这一步能把时间复杂度从 O(n²) 降到 O(评分过的电影数×候选电影数),数据量大的情况下效果立竿见影。

5.4 评分矩阵 0 填充带来的“伪相似”

现象:两个用户明明没有任何共同评分,但算法算出来的相似度不为 0,甚至偏高。推荐结果出现得莫名其妙。

原因:fillna(0)把“没看过”和“打了 0 分”混为一谈了。余弦相似度把 0 当成负向评分,于是没看过的电影全部变成 0,反而拉近了向量方向。

解决:评分体系里禁止出现 0 分,然后计算相似度时只统计双方都有评分的维度;或者对评分做中心化处理——每个用户减去自己的平均分,让 0 代表“用户的中性偏好”。我在项目里用的是“只统计共同评分的电影”的方式,因为实现最简单,讲起来也直白。

5.5 冷启动:新用户打开推荐页只有一个空列表

现象:新注册的用户没有任何评分,recommend_for_user走到最后返回空列表,页面一片空白,观感上像个 bug。

原因:评分矩阵里新用户的行全是 0,和任何老用户的相似度都低,取不到可靠邻居。

解决:按 4.4 的分支处理,评分数为 0 时走热门榜。热门榜不要只看平均分,否则一部电影只有一个人打了 5 分就会排到第一。我一般用加权分:平均分乘以评分人数占比,再加一个常数平滑,避免冷门电影刷榜。这个细节解释给导师听,对方立刻知道你懂推荐系统的常见陷阱。

6. 验收与答辩:演示路径设计与高频追问的应答准备

6.1 演示脚本:五分钟走完一条验收路径

答辩演示不要临时点来点去找功能,按固定脚本走,既省时间又不容易出事故。我推荐的五分钟路径是:

  1. 注册一个新账号,进入推荐页,截图当前的热门榜兜底推荐;
  2. 随便挑三部电影打 5 分,再刷一次推荐页,看结果是否变化;
  3. 打开 MySQL 的 ratings 表,展示刚才的三条评分记录存在,证明数据真实落库;
  4. 切换到“基于用户”和“基于物品”两个标签页,对比推荐列表的差异,顺带说一句各自原理。

这段脚本覆盖了 Web 基本功能、数据库、算法三个维度,每一项都是答辩时可能被追问的点,主动演示完,老师再提问就有了上下文。

6.2 高频追问与应答要点

老师提问应答要点
协同过滤原理是什么?用一句话讲明白:把用户的历史评分当成向量,用余弦距离找相似用户或相似物品,再按加权分排序取 top N
为什么不用深度学习?数据量不够,深度学习容易过拟合;且毕设强调可解释,协同过滤每一条推荐都能讲出依据
相似度为什么选余弦?余弦只关注向量方向,不受用户打分习惯影响,比如有人全场打 3 分、有人全场打 5 分,余弦能消除这种尺度偏差
怎么证明推荐有效?离线做留一法评估:每次拿掉用户的一条真实评分,看推荐列表前 10 里是否包含它,统计命中率

留一法可以在答辩前写一个轻量脚本跑出数值,不需要做完整评测集,取 100 个用户各留 1 条评分即可。就算精度只有百分之十几,也远比口说“效果不错”强,因为你有数字。

有一次验收前夜,我发现演示机上的 MySQL 还是 latin1,页面标题全是问号,整个推荐列表看起来像乱码。那次确实狼狈,改表重导数据搞到凌晨。从那以后,我每次拿到一个 Django 项目,部署完环境第一件事就是强制走一遍“注册、打分、推荐、查库”四步,不跑通不罢休。这条习惯帮我避掉了至少三次现场事故,希望帮到你。

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

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

跨平台SSL证书导入脚本:Windows与Linux自动化实践

简介:这份资源提供了一套跨平台的SSL证书导入脚本,面向需要在Windows与Linux服务器上部署HTTPS通信的运维人员及系统管理员。包内共2个文件,分别为Linux环境下的sh脚本与Windows环境下的bat脚本,压缩包体积约578B,轻量…

作者头像 李华
网站建设 2026/10/17 8:27:37

AI编码可控性:别让Agent既当运动员又当裁判

最近圈里玩vibecoding的人越来越多了,大家都有同一种体验:让coding agent从零写一个功能,它唰唰唰就出来了,跑起来好像也没啥大问题,可一旦往生产环境一放,各种想不到的破事就冒出来了。前一篇我聊过怎么从…

作者头像 李华
网站建设 2026/10/17 8:27:35

C语言嵌入式飞控源码深度拆解:传感器、姿态解算与PID控制实战

简介:这是一份面向嵌入式与无人机开发学习者的C语言开源项目源代码包,完整覆盖无人机硬件控制、传感器数据采集、飞行控制算法实现等核心环节,代码结构清晰、注释丰富,适合具备一定单片机基础、希望深入理解无人机系统设计及PID控…

作者头像 李华
网站建设 2026/10/17 8:27:27

开源Agent Skill实战:9个模块搭建全媒体自动化流水线

1. 一个人就是一支全媒体团队:这套玩法到底在解决什么问题内容创作者最头疼的事,从来不是“没想法”,而是想法太多、人手太少。写稿、配图、排版、分发、数据回收,每一个环节单拎出来都不难,但串在一起就是一条吞时间的…

作者头像 李华
网站建设 2026/10/17 8:26:01

设备运维进入主动服务时代:IoT的应用价值与管理逻辑

对于工业设备制造企业而言,设备交付并不是服务的终点。随着设备数量增长和安装范围扩大,企业不仅需要处理故障维修,还需要了解设备运行状态、开展定期维护,并持续积累服务记录。传统的设备售后通常以客户报修为起点,而…

作者头像 李华
网站建设 2026/10/17 8:25:59

Flask SSTI漏洞原理与Jinja2沙箱逃逸实战

1. 从一道CTF题看SSTI漏洞的底层逻辑与实战拆解“【攻防世界】十七 --- shrine --- SSTI”这个标题,对刚接触Web安全的朋友可能像一串密码:它既不是常规的登录绕过,也不是常见的SQL注入,而是一个典型的服务端模板注入(…

作者头像 李华