简介:这份资源面向计算机专业毕业设计学生与推荐系统入门者,提供一套可运行的电影推荐系统完整实现。系统基于Django2.2.1与Python3.7搭建,后端可选用MySQL或SQLite,前端采用Bootstrap3,同时实现了基于用户和基于物品两种协同过滤算法,并配套MovieLens数据集,支持在线预览效果。压缩包共83个文件,约8.41MB,以Python源码与编译文件为主体,辅以HTML模板、CSV数据集、数据库文件及图片资源,另含论文PDF、readme说明与爬虫脚本,结构完整便于按模块查阅。目前已有10613人学习下载,热度较高。读者可获得可直接部署运行的工程代码、数据库文件、详细技术文档与论文参考,并借助爬虫脚本与数据填充脚本理解数据流转过程,适合作为毕设模板或协同过滤算法的实践案例。资源链接:https://download.csdn.net/download/fanjialiang2401/12363909
1. 从一份"跑不起来"的毕设源码说起
去年帮人看一份毕业设计,压缩包解压后目录里躺着manage.py、几个 app 文件夹和一份requirements.txt,README 只有一行"请先建库再迁移"。对方说在自己机器上python manage.py runserver直接报错,数据库连不上,推荐结果永远是空列表。这类基于协同过滤的电影推荐系统,技术栈是 python3.7 + django2.2.1 + mysql,功能上要完成用户注册登录、电影浏览检索、评分行为采集、基于用户和基于物品的协同过滤推荐、后台数据管理这几件事。它适合两类人:一是要交毕设、需要一套能演示完整链路的同学;二是想拿一个真实 Django 项目练手、把推荐算法从公式落到数据库查询的开发者。下面按"它是什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改"的顺序拆开讲,中间会穿插我踩过的坑。
2. 项目骨架与运行链路:从 manage.py 到推荐结果页
2.1 目录结构与各 app 职责
这类项目的目录结构高度相似,拿到源码先别急着运行,先认清每个文件夹在干什么。典型布局如下:
movie_recommend/ ├── manage.py # Django 入口,所有命令都从它走 ├── requirements.txt # 依赖清单,注意版本锁定 ├── movie_recommend/ # 项目配置包 │ ├── settings.py # 数据库、静态文件、时区、注册 app │ ├── urls.py # 根路由,include 各 app 的 urls │ └── wsgi.py ├── users/ # 用户模块:注册、登录、资料 │ ├── models.py # 继承 AbstractUser 或独立 Profile │ ├── views.py │ └── urls.py ├── movies/ # 电影模块:列表、详情、分类、搜索 │ ├── models.py # Movie、Category、Rating │ ├── views.py │ └── templates/ ├── recommend/ # 推荐模块:算法实现与结果页 │ ├── cf.py # 协同过滤核心计算 │ ├── views.py │ └── management/commands/ # 常见做法:用自定义命令离线算相似度 └── static/ # css/js/图片users管身份,movies管内容与评分数据,recommend只负责算。这个划分很关键:推荐算法依赖的是Rating表里的用户-电影-分数三元组,只要这张表结构稳定,算法层可以随便换实现,不影响前端。我一般会先打开movies/models.py确认评分表长什么样,因为整条推荐链路的数据源头都在这里。
2.2 环境准备与依赖安装
python3.7 是个有年代感的版本,直接pip install django会装到 4.x,跟 django2.2.1 的 API 不兼容,url()写法、django.conf.urls的导入路径都会翻车。所以第一步是把版本钉死:
# 建议用 conda 或 pyenv 单独建 3.7 环境,别污染系统 python conda create -n movie python=3.7 -y conda activate movie # 按项目实际依赖装,版本必须锁 pip install django==2.2.1 pip install mysqlclient==1.4.6 # 连 mysql 用,Windows 下编译失败可换 pymysql pip install numpy # 协同过滤算相似度常用mysqlclient在 Windows 上经常因为缺少编译工具报错,血泪经验是:要么装好 Visual C++ Build Tools,要么在movie_recommend/__init__.py里加两行用pymysql顶替:
import pymysql pymysql.install_as_MySQLdb()这样 Django 的 mysql 后端会走 pymysql,纯 Python 实现,不用编译。代价是性能略低,但对毕设数据量完全够用。
2.3 数据库配置与迁移
打开settings.py,找到DATABASES,改成自己的 mysql 连接信息。注意 django2.2 要求 mysql 5.7 及以上,且建库时字符集用utf8mb4,否则电影名里的特殊字符会存成问号:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'movie_db', # 先在 mysql 里 CREATE DATABASE movie_db 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }建库命令:
CREATE DATABASE movie_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后依次执行迁移和建管理员:
python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runservermakemigrations生成迁移文件,migrate真正建表。如果这一步报Unknown database,就是库没建;报Access denied,是账号密码或权限问题;报字段长度超限,多半是 mysql 版本太低或字符集不对。跑通后访问127.0.0.1:8000/admin能登录,说明数据库链路通了。
2.4 数据初始化与推荐页验证
空库是没法推荐任何东西的,协同过滤需要评分数据。常见做法是项目里带一个init_data.py或 fixture 文件,或者用 Django 自定义命令灌数据:
# 方式一:fixture python manage.py loaddata movies/fixtures/movies.json # 方式二:自定义命令(项目里 recommend/management/commands/ 下) python manage.py init_movies灌完数据后,用两个不同账号分别给几部电影打分,再访问推荐页。如果推荐列表还是空,先查Rating表有没有数据,再看算法里相似度阈值是不是设得太高(比如要求共同评分电影数 ≥ 5,但测试数据只有 3 条)。这一步是验证整条链路是否打通的最快方式,比读代码快得多。
3. 协同过滤核心实现:用户-物品评分矩阵怎么算出来
3.1 数据模型:Rating 表是整个系统的地基
推荐算法的一切输入都来自评分表。movies/models.py里通常这样定义:
from django.db import models from django.contrib.auth.models import User class Movie(models.Model): title = models.CharField(max_length=200, db_index=True) category = models.CharField(max_length=50, blank=True) avg_rating = models.FloatField(default=0) # 冗余字段,列表页排序用 rating_count = models.IntegerField(default=0) class Rating(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) movie = models.ForeignKey(Movie, on_delete=models.CASCADE) score = models.FloatField() # 1~5 分 created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'movie') # 一个用户对一部电影只能打一次分 indexes = [models.Index(fields=['user', 'movie'])]unique_together是必须的,否则同一用户重复打分会让相似度计算失真。indexes那行是我一般会加的,因为协同过滤要频繁按 user 和 movie 查评分,没索引在几千条数据时就开始明显变慢。avg_rating和rating_count属于冗余字段,每次打分后更新,避免列表页每次都做聚合查询。
3.2 基于用户的协同过滤:找相似用户再推电影
核心思路是:找到和目标用户评分习惯最像的一批用户,把他们喜欢但目标用户没看过的电影推过来。相似度用余弦相似度,代码落在recommend/cf.py:
import numpy as np from movies.models import Rating, Movie def build_user_movie_matrix(): """把 Rating 表转成 {user_id: {movie_id: score}} 的稀疏字典""" matrix = {} for r in Rating.objects.values('user_id', 'movie_id', 'score'): matrix.setdefault(r['user_id'], {})[r['movie_id']] = r['score'] return matrix def cosine_sim(u1, u2): """两个用户的评分字典求余弦相似度,只取共同评分的电影""" common = set(u1) & set(u2) if not common: return 0.0 v1 = np.array([u1[m] for m in common]) v2 = np.array([u2[m] for m in common]) denom = np.linalg.norm(v1) * np.linalg.norm(v2) return float(v1.dot(v2) / denom) if denom else 0.0 def recommend_for_user(user_id, top_n=10, neighbor_k=20): matrix = build_user_movie_matrix() if user_id not in matrix: return [] # 新用户冷启动,走热门兜底 target = matrix[user_id] sims = [] for uid, ratings in matrix.items(): if uid == user_id: continue sims.append((uid, cosine_sim(target, ratings))) sims.sort(key=lambda x: x[1], reverse=True) neighbors = sims[:neighbor_k] scores, weights = {}, {} for uid, sim in neighbors: if sim <= 0: continue for mid, score in matrix[uid].items(): if mid in target: # 已经看过的跳过 continue scores[mid] = scores.get(mid, 0) + sim * score weights[mid] = weights.get(mid, 0) + sim ranked = sorted(scores.items(), key=lambda x: x[1] / weights[x[0]], reverse=True) movie_ids = [mid for mid, _ in ranked[:top_n]] return list(Movie.objects.filter(id__in=movie_ids))逐段说明:build_user_movie_matrix一次性把评分表读进内存字典,避免在循环里反复查库,这是性能关键;cosine_sim只对共同评分项计算,没有交集直接返回 0,防止除零;recommend_for_user先算相似邻居取前neighbor_k个,再用加权平均分排序。参数neighbor_k控制参与推荐的邻居数量,设太大会引入不相关用户拉低准确率,设太小又覆盖不足,毕设数据量下 20 左右比较稳。top_n就是返回条数。
3.3 基于物品的协同过滤与离线预计算
基于物品的思路反过来:算电影之间的相似度,用户看过 A,就推和 A 相似的 B。它的好处是物品相似度相对稳定,可以离线算好存表,线上只做查表加权,响应快很多。常见做法是建一张MovieSimilarity表:
class MovieSimilarity(models.Model): movie_a = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='sim_a') movie_b = models.ForeignKey(Movie, on_delete=models.CASCADE, related_name='sim_b') score = models.FloatField() class Meta: unique_together = ('movie_a', 'movie_b')然后用自定义命令离线跑:
# recommend/management/commands/build_sim.py from django.core.management.base import BaseCommand from recommend.cf import build_user_movie_matrix from movies.models import MovieSimilarity class Command(BaseCommand): help = '离线计算物品相似度并落库' def handle(self, *args, **options): matrix = build_user_movie_matrix() # 转成 {movie_id: {user_id: score}} item_users = {} for uid, ratings in matrix.items(): for mid, s in ratings.items(): item_users.setdefault(mid, {})[uid] = s MovieSimilarity.objects.all().delete() items = list(item_users.items()) for i in range(len(items)): for j in range(i + 1, len(items)): mid_a, ua = items[i] mid_b, ub = items[j] common = set(ua) & set(ub) if len(common) < 2: # 共同评分太少,相似度不可信 continue # 简化余弦,实际可换皮尔逊 import numpy as np va = np.array([ua[u] for u in common]) vb = np.array([ub[u] for u in common]) sim = float(va.dot(vb) / (np.linalg.norm(va) * np.linalg.norm(vb))) if sim > 0.1: MovieSimilarity.objects.create(movie_a_id=mid_a, movie_b_id=mid_b, score=sim) self.stdout.write('相似度计算完成')这里len(common) < 2的过滤很重要,只有一两个共同评分用户算出来的相似度基本是噪声。sim > 0.1是入库阈值,低于它的相似关系存了也没意义,还占空间。跑完python manage.py build_sim,线上推荐页直接查这张表就行,不用每次请求都重算,这是把算法工程化的关键一步。
4. 避坑与排查:那些让项目"看起来能跑"的坑
4.1 推荐结果永远为空
现象是页面正常渲染但推荐列表是空数组。原因通常有三个:一是Rating表没数据,协同过滤无米下锅;二是相似度阈值或neighbor_k设得过高,测试数据量下找不到合格邻居;三是新用户冷启动,user_id不在矩阵里直接返回空。解决方式是先Rating.objects.count()确认数据量,再临时把阈值调低验证算法本身没问题,最后给冷启动加兜底——按avg_rating排序返回热门电影,别让页面空着。
4.2 迁移时报字段长度或字符集错误
现象是migrate中途报Specified key was too long或中文变问号。原因是 mysql 5.6 及以下默认字符集不是 utf8mb4,或者CharField加了unique且长度超过索引限制。解决是建库时显式指定utf8mb4,settings.py的OPTIONS里加charset,必要时把长文本字段改成TextField或缩短max_length。
4.3 打分后平均分不更新
现象是详情页显示的平均分和实际评分对不上。原因是avg_rating冗余字段只在初始化时算过一次,后续打分没触发更新。解决是在评分保存逻辑里重算:
from django.db.models import Avg, Count def refresh_movie_stat(movie_id): stat = Rating.objects.filter(movie_id=movie_id).aggregate( avg=Avg('score'), cnt=Count('id')) Movie.objects.filter(id=movie_id).update( avg_rating=stat['avg'] or 0, rating_count=stat['cnt'])每次Rating新增或修改后调用一次,保证列表页排序数据是准的。
4.4 相似度计算把服务器跑挂
现象是数据量到几万条评分后,推荐页请求超时甚至内存溢出。原因是在视图里实时构建全量矩阵并两两算相似度,复杂度是 O(n²)。解决是把物品相似度改成离线命令预计算落库,线上只做查表和加权;用户相似度如果也要实时算,至少加缓存,用django.core.cache把矩阵缓存几分钟,别每个请求都重建。
4.5 静态文件和模板找不到
现象是页面样式全丢或TemplateDoesNotExist。原因是settings.py里TEMPLATES的DIRS没配,或STATICFILES_DIRS漏了项目级 static 目录。解决是确认每个 app 的templates/下有对应 html,项目级模板放BASE_DIR/templates并在DIRS里注册,开发阶段DEBUG=True时 Django 会自动找 app 内静态文件,上线才需要collectstatic。
5. 二次开发改造:把毕设级代码推到能用的程度
拿到这套代码后,最值得改的不是界面,是推荐链路的工程化程度。第一个改造点是把算法参数外置。现在neighbor_k、top_n、相似度阈值都硬编码在函数里,改一次要动代码。我一般会在settings.py加一组配置,或者建一张RecommendConfig表,让运营能调:
# settings.py RECOMMEND = { 'NEIGHBOR_K': 20, 'TOP_N': 10, 'MIN_COMMON': 2, 'SIM_THRESHOLD': 0.1, }算法函数从settings.RECOMMEND读,改参数不用重新部署逻辑代码。第二个改造点是加缓存层。用户相似度矩阵在数据没大变动时是稳定的,用cache.set('user_sim_matrix', matrix, 600)缓存十分钟,请求量上来后响应时间能降一个数量级。第三个改造点是混合推荐:纯协同过滤对新用户和新电影都无能为力,常见做法是冷启动走热门榜、有行为后走协同过滤、再叠加一点分类偏好加权,公式不用复杂,final_score = 0.7 * cf_score + 0.3 * category_match就够演示出效果差异。
验证改造是否有效,别靠肉眼看列表。我习惯留一个小脚本,固定一批测试用户,对比改造前后推荐结果的命中率(推荐里有多少是用户实际打过高分的):
def hit_rate(user_ids, top_n=10): hit, total = 0, 0 for uid in user_ids: recs = set(m.id for m in recommend_for_user(uid, top_n=top_n)) liked = set(Rating.objects.filter(user_id=uid, score__gte=4) .values_list('movie_id', flat=True)) if liked: hit += len(recs & liked) total += len(liked) return hit / total if total else 0跑一遍改造前后各一次,数字涨了才算改对。这套项目本身不复杂,真正拉开差距的是你有没有把"能演示"变成"能解释每个推荐是怎么来的"。从那以后我每次接手这类推荐项目,都先把评分表结构和相似度落库这两件事确认一遍,再谈算法优化,不然调参调半天,问题其实出在数据源头。希望帮到你。
本文还有配套的精品资源,点击获取