news 2026/10/12 17:23:34

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

作者头像

张小明

前端开发工程师

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

简介:面向毕业设计、课程设计与Python/Django学习者的在线电影推荐系统完整项目包,覆盖数据采集、预处理、特征提取、相似度计算、推荐算法到可视化展示全流程。系统采用余弦相似度与协同过滤思路,可帮助用户构建个性化电影推荐服务,适合需要快速搭建原型或撰写论文的本科、专科学生及相关开发者。压缩包共392个文件,约9.76MB,主要包含28个Python源码(含核心脚本movie_recommender.py)、43个pyc编译文件、35个CSS样式、62个JS脚本、9个HTML页面,以及用于展示电影海报与界面效果的91张JPG、34张PNG图片和75个GIF动图,另有SQL数据库文件可供初始化数据。源码模块划分清晰,静态资源与后端逻辑分离,方便二次开发与功能扩展。已有255人学习下载,适合作为毕业设计参考、课程项目改进素材或推荐系统入门实践。

1. 在线电影推荐系统到底解决什么问题

第一次拿到这份基于 Python 的在线电影推荐系统 zip 包时,我以为是轻量爬虫脚本,解压之后发现里面同时躺着 Django 后端和一套完整的前端主题资源——这是一个能出演示效果的课程设计级项目。它的核心逻辑建立在两条线上:一条从网页端收集用户的播放、评分行为,另一条用协同过滤和内容相似度计算对电影库做个性化排序,最终把推荐结果渲染成带海报的页面。对毕设和课设来说,它最大的价值就是“能跑、看得见、答辩有交代”;对刚入行想做推荐系统的工程师来说,它是理解用户画像、余弦相似度、召回排序这套闭环的最佳起步样本。

你需要先清楚一点:推荐系统是数据敏感型应用,算法只占三分之一,另外三分之二在数据清理和参数调优。这份资源里 movie_recommender.py 是算法入口,但它不会自己长出数据,也不会自动适配你的 MySQL 密码。接下来的内容我会按照“项目结构 → 数据落库 → 推荐算法 → 启动运行 → 避坑 → 效果验证”的顺序,把每个环节的操作细节和翻车点说透。

2. 项目骨架与数据层:看懂 zip 里装了什么

2.1 静态资源与 Python 代码的分工

解压 zip 之后你大概率能看到两类文件:一类是 agriox.css、bootstrap.min.css、layui.css、animate.min.css、custom-animate.css 这类前端资源,另一类是核心的 Python 代码文件。CSS 文件的存在说明项目不是 API-only 的后端,而是“前端渲染 + 后端接口”的完整 Web 应用。

我们常说的 Django 模板机制会让 HTML 文件引用这些静态资源,Django 在开发环境下通过{% static %}标签把 css 文件映射到/static/路径下。如果你发现页面打开后样式全丢,十有八九是 settings.py 里STATIC_URL配置不对,或者静态文件压根没放到static/目录下。

我一般会把 zip 里的目录结构做一次归位,再开始动代码:

unzip 基于python的在线电影推荐系统.zip -d movie_recommender cd movie_recommender python --version # 确认当前 Python 版本,低于 3.6 建议先升级

提示:在 Windows 上解压后,记得检查项目根目录下有没有manage.py。如果没有,说明你拿到的只是某个 app 目录,需要自己拼进一个 Django 工程。

2.2 数据采集与预处理:爬虫部分的常规写法

项目摘要里提到的数据收集,常见做法是用 requests 抓取公开的电影详情页,然后用 BeautifulSoup 解析标题、导演、演员、类型字段,最后把干净数据写入数据库。movie_recommender.py 里通常会有类似fetch_movie_list的函数负责这件事。

import requests import pandas as pd from bs4 import BeautifulSoup from sqlalchemy import create_engine def fetch_movie_list(url, pages=5): movie_rows = [] for page in range(1, pages + 1): try: resp = requests.get(url, params={"start": (page - 1) * 25}, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") for item in soup.select(".movie-item"): title = item.select_one(".title").text.strip() genres = [g.text for g in item.select(".genre")] score = item.select_one(".score").text movie_rows.append({"title": title, "genres": "|".join(genres), "score": float(score)}) except requests.RequestException as e: print(f"第 {page} 页抓取失败: {e}") continue return pd.DataFrame(movie_rows)

这里有两个细节需要说明。params参数控制翻页起点,start每次加 25 是常规的列表页分页逻辑;.movie-item、.genre这些选择器必须按目标页面实际 HTML 结构调整,写死之后如果页面改版就要重新检查。我建议把 pages 参数从 5 调小到 2 做冒烟测试,确认能拿到数据再完整跑,避免一次性触发访问频率限制。

数据抓回来之后还不能直接入库。重复标题要去重,空值要补默认值,时间字段要统一格式,评分要做区间校验。预处理做得越干净,后面算相似度时产生的脏数据就越少。

2.3 系统需要哪几张数据表:建表 SQL 与字段选择

在线电影推荐系统虽然是课程设计,但表结构设计不能太“学生气”。至少需要四张表:用户表、电影表、评分表、推荐结果缓存表。评分表是整个推荐系统的核心,因为协同过滤的输入就是“用户—物品—行为”三元组。

CREATE TABLE `user` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(64) NOT NULL UNIQUE, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE `movie` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `title` VARCHAR(255) NOT NULL, `director` VARCHAR(128), `actors` VARCHAR(512), `genres` VARCHAR(128), `release_year` SMALLINT, `poster_url` VARCHAR(512), `avg_rating` DECIMAL(3,2) ); CREATE TABLE `rating` ( `user_id` INT NOT NULL, `movie_id` INT NOT NULL, `score` TINYINT NOT NULL, `timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`, `movie_id`), CONSTRAINT `fk_rating_user` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), CONSTRAINT `fk_rating_movie` FOREIGN KEY (`movie_id`) REFERENCES `movie`(`id`) );

字段类型上,genres 字段用 VARCHAR 并以|分隔类型,是课程设计里实用主义的选择,因为类型参与相似度计算时只需split("|")就能转换成集合,不需要再做关联表。avg_rating 用 DECIMAL(3,2) 而不是 FLOAT,是为了避免浮点误差。rating 表用联合主键 (user_id, movie_id) 能天然挡住重复打分。

这四张表建好之后,Django 侧还需要在 models.py 里同步定义模型类,再执行python manage.py makemigrations和python manage.py migrate把表映射进数据库。注意顺序一定是先建 Django 模型再迁移,如果用 Navicat 手动建了表,Django 的 ORM 反而会认为迁移状态不一致。

3. 推荐算法核心:从相似度计算到双路召回

3.1 余弦相似度:为什么选它而不是欧氏距离

电影评分数据是典型的高维稀疏数据。假设有 5000 部电影,一个用户可能只评分过 80 部,那么他的特征向量里 98% 的维度都是空的。欧氏距离在高维稀疏空间里会被这些全零维度严重干扰,而余弦相似度只关注向量方向的夹角,不受向量长度影响。换句话说,一个用户只评了 5 部电影,另一个用户评了 500 部电影,只要他们评分过的电影风格分布接近,余弦相似度依然能给出较高的相似值。

import numpy as np def cosine_similarity(vec_a, vec_b): # vec_a 和 vec_b 是两个等长的稀疏评分向量 dot = np.dot(vec_a, vec_b) norm_a = np.linalg.norm(vec_a) norm_b = np.linalg.norm(vec_b) if norm_a == 0 or norm_b == 0: return 0.0 return dot / (norm_a * norm_b)

这段代码的逻辑很直白:分子是两个向量的点积,分母是两个向量模长的乘积。返回 0.0 的情况必须处理,否则会有除零警告。上一个项目中我就遇到过这种情况——冷启动的空白用户向量,直接导致训练过程报错。

3.2 基于内容的推荐:从类型和导演找“同款”

基于内容的推荐适合第一版演示,因为它可解释性极强:用户看过《星际穿越》,系统推荐《火星救援》,理由是类型都是科幻 + 冒险。实现思路是把电影转成特征文本,类型、导演、演员拼成一个字符串,再用 TF-IDF 或简单的集合交集去计算两部电影的内容相似度。

def content_based_recommend(movie_id, movie_df, top_n=10): target = movie_df.loc[movie_id] # 用类型集合的交集大小作为粗略相似度 target_genres = set(target["genres"].split("|")) similarity_list = [] for idx, row in movie_df.iterrows(): if idx == movie_id: continue genres = set(row["genres"].split("|")) # 交集越大越相似,同时微弱加权评分 score = len(target_genres & genres) * 0.7 + row["avg_rating"] * 0.01 similarity_list.append((idx, score)) similarity_list.sort(key=lambda x: x[1], reverse=True) return [movie_id for movie_id, _ in similarity_list[:top_n]]

这里我故意把交集大小作为主权重,评分只做 0.01 的微弱加权,是因为内容推荐不应该被评分绑架。一个 9.2 分的文艺片和一个 8.9 分的科幻片相比,用户当前场景想看的是“同款类型”,而不是“绝对高分”。top_n 参数决定推荐列表长度,10 在网页端比较合适,5 适合手机端展示。

内容推荐的最大问题是多样性差,用户看多了会觉得“推荐来推荐去都是这些”,所以实战中它通常只作为一路召回,不单独作为最终结果。

3.3 协同过滤:找到口味相同的人

基于用户的协同过滤是这类型项目里的点睛环节。核心步骤是三步:构建用户—物品评分矩阵,计算用户之间相似度,找到邻居用户已评分且目标用户未接触的电影作为候选推荐。

from scipy.sparse import csr_matrix def user_based_recommend(user_rating_matrix, target_user_id, k=10, top_n=10): # 每一行是一个用户,每一列是一部电影,值是评分 similar_users = [] target_vector = user_rating_matrix[target_user_id].toarray().flatten() for other_user_id in range(user_rating_matrix.shape[0]): if other_user_id == target_user_id: continue other_vector = user_rating_matrix[other_user_id].toarray().flatten() sim = cosine_similarity(target_vector, other_vector) similar_users.append((other_user_id, sim)) similar_users.sort(key=lambda x: x[1], reverse=True) neighbors = [uid for uid, sim in similar_users[:k] if sim > 0.1] # 聚合邻居的评分,排除用户已看过的电影 recommend_scores = {} for neighbor_id in neighbors: neighbor_ratings = user_rating_matrix[neighbor_id].toarray().flatten() for movie_idx, score in enumerate(neighbor_ratings): if score > 0 and user_rating_matrix[target_user_id, movie_idx] == 0: recommend_scores[movie_idx] = recommend_scores.get(movie_idx, 0) + score ranked = sorted(recommend_scores.items(), key=lambda x: x[1], reverse=True) return [movie_idx for movie_idx, _ in ranked[:top_n]]

这段代码有几个参数需要解释。k 控制邻居数量,10 是经验值,数据集小的话可以压到 5;sim > 0.1 的阈值是过滤弱相关用户的闸门,防止噪声邻居污染推荐结果;聚合时直接累加邻居评分是最简单的评分预测,严格来说还应该除以邻居权重总和做归一化。矩阵用csr_matrix稀疏格式存储,5000 部电影 × 3000 用户的矩阵在内存占用上这是近乎必须的选择。

4. 把推荐跑起来:Django 工程启动与页面操作

4.1 环境准备:Python 版本、依赖与虚拟环境

这个阶段是新手最容易卡住的地方,也是搜“python安装教程”的痛点来源。项目虽然是 Django 驱动,但算法部分依赖 numpy 和 pandas,数据库驱动至少需要 pymysql 或 mysqlclient 其中一个。我建议先用虚拟环境隔离,避免和本机其他 Python 项目的包版本互相干扰。

python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install django numpy pandas pymysql requests beautifulsoup4

提示:如果机器上同时存在 Python 2 和 Python 3,务必用python3 -m venv venv指定版本,否则可能建出一个基于 Python 2 的虚拟环境,随后 pip 安装的 Django 版本会不兼容。

安装完成后马上验证三个核心包能不能正常导入,这一步能提前暴露 90% 的环境问题。很多同学反映“Django 界面能打开但是算法接口 500 错误”,通常就是 numpy 版本不一致或者 pandas 装失败。

4.2 数据库配置与数据初始化

在 settings.py 里把默认的 SQLite 改成 MySQL 是常规操作,但要注意字符集设置。电影标题里常见中文和特殊符号,utf8mb4 是最稳妥的选择。

DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "movie_recommender", "USER": "root", "PASSWORD": "你的数据库密码", "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": { "charset": "utf8mb4" } } }

代码里最容易被忽略的是OPTIONS里的 charset 配置。如果你用默认 utf8,遇到片名里的生僻字或 emoji 符号会出现Incorrect string value报错。这里的连接方式也决定了一个常见坑:pymysql 需要在项目__init__.py中执行一段兼容声明,否则 Django 会找不到 MySQLdb 模块。一般在__init__.py里写import pymysql; pymysql.install_as_MySQLdb()就能解决。

初始化顺序不要搞反:先建库,再迁移,最后跑爬虫或导入数据集。我曾经先跑迁移再建库,结果 Django 报Unknown database,原因很简单——迁移脚本自己不会帮你创建空数据库。

mysql -uroot -p -e "CREATE DATABASE movie_recommender DEFAULT CHARACTER SET utf8mb4;" python manage.py makemigrations python manage.py migrate python movie_recommender.py --fetch-pages 3

最后一行是调用爬虫脚本的示意命令,--fetch-pages 3表示只准备 3 页数据用于演示,全量爬取时要根据目标网站的反爬力度调整。

4.3 启动 Web 服务与验证推荐接口

数据入库之后,启动 Django 开发服务器看效果。如果发现样式错乱,第一排查顺序是:浏览器控制台 404 静态文件请求 → settings.py 的 STATICFILES_DIRS → 模板里{% static %}标签拼出的资源路径。

python manage.py runserver 0.0.0.0:8000 curl "http://127.0.0.1:8000/recommend/?user_id=1&top_n=10"

返回的 JSON 里应该有电影标题、推荐理由、相似度分数三个字段。为什么用 GET 而不是 POST?因为演示阶段我们希望直接在浏览器地址栏输入即可看到推荐结果,方便答辩。实际工程中推荐接口必须改成 POST 并加权限校验,否则任何人都能通过遍历 user_id 获取所有用户的观影偏好,属于严重的越权漏洞。

5. 避坑指南:推荐系统项目的常见问题与排查

5.1 CSS 样式全部失效

现象:页面内容正常显示,但所有按钮、海报、布局都是裸露的 HTML,没有颜色和排版。浏览器 F12 控制台显示大量404请求,指向/static/css/...。原因:settings.py 里STATIC_URL和项目实际目录不匹配,或者 templates 中的静态资源路径写成了绝对路径。解决:确认项目根目录存在 static 文件夹,然后编辑 settings.py 补上STATICFILES_DIRS = [BASE_DIR / "static"]。注意 Django 4.x 的路径写法需要用BASE_DIR / "static"而不是拼接字符串。

5.2 中文乱码或写入数据库报错

现象:启动爬虫后,终端输出的电影标题显示为????????,或者 MySQL 报Incorrect string value: '\xE7\x94\xB5...'。原因:数据库表或连接字符集不是 utf8mb4。解决:检查三个位置——建库语句的DEFAULT CHARACTER SET utf8mb4、settings.py 的 OPTIONS 中 charset 值、以及 MySQL 配置文件 my.ini 中的character-set-server=utf8mb4。修改之后需要重启 MySQL 服务,只改 Django 端不重启数据库服务等于白改。

5.3 相似度计算结果全是零

现象:推荐列表为空,调试发现所有用户或电影的余弦相似度都是 0.0。原因:评分矩阵构建时索引错位,导致向量之间完全无交集;或者输入向量没有正确归一化。解决:打印形状确认矩阵维度,检查 movie_id 是不是从 0 连续索引,因为稀疏矩阵不接受非连续索引。我在第一次实验里直接把数据库自增 id 当成矩阵列索引,而自增 id 从 1 开始、有删除空洞,最后矩阵形状校验直接崩掉。

5.4 导入 zip 后提示缺少模块

现象:pip 安装依赖后运行仍报No module named 'MySQLdb'或No module named 'bs4'。原因:pymysql 没有注册为 MySQLdb 替身,或依赖装进了错误的 Python 环境。解决:在项目__init__.py中注入pymysql.install_as_MySQLdb();再执行pip list检查包所在环境,和which python确认解释器路径是否一致。很多时候是因为开了一个新的终端窗口,激活虚拟环境的命令被重置了。

5.5 推荐结果永远是热门的几部电影

现象:无论 user_id 怎么换,推荐列表都是库里评分最高的那几部,个性化几乎为零。原因:协同过滤的邻居列表中有效邻居太少,最终聚合分数被一两个高分用户主导。解决:检查该用户的历史评分数量,少于 5 条时建议走内容推荐或热门榜兜底,不要在协同过滤上硬撑;同时把sim > 0.1的阈值提高到 0.25,过滤弱相关邻居。

6. 冷启动处理与推荐效果离线验证

冷启动是推荐系统的老大难,也是答辩时最容易露怯的提问点。新注册用户没有任何评分行为,协同过滤直接无解。我在这类项目里的处理方式是做一个三级兜底策略:首先用全站热门 Top20 填充,保证页面不为空;其次在详情页埋几个“相似电影”位,走基于内容的推荐;最后等用户产生至少 5 条评分后,再切换为协同过滤主推。这套策略代码加在视图层而不在算法里,改动最轻。

离线验证部分,很多同学的实验报告里写“准确率 90%”,我建议你动手做一次拆分验证,这个数据才有说服力。常规做法是把评分记录按 8:2 分为训练集和测试集,只让算法看到训练集,然后统计推荐列表命中测试集的比例。

from sklearn.model_selection import train_test_split # 假设 data 是 DataFrame,包含 user_id, movie_id, score 三列 train_data, test_data = train_test_split(data, test_size=0.2, random_state=42) def precision_at_k(train, test, k=10): hits = 0 test_users = test["user_id"].unique() for uid in test_users: rec_list = user_based_recommend(train, uid, k=k) actual = set(test[test["user_id"] == uid]["movie_id"]) hits += len(set(rec_list) & actual) return hits / (len(test_users) * k) print(f"Precision@10 = {precision_at_k(train_data, test_data):.3f}")

验证时要特别注意两个边界:random_state 固定,这样实验可以复现;如果测试用户召回不到 k 部电影,分母直接按 k 算会略微低估精确率,更严格的口径是只统计至少有一条推荐结果的用户。

整个项目跑顺之后,我最深的使用感受是“脏数据是大敌,参数是配角”——只要把爬虫清洗和入库这条链路管严,推荐结果不会差到哪去;一旦数据乱,再调相似度阈值都是白费功夫。后来我每次拿到新的推荐系统项目,都会强制要求自己先花一半时间摸数据形态,再让推荐代码碰数据。这个方法在这个电影推荐系统的好几次修改里都帮了大忙。希望你下载复现的时候,也能把前半段的数据链路当成重点,这会少走很多弯路,希望帮到你。

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

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

open-code-review:把代码审查变成可沉淀的工程资产

代码审查大概是整个研发流程里最容易被消耗掉价值的一环。我见过不少团队把代码审查写进制度,每次合并前必须有一个人点通过,结果呢?白天各忙各的,晚上打开待审查的改动列表,里面躺着十几个几百行的大文件,…

作者头像 李华
网站建设 2026/10/12 17:16:14

Agent框架工程化:工具插件化与会话日志可回放的设计实践

做 Agent 框架工程化,我最大的体会是:模型的能力边界,一半在权重里,另一半在“它到底能触达什么样的环境”里。我最近在持续打磨一套开源推理模型配套的 Harness 评估框架,它要解决的核心问题,是评估一个会…

作者头像 李华
网站建设 2026/10/12 17:15:00

梯度散度旋度:电磁场的三把物理探针

1. 这不是数学考试,是电磁场的“操作手册”你翻开任何一本《电磁场与电磁波》教材,翻到第二章,大概率会撞上三个符号:∇φ、∇A、∇A。旁边密密麻麻写着“梯度”、“散度”、“旋度”,再配上一堆矢量微积分公式和几何解…

作者头像 李华
网站建设 2026/10/12 17:14:44

Redis分布式误区与四大运行模式深度解析

1. 为什么“Redis的分布式”这个说法本身就是一个典型误区刚接触分布式缓存的开发者,十有八九会在文档、面试题甚至技术分享里看到“Redis的分布式”这个短语。我第一次在某次内部架构评审会上听到同事说“我们用的是Redis分布式集群”,当场就愣了一下—…

作者头像 李华