简介:这是一套面向高校计算机相关专业学生与Python初学者打造的在线电影推荐系统完整项目源码,可直接用于毕业设计、课程设计或论文配套实践。项目以Python为核心,结合Django框架搭建Web端,围绕数据采集、清洗预处理、特征提取、相似度计算与推荐算法等环节,实现基于内容与协同过滤的个性化电影推荐,并支持网页端可视化展示推荐结果。压缩包共392个文件,约9.76MB,包含28个py源码、43个pyc编译文件、62个js脚本、35个css样式、9个html页面,以及大量jpg、gif、png图片素材和字体文件,另附sql数据库脚本,前后端资源与静态素材齐全,目录结构清晰。目前已有255人学习下载。读者可据此掌握推荐系统从数据到展示的完整链路,理解余弦相似度与推荐策略的落地方式,并在此基础上优化算法与特征提取,提升推荐准确度,适合作为项目实战与论文写作的参考。
1. 从一份“能跑起来”的毕设说起:这个电影推荐系统到底交付了什么
如果你正在为毕业设计或课程设计找一个能讲清楚、能演示、代码量适中的题目,基于 Python 的在线电影推荐系统是一个被反复验证过的方向。它不像深度学习项目那样吃显卡,也不像纯前端页面那样缺乏算法含量,刚好卡在“有东西可讲、有代码可跑、有结果可看”的区间里。这份资源包的核心是一个名为 movie_recommender.py 的脚本,配合 Django 或 Flask 搭建的 Web 界面,把数据采集、清洗、特征提取、相似度计算、推荐生成和结果展示串成了一条完整链路。它适合两类人:一类是时间紧、需要快速搭出可演示系统的同学;另一类是刚接触推荐算法、想找一个能拆开看每一步的实战项目的新手。你拿到的不只是一份源码,而是一个可以逐行调试、逐模块替换的推荐系统骨架。
2. 推荐链路拆解:从爬虫到相似度矩阵的五个环节
2.1 数据从哪来:爬虫模块的边界与替代方案
推荐系统的第一道坎永远是数据。这份资源里的数据收集环节默认走的是网络爬取路线,目标字段包括电影标题、导演、演员、类型、评分和简介。常见做法是用 requests 配合 BeautifulSoup 抓取公开的电影信息页面,再落库到 SQLite 或 MySQL。但这里有一个现实问题:目标网站的结构随时可能变,爬虫代码放半年就可能失效。所以我在复现时一般会把爬虫部分做成可替换的适配器模式,核心推荐逻辑不依赖具体数据源。
# spider_adapter.py
import requests
from bs4 import BeautifulSoup
import sqlite3
def fetch_movie_list(base_url, pages=5):
"""抓取电影列表页,返回原始 HTML 列表"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
}
html_list = []
for page in range(pages):
url = f"{base_url}?start={page * 25}"
resp = requests.get(url, headers=headers, timeout=10)
if resp.status_code == 200:
html_list.append(resp.text)
else:
print(f"第 {page} 页抓取失败,状态码 {resp.status_code}")
return html_list
def parse_and_store(html_list, db_path="movies.db"):
"""解析 HTML 并写入 SQLite"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
director TEXT,
actors TEXT,
genres TEXT,
rating REAL,
summary TEXT
)
""")
for html in html_list:
soup = BeautifulSoup(html, "html.parser")
for item in soup.select(".item"):
title = item.select_one(".title").get_text(strip=True) if item.select_one(".title") else ""
rating_text = item.select_one(".rating_num")
rating = float(rating_text.get_text(strip=True)) if rating_text else 0.0
# 其余字段按实际页面结构补全
cursor.execute(
"INSERT INTO movies (title, director, actors, genres, rating, summary) VALUES (?, ?, ?, ?, ?, ?)",
(title, "", "", "", rating, "")
)
conn.commit()
conn.close()
这段代码的关键参数是 pages 和 base_url 。 pages 控制抓取深度,毕设演示一般 5 到 10 页就够,太多会拖慢首次运行速度。 headers 里的 User-Agent 必须带,否则很多站点直接返回 403。如果你不想碰爬虫,可以直接用 MovieLens 的 ml-latest-small 数据集,它自带 ratings.csv 和 movies.csv,导入后字段更规整,推荐效果也更容易复现。我一般会建议时间紧的同学走这条路,把精力留给推荐算法本身。
2.2 数据清洗与特征提取:把“脏数据”变成可用向量
爬下来的数据不能直接喂给算法。标题里可能带年份括号,类型字段可能是“剧情/动作/科幻”这种斜杠分隔的字符串,演员列表可能为空。数据预处理要做三件事:去重、补缺、把文本转成数值。去重按标题加年份做唯一键;补缺时评分缺失用该电影类型的中位数填充,演员缺失直接留空但不影响基于内容的推荐;文本转数值最常用的就是 TF-IDF 或者 CountVectorizer 把类型和简介转成向量。
# preprocess.py
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
def load_and_clean(db_path="movies.db"):
conn = sqlite3.connect(db_path)
df = pd.read_sql("SELECT * FROM movies", conn)
conn.close()
# 去重:标题相同且评分相同的视为重复
df.drop_duplicates(subset=["title", "rating"], inplace=True)
# 补缺:评分缺失用同类型中位数填充
df["rating"] = df.groupby("genres")["rating"].transform(
lambda x: x.fillna(x.median())
)
df["rating"].fillna(df["rating"].median(), inplace=True)
# 组合特征文本:类型 + 简介
df["feature_text"] = df["genres"].fillna("") + " " + df["summary"].fillna("")
return df
def build_tfidf_matrix(df):
"""把 feature_text 转成 TF-IDF 矩阵"""
vectorizer = TfidfVectorizer(
max_features=5000,
stop_words="english",
ngram_range=(1, 2)
)
tfidf_matrix = vectorizer.fit_transform(df["feature_text"])
return tfidf_matrix, vectorizer
max_features=5000 是个经验值,太小会丢信息,太大矩阵稀疏且计算慢。 ngram_range=(1, 2) 表示同时考虑单个词和相邻两个词的组合,对“科幻 动作”这种类型组合的捕捉更准。清洗完的数据行数建议控制在 2000 到 5000 条之间,太少推荐多样性差,太多余弦相似度矩阵会变成几千乘几千的稠密矩阵,内存吃不消。
2.3 相似度计算:余弦相似度矩阵的生成与存储
有了 TF-IDF 矩阵,下一步就是算电影之间的相似度。余弦相似度是这份资源里默认用的方法,它的好处是对向量长度不敏感,只看向量方向,适合文本特征。计算出来的相似度矩阵是一个 N×N 的对称矩阵,N 是电影数量。如果 N 是 3000,矩阵就是 900 万个浮点数,用 float32 存大约 36MB,可以接受。但如果你有 2 万部电影,这个矩阵就会膨胀到 1.6GB,必须换用稀疏矩阵或者只算 Top-K 邻居。
# similarity.py
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
def compute_similarity(tfidf_matrix):
"""计算电影之间的余弦相似度矩阵"""
sim_matrix = cosine_similarity(tfidf_matrix, tfidf_matrix)
# 对角线置零,避免推荐自己
np.fill_diagonal(sim_matrix, 0)
return sim_matrix
def get_top_n_similar(sim_matrix, movie_idx, n=10):
"""取某部电影最相似的 n 部"""
sim_scores = list(enumerate(sim_matrix[movie_idx]))
sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
return sim_scores[:n]
cosine_similarity 默认返回稠密矩阵,如果电影数超过 1 万,建议改用 sklearn.neighbors.NearestNeighbors 配合 metric="cosine" ,它内部用树结构或近似算法,内存和速度都更友好。 np.fill_diagonal 那一步容易被忽略,但如果不做,推荐列表里会出现用户正在看的电影本身,演示时很尴尬。
2.4 推荐算法选择:基于内容还是协同过滤
这份资源提到了两种推荐思路:基于内容的推荐和协同过滤。基于内容的推荐直接用上面算好的电影相似度矩阵,给用户看过的电影找相似电影,优点是冷启动友好,新电影只要有了类型和简介就能被推荐;缺点是无法发现用户潜在兴趣,推荐结果容易同质化。协同过滤则需要用户-物品评分矩阵,用矩阵分解或者用户相似度来推,优点是能挖掘跨类型偏好,缺点是数据稀疏时效果差。
# recommender.py
def content_based_recommend(sim_matrix, user_history, movies_df, top_n=10):
"""基于内容的推荐:根据用户看过的电影找相似"""
scores = np.zeros(len(movies_df))
for movie_id in user_history:
if movie_id < len(sim_matrix):
scores += sim_matrix[movie_id]
# 排除已看过的
for movie_id in user_history:
scores[movie_id] = 0
top_indices = scores.argsort()[::-1][:top_n]
return movies_df.iloc[top_indices][["title", "genres", "rating"]]
user_history 是用户看过的电影索引列表。这个函数把用户历史中每部电影的相似度向量累加,得到候选电影的得分,再排除已看过的。实际项目中我会把两种算法都保留,在配置里加一个 ALGO 开关,演示时切换对比效果,答辩时也能多一个可讲的对比维度。
3. 把推荐结果搬到网页上:Django 接口与前端渲染的衔接
3.1 Django 视图与 URL 配置
推荐逻辑跑通后,需要把它包装成 Web 接口。这份资源的前端用了 Bootstrap 和 Layui 的样式文件,后端如果是 Django,核心工作就是写一个视图函数接收用户 ID,调用推荐函数,把结果渲染到模板。下面是一个最小可用的视图和路由配置。
# views.py
from django.shortcuts import render
from .recommender import content_based_recommend
from .models import Movie, UserHistory
def recommend_view(request, user_id):
history = UserHistory.objects.filter(user_id=user_id).values_list("movie_id", flat=True)
movies_df = pd.DataFrame(list(Movie.objects.all().values()))
sim_matrix = load_similarity_matrix() # 从缓存或文件加载
result = content_based_recommend(sim_matrix, list(history), movies_df, top_n=12)
return render(request, "recommend.html", {"movies": result.to_dict("records")})
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path("recommend/<int:user_id>/", views.recommend_view, name="recommend"),
]
user_id 从 URL 传入,方便演示时切换不同用户看推荐差异。 load_similarity_matrix 建议把矩阵存成 .npy 文件,启动时加载一次,不要每次请求都重算。如果每次请求都跑一遍 cosine_similarity ,3000 部电影大概要 2 到 3 秒,用户体验很差。
3.2 模板渲染与静态文件组织
模板里用 Django 的模板语法遍历推荐结果,把电影海报、标题、类型和评分展示出来。静态文件目录下放好 Bootstrap、Layui 和自定义的 agriox.css ,注意在 settings.py 里配好 STATICFILES_DIRS 。
<!– recommend.html –>
<div class="row">
{% for movie in movies %}
<div class="col-md-3">
<div class="card">
<img src="{{ movie.poster_url }}" class="card-img-top" alt="{{ movie.title }}">
<div class="card-body">
<h5 class="card-title">{{ movie.title }}</h5>
<p class="card-text">{{ movie.genres }} | 评分:{{ movie.rating }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
如果海报字段为空,前端会显示裂图,建议在模板里加一个 default 过滤器或者用占位图。静态文件路径在 Django 开发模式下由 static 模板标签处理,部署到生产环境时需要走 Nginx 或 WhiteNoise,这个坑后面会细说。
4. 避坑与排查:跑通这个项目最容易翻车的五个地方
4.1 爬虫被封或返回空列表
现象 :运行爬虫脚本后数据库里一条记录都没有,或者只抓到前两页就断了。 原因 :目标站点有频率限制或反爬机制,短时间内大量请求触发封禁;也可能是页面结构和你写的选择器不匹配。 解决 :在请求之间加 time.sleep(1) 到 time.sleep(3) 的随机延迟;检查 select 里的 class 名是否和当前页面一致;如果持续失败,直接换用 MovieLens 数据集,不要在这上面耗时间。
4.2 相似度矩阵内存溢出
现象 :电影数据超过 8000 条时,程序在 cosine_similarity 那一步卡死或报 MemoryError。 原因 :稠密矩阵是 N×N 的 float64,8000 部电影就是 512MB,加上中间计算副本很容易超过 2GB。 解决 :把数据类型转成 float32,内存直接减半;或者改用 NearestNeighbors(metric="cosine", algorithm="brute") 只算 Top-K 邻居,不存完整矩阵。
4.3 Django 静态文件 404
现象 :页面能打开,但所有 CSS 和 JS 都加载失败,样式全乱。 原因 : DEBUG=False 时 Django 不再自动服务静态文件,而 STATIC_ROOT 和 STATIC_URL 配置不正确。 解决 :开发阶段保持 DEBUG=True ;部署时运行 python manage.py collectstatic ,然后用 Nginx 把 /static/ 指向 STATIC_ROOT 目录,或者装 whitenoise 中间件。
4.4 推荐结果全是同一类型
现象 :不管用户看过什么,推荐列表里永远是“剧情”或“动作”占绝大多数。 原因 :TF-IDF 特征里类型字段权重过高,简介字段被稀释;或者相似度矩阵被少数高频类型主导。 解决 :在 feature_text 拼接时给类型和简介分别加权重,比如 genres * 2 + summary ;或者在推荐得分里加一个随机扰动因子,增加多样性。
4.5 用户历史为空时推荐报错
现象 :新用户没有任何观看记录,调用推荐函数时返回空列表或抛异常。 原因 : content_based_recommend 里 scores 全为零, argsort 取前 N 个还是零分电影,没有做冷启动处理。 解决 :在函数入口判断 user_history 是否为空,为空时直接返回按评分排序的热门电影列表,作为兜底推荐。
5. 进阶技巧:用缓存和增量更新把推荐响应压到 200ms 以内
推荐系统跑通之后,下一个瓶颈通常是响应速度。每次请求都重新算相似度矩阵不现实,我一般会做两层缓存:第一层是相似度矩阵本身,用 numpy.save 存成 .npy 文件,Django 启动时加载到内存;第二层是用户推荐结果,用 Redis 或 Django 的 cache 框架缓存 10 分钟,键用 user_id 加算法版本号。这样大部分请求直接命中缓存,响应时间能压到 200ms 以内。
# cache_utils.py
import numpy as np
from django.core.cache import cache
def load_similarity_matrix(path="sim_matrix.npy"):
"""加载预计算的相似度矩阵"""
return np.load(path)
def get_cached_recommend(user_id, algo_version="v1"):
"""带缓存的推荐结果获取"""
cache_key = f"rec:{user_id}:{algo_version}"
result = cache.get(cache_key)
if result is None:
result = compute_recommend(user_id) # 实际推荐逻辑
cache.set(cache_key, result, timeout=600)
return result
timeout=600 是 10 分钟,适合演示场景。如果电影库更新频繁,可以把相似度矩阵的更新做成定时任务,比如每天凌晨用 Celery 跑一次增量计算,只对新加入的电影算它与已有电影的相似度,然后追加到矩阵里。增量更新比全量重算快一个数量级,但要注意矩阵索引的映射关系不能乱。
另一个容易被忽略的点是推荐解释。答辩时老师经常会问“为什么推荐这部电影”,如果只给一个列表没有理由,说服力会打折扣。我习惯在推荐结果里附带一句解释,比如“因为你看了《星际穿越》,这部同样是科幻且评分接近”。实现方式是在 content_based_recommend 里记录得分最高的那部历史电影,把它的标题和类型拼进返回字段。这个改动很小,但演示效果提升明显。
从那以后我每次交付推荐系统项目,都会强制走一遍“空历史兜底、缓存命中、解释字段”这三项检查,缺一个都不算完整。希望帮到你。
本文还有配套的精品资源,点击获取




