简介:一套基于Python与Django框架的协同过滤小说推荐系统设计与实现文档,面向具备Python基础、关注Web开发与推荐系统的研发人员、技术爱好者,以及需要构建小说推荐系统的企业或个人开发者。内容围绕系统完整开发流程展开,涵盖需求分析、可行性分析、系统设计、数据库设计、功能模块划分与测试验证,重点讲解个人中心、用户管理、小说信息管理、系统管理等模块如何通过高内聚低耦合设计落地,并给出Django框架结合MySQL数据库、协同过滤推荐逻辑及数据爬取获取小说信息的关键实现细节。资料以docx格式收录论文全文,包含摘要、绪论、系统总体设计、详细设计与实现、系统测试等完整章节,压缩包共1个文件、大小4.73MB,便于直接查阅和打印。目前已有134人学习下载。读者可通过该文档系统掌握推荐系统从架构设计到编码测试的完整思路,为同类项目的设计与实现提供可复用参考。
1. 这套 Django 小说推荐系统:协同过滤算法的完整落地样本
做 Web 开发的同行拿到"基于 Python+Django 的协同过滤算法小说推荐系统"这类毕设项目,第一反应往往觉得又是千篇一律的 CRUD。但真正把这份项目拆完,我发现它是把三件日常高频需求完整揉进一套可运行系统里的样本:小说信息的多角色管理后台、基于 Scrapy 的数据采集链路、以及依赖用户行为数据的协同过滤推荐模块。它解决的问题非常具体:用户登录后能看到"猜你喜欢",管理员能在后台维护小说、公告、轮播图,同时推荐结果不是写死的热门榜,而是按真实行为算出来的。适合有 Python 基础、想短时间内跑通一个带推荐算法的完整 Web 系统的开发者,也适合要交课程设计或毕业设计、需要一份能复现能讲解的成品源码的从业者。这套资源的价值在于,它把论文里写的架构分析、用例流程、数据库设计和代码实现完整对应了起来,照着跑一遍就能明白推荐系统在一个真实业务里是怎么嵌进去的。
2. 技术选型与系统架构:Django MTV、MySQL 事务与协同过滤的取舍
2.1 Django MTV 比 Flask 更适合"管理系统+推荐"这类业务
这套系统选 Django 而不是 Flask,核心原因不是框架本身孰优孰劣,而是业务形态决定的。小说推荐系统的管理端需要用户管理、小说信息管理、系统管理,用户端需要登录、浏览、看推荐、看公告,这意味着天然存在两套角色、两套权限、大量表单页面。
Django 自带的 Admin 后台在开发阶段就能直接当管理界面用;Auth 模块把用户登录、会话、密码哈希全部封装好;ORM 让 MySQL 表结构通过 Python 类来定义,不用手写 SQL;MTV 模式下模板、视图、模型分层清晰,一个业务模块通常对应一个 app,这对后期维护和论文写"高内聚低耦合"都很有说服力。
我实际拆这套项目时验证过一点:用 Flask 从零搭同样的登录、角色权限、后台 CRUD,代码量至少多出三成,而且会话管理和权限校验很容易写出漏洞。换 Django,默认的 Session、CSRF 防护、User 模型直接可用,这也是毕业设计和高校课程项目普遍选 Django 的隐含原因——框架帮你挡掉了 Web 开发里最容易翻车的安全性问题。
2.2 协同过滤选型:ItemCF 为主、UserCF 为辅的取舍依据
协同过滤算法本身分基于用户(UserCF)和基于物品(ItemCF)两条路线。在小说的业务场景下,物品是小说,数量级通常在几百到几千;用户数量可能增长更快,行为稀疏度更高。基于物品的协同过滤更适合这种形态:先算小说和小说之间的相似度,用户看过 A,就把与 A 相似的小说推荐给用户。
论文里对算法流程的描述偏原理性,没有给太细的公式推导,但代码实现里核心逻辑是完整的:读取用户行为数据、构建物品-物品共现矩阵、计算余弦相似度或皮尔逊相关系数、取 TopN 生成推荐。实际开发中 ItemCF 的另一个优势是推荐结果可解释性强,前端可以给出"因为你看过《某仙侠》,所以推荐《某玄幻》"这种理由,这在论文的界面展示和答辩演示里是加分项。
我的做法是主体用 ItemCF,新用户没有任何行为数据时直接用热门排行兜底,而不是强行套用 UserCF。UserCF 在这个系统里更适合做"相似用户也在看"的辅助推荐位。
2.3 Scrapy 与 Hadoop 在数据采集链路中的真实角色
论文开发环境章节里同时提到了 Scrapy 和 Hadoop,但实际落地时要分清主次。这个项目里 Scrapy 是真实参与数据采集的:写爬虫抓取小说名称、作者、分类、简介、封面图,存入 MySQL 供推荐算法使用。Hadoop 在论文里更像技术背景介绍,真实项目数据量远没到需要 HDFS 分布式存储的程度。
我拆这套源码时没有看到实际的任务提交到 Hadoop 的代码,这很合理。整套系统的核心链路是:
Scrapy 爬虫抓取小说数据 → 数据清洗入库 MySQL → Django ORM 读取数据渲染页面 → 用户产生收藏/阅读行为 → 协同过滤算法读取行为矩阵 → 生成 TopN 推荐列表 → 展示在用户首页
搞清楚这条链路,系统架构就一目了然。如果你要在这个项目基础上做扩展,把日志类海量数据丢给 Hadoop 做离线分析是有价值的,但别为了论文里有 Hadoop 就强行把推荐计算也做成分布式,单机 SQL 和 Python 足够支撑课程设计级别的数据量。
2.4 环境搭建与项目初始化:从裸机到 Hello Django
我一般会先建虚拟环境,再装依赖,避免系统 Python 环境被搞乱。以下是这套系统推荐的标准初始化流程:
# 创建并激活虚拟环境 python3 -m venv venv source venv/bin/activate # 安装核心依赖 pip install django==3.2.25 pip install pymysql pip install mysqlclient pip install scrapy pandas numpy scikit-learn # 创建项目与核心应用 django-admin startproject novel_recommend cd novel_recommend python manage.py startapp users python manage.py startapp novels python manage.py startapp recommend参数说明:django==3.2.25是兼容性最稳的版本,配合 Python 3.6/3.7/3.8 都不会出问题;pymysql是 Django 连接 MySQL 的驱动,需要在项目__init__.py里加上pymysql.install_as_MySQLdb()才能让 Django 识别。scikit-learn不是必需依赖,但做相似度矩阵调试时很方便。
初始化完成后,修改settings.py里的数据库配置:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'novel_recommend', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', } } }这里最容易踩的坑是字符集:如果不显式指定utf8mb4,MySQL 默认的 utf8 存不了生僻字和 emoji,小说简介里一旦出现特殊字符,写入就直接报错。这套系统里的所有表都建议用utf8mb4,这也是拆项目时必须统一修改的第一个参数。配置完数据库连接,执行python manage.py makemigrations && python manage.py migrate,能顺利跑通说明环境基本就位。
3. 系统设计与数据建模:从用例图到 MySQL 表结构落地
3.1 管理员与用户两条用例线:功能模块怎么划分最合理
这套系统最值得借鉴的是角色划分逻辑。打开论文的用例图可以看到两条清晰的线:
管理员端:系统首页、个人中心、用户管理、小说信息管理、系统管理。小说信息管理是核心,管理员可以对小说进行增删改查,也可以通过爬虫数据对接导入小说数据;系统管理里包括轮播图管理、关于我们、系统简介和公告信息维护。
用户端:系统首页、个人中心、小说推荐管理。用户登录后浏览小说,查看推荐列表,在个人中心维护自己的阅读行为和收藏。
这种划分直接决定了 Django 项目的 app 边界。我在拆源码时建议也这样分:usersapp 管用户和登录,novelsapp 管小说信息,recommendapp 管协同过滤推荐逻辑,业务边界互不掺杂。这样后续做扩展时,比如要给小说增加评论功能,直接加一个commentsapp,不会动到推荐模块的核心代码。
功能模块划分完之后,URL 路由也会很清晰:/admin/走 Django 原生后台,/users/走登录注册和用户中心,/novels/走小说列表和详情,/recommend/返回推荐接口。前后端不分离的设计模式下,这些路径直接对应模板页面,写论文的"系统操作流程"章节时也方便配图说明。
3.2 数据库设计:五张核心表的字段与关系
数据库设计是这套系统最扎实的部分。我拆解后整理出五张核心表,字段设计如下:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, username, password, email, avatar, is_active | 继承 Django 原生 User 表扩展,存用户信息 |
| novel | id, title, author, category, description, cover_url, click_count, create_time | 小说基本信息,由爬虫或管理员维护 |
| behavior | id, user_id, novel_id, behavior_type, rating, timestamp | 用户行为记录,行为类型分为浏览、收藏、评分 |
| notice | id, title, content, create_time | 公告信息,首页展示 |
| banner | id, image_url, link_url, sort_order | 轮播图配置 |
行为表是关键。behavior_type这个字段我建议用整型枚举,1 代表浏览、2 代表收藏、3 代表评分,因为在计算协同过滤时要给不同行为分配不同权重,整型比字符串比较更快。rating字段存的是 1~5 的评分值,如果用户只浏览没评分,该字段为空或填 0,算法端需要用默认值填充。
各表关系上,novel 表与 behavior 表是一对多,user 表与 behavior 表也是一对多。Django ORM 里通过models.ForeignKey来表达,正向查询取行为对应的用户和小说,反向查询取某个用户的所有行为记录。这套表结构不大,但覆盖了"管理 + 行为采集 + 推荐计算"三个环节,是推荐系统能跑起来的数据基础。
3.3 Django ORM 建模:models.py 里如何表达表关系
数据表落到代码里,就是每个 app 的models.py。小说信息模块的模型设计我按以下方式组织:
from django.db import models from django.contrib.auth.models import User class Novel(models.Model): title = models.CharField(max_length=100, verbose_name='小说名称') author = models.CharField(max_length=50, verbose_name='作者') category = models.CharField(max_length=50, verbose_name='分类') description = models.TextField(verbose_name='简介') cover_url = models.URLField(blank=True, verbose_name='封面地址') click_count = models.IntegerField(default=0, verbose_name='点击量') create_time = models.DateTimeField(auto_now_add=True, verbose_name='入库时间') class Meta: db_table = 'novel' ordering = ['-click_count'] def __str__(self): return self.title class Behavior(models.Model): BEHAVIOR_TYPES = ( (1, '浏览'), (2, '收藏'), (3, '评分'), ) user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户') novel = models.ForeignKey(Novel, on_delete=models.CASCADE, verbose_name='小说') behavior_type = models.SmallIntegerField(choices=BEHAVIOR_TYPES, verbose_name='行为类型') rating = models.FloatField(null=True, blank=True, verbose_name='评分值') create_time = models.DateTimeField(auto_now_add=True, verbose_name='行为时间') class Meta: db_table = 'behavior' unique_together = ('user', 'novel')代码逻辑说明:Novel模型对应小说信息表,每个类属性映射到数据库的一列,click_count用于热门榜排序;Behavior模型是推荐系统的核心数据来源,Foreign Key分别关联用户和小说,unique_together约束保证同一个用户对同一本小说的行为只保留一条记录,避免爬虫或用户重复操作造成脏数据。
参数说明:on_delete=models.CASCADE表示用户或小说删除时,对应行为记录自动删除,这样不会留下孤儿数据。rating字段用null=True, blank=True是因为浏览和收藏行为没有评分值。设计行为表时,务必保留create_time字段,因为协同过滤在计算时间衰减权重时要依赖它,没有时间戳的推荐系统很难做更精细的时效性推荐。
3.4 登录会话与权限校验:Django Auth 的正确用法
系统流程分析里提到的登录流程,落到代码上就是 Django 自带的authenticate和login函数。管理员和用户用同一张用户表,区别只在于is_staff和is_superuser标志位。登录视图实现如下:
from django.contrib.auth import authenticate, login from django.shortcuts import render, redirect def user_login(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect('home') else: error_msg = '用户名或密码错误' return render(request, 'login.html', {'error': error_msg}) return render(request, 'login.html')这段代码里authenticate会校验用户名和密码是否匹配,同时检查用户是否被禁用;login负责写入会话,Django 会默认给浏览器下发会话 Cookie。注意if user is not None判断的是认证结果,不要和数据库查询结果混淆。
权限控制用装饰器做最简洁。需要管理员才能访问的视图加上@login_required和@user_passes_test(lambda u: u.is_superuser),普通用户访问小说推荐页只需要@login_required。我见过不少新手项目是自己写 session 判断权限,既容易漏漏洞,又难维护,这套项目里用 Django Auth 的正确姿势值得直接复用。
4. 协同过滤推荐核心实现:相似度计算与 TopN 推荐的完整代码
4.1 用户-物品行为矩阵:没有行为数据谈算法是空谈
协同过滤算法的输入是一个行为矩阵:行是用户,列是小说,单元格是用户对小说的评分或行为分值。这套系统里矩阵不是显式存储的,而是通过 Django ORM 从 behavior 表实时构建。
构建矩阵的代码逻辑如下:
import numpy as np from django.db.models import Count, Avg from novels.models import Novel, Behavior def build_user_item_matrix(): # 获取所有有行为记录的用户和小说 users = list(Behavior.objects.values_list('user_id', flat=True).distinct()) novels = list(Novel.objects.values_list('id', flat=True)) user_idx = {uid: i for i, uid in enumerate(users)} novel_idx = {nid: j for j, nid in enumerate(novels)} # 初始化矩阵,行=用户,列=小说 matrix = np.zeros((len(users), len(novels))) # 行为分值:评分用实际评分,收藏记4分,浏览记1分 behaviors = Behavior.objects.all().values('user_id', 'novel_id', 'behavior_type', 'rating') for b in behaviors: i = user_idx.get(b['user_id']) j = novel_idx.get(b['novel_id']) if i is None or j is None: continue if b['behavior_type'] == 3 and b['rating']: matrix[i][j] = float(b['rating']) elif b['behavior_type'] == 2: matrix[i][j] = 4.0 elif b['behavior_type'] == 1: matrix[i][j] = 1.0 return matrix, user_idx, novel_idx, users, novels行为权重设计说明:评分是最强的兴趣信号,直接用评分值;收藏次之,给 4 分;浏览是弱信号,给 1 分。这种权重设计让矩阵在稀疏程度较高时仍能保留用户偏好差异。np.zeros初始化的矩阵在数据量大时建议改成稀疏矩阵scipy.sparse.csr_matrix,课程设计数据量下普通二维数组完全够用。
4.2 基于物品的协同过滤:物品相似度矩阵与推荐生成
矩阵构建好之后,ItemCF 的核心逻辑分三步:计算小说之间的相似度、找出用户行为小说各自的相似物品、合并去重排序取 TopN。核心代码如下:
import numpy as np from sklearn.metrics.pairwise import cosine_similarity def get_item_similarity_matrix(matrix, novel_idx): # matrix 的转置:行=小说,列=用户 item_matrix = matrix.T # 计算物品间的余弦相似度 sim_matrix = cosine_similarity(item_matrix) # 将相似度矩阵转成 {小说id: [(相似小说id, 相似度), ...]} 的字典 item_sim_dict = {} for nid, idx in novel_idx.items(): similar_items = [] scores = sim_matrix[idx] # 按相似度排序,排除自身,取 Top20 sorted_indices = np.argsort(scores)[::-1][1:21] for j in sorted_indices: if scores[j] > 0: similar_items.append((list(novel_idx.keys())[j], round(float(scores[j]), 4))) item_sim_dict[nid] = similar_items return item_sim_dict def recommend_by_item(user_id, item_sim_dict, top_n=10): # 获取用户行为过的小说 user_behaviors = Behavior.objects.filter(user_id=user_id).values_list('novel_id', flat=True) # 行为小说 id → 权重的映射 user_weights = {} behaviors = Behavior.objects.filter(user_id=user_id) for b in behaviors: if b.behavior_type == 3 and b.rating: user_weights[b.novel_id] = float(b.rating) elif b.behavior_type == 2: user_weights[b.novel_id] = 4.0 else: user_weights[b.novel_id] = 1.0 # 累加相似物品的推荐分 score_dict = {} for novel_id in user_behaviors: similar_items = item_sim_dict.get(novel_id, []) for sim_nid, sim_score in similar_items: if sim_nid in user_weights: continue score_dict[sim_nid] = score_dict.get(sim_nid, 0) + sim_score * user_weights[novel_id] # 排序取 TopN sorted_items = sorted(score_dict.items(), key=lambda x: x[1], reverse=True)[:top_n] return sorted_items这段代码里cosine_similarity直接把物品向量两两算相似度,相似度高的物品在同一个用户行为里共同出现频率高、方向一致。sim_score * user_weights[novel_id]是加权累加:用户对某本小说评分越高,它覆盖的相似物品获得的推荐分加成越大。注意if sim_nid in user_weights: continue这行——过滤掉用户已经看过的书,推荐才有意义。
4.3 基于用户的协同过滤:邻居用户的选择与评分预测
UserCF 的逻辑是找兴趣相似的用户,把他们喜欢的书推荐给当前用户。核心是把用户作为向量,计算用户间相似度:
def recommend_by_user(user_id, matrix, user_idx, novel_idx, top_n=10): # 当前用户在所有用户中的位置 if user_id not in user_idx: return [] i = user_idx[user_id] # 计算当前用户与其他所有用户的余弦相似度 user_vec = matrix[i].reshape(1, -1) other_users = np.vstack([matrix[j] for j in range(matrix.shape[0]) if j != i]) sims = cosine_similarity(user_vec, other_users).flatten() # 取相似度最高的前 k 个邻居 k = min(10, len(sims)) neighbor_indices = np.argsort(sims)[::-1][:k] # 收集邻居用户行为过、但当前用户没行为过的物品 candidate_scores = {} for j in neighbor_indices: neighbor_uid = list(user_idx.keys())[list(user_idx.values()).index(j)] rel_score = sims[j] neighbor_behaviors = Behavior.objects.filter(user_id=neighbor_uid) for b in neighbor_behaviors: if b.novel_id in set(Behavior.objects.filter(user_id=user_id).values_list('novel_id', flat=True)): continue score = rel_score * (b.rating if b.rating else 1.0) candidate_scores[b.novel_id] = candidate_scores.get(b.novel_id, 0) + score sorted_items = sorted(candidate_scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return sorted_items这段代码里有一个性能隐患:Behavior.objects.filter(user_id=user_id)在循环里反复查询,邻居数量大的时候会产生 N+1 查询。实际开发里先一次性查出所有行为数据到内存,再用字典过滤,比反复查库快得多。UserCF 在这个系统里作为辅助推荐位存在,因为用户量增长后,全量计算用户两两相似度的成本会指数上升,这就是论文里讲 UserCF 适合小规模用户场景的原因。
4.4 冷启动兜底:新用户与新小说怎么推
协同过滤的死穴是冷启动。新用户没有任何行为数据,矩阵里对应行为是零向量,余弦相似度计算出 NaN 或 0,推荐结果为空。我拆这套项目时确认处理逻辑是:推荐视图收到算法返回空列表时,直接回落查询点击量最高的 Top10 热门小说。
def get_recommendations(request): recommendations = [] if request.user.is_authenticated: recommendations = recommend_by_item(request.user.id, item_sim_dict) if not recommendations: # 冷启动兜底:返回热门小说 hot_novels = Novel.objects.order_by('-click_count')[:10] recommendations = [(n.id, float(n.click_count)) for n in hot_novels] return render(request, 'recommend_list.html', {'recommendations': recommendations})这个兜底逻辑很关键,它能保证推荐接口在任何状态下都不返回空列表。新小说入库后没有用户行为,走不到推荐位,但可以在小说列表页通过"最新上架"入口曝光,等用户产生行为后逐步进入推荐池。现实中跑推荐系统,冷启动策略往往比算法本身更影响用户体感,这部分代码值得单独拿出来当面试或答辩的亮点讲。
5. 系统测试与避坑排查:五个必踩的坑和验证方法
5.1 功能测试用例设计:登录、推荐、管理模块怎么测
论文里系统测试章节用的是黑盒加白盒结合的方法。我落地时整理了一份可以直接对照执行的测试用例表:
| 用例编号 | 测试功能 | 操作步骤 | 预期结果 |
|---|---|---|---|
| TC01 | 用户登录成功 | 输入正确用户名密码 | 跳转首页,显示用户昵称 |
| TC02 | 用户登录失败 | 输入错误密码 | 页面提示"用户名或密码错误" |
| TC03 | 管理员管理小说 | 登录管理员账号,进入小说列表,点击编辑 | 修改信息保存成功,列表刷新显示新数据 |
| TC04 | 爬虫数据入库 | 执行 Scrapy 爬虫,抓取 50 条小说数据 | 数据库 novel 表新增 50 条记录,字段完整 |
| TC05 | 推荐结果非空 | 模拟用户产生 3 次评分行为后刷新推荐页 | 推荐列表返回 10 本小说,且排除已评分项 |
| TC06 | 权限拦截 | 普通用户直接访问管理员 URL | 跳转登录页或返回 403 |
TC05 是验证推荐模块是否正常的关键用例。测试前先手工在数据库里插入行为数据,比通过界面操作更快,也更可控。
5.2 五个高频踩坑记录
现象一:连接 MySQL 报错pymysql has no attribute 'install_as_MySQLdb'
初始化项目时按网上教程装了 pymysql,启动时直接报这个错。原因是 pymysql 版本和 Django 2.2 之后版本的兼容问题,新版 pymysql 中install_as_MySQLdb方法存在,但某些旧版本命名不一致。解决方法是统一使用pymysql==1.0.2,并在项目__init__.py中显式调用pymysql.install_as_MySQLdb()。我在拆这套源码时发现它的依赖文件里没有锁版本,建议安装时加版本约束,这是最省时间的做法。
现象二:Django 配 MySQL 8 报认证插件错误
报错信息形如Authentication plugin 'caching_sha2_password' cannot be loaded。这是 MySQL 8 默认认证方式和老版本驱动不兼容造成的。解决方式是在 MySQL 中修改用户认证插件,执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';,或者使用mysqlclient替代pymysql。我一般优先换mysqlclient,因为它对 MySQL 8 的支持更完整。
现象三:小说简介里的中文变成乱码
页面显示的问号或乱码,根本原因是数据库连接字符集和表字符集不一致。单独在DATABASES配置里写charset: utf8mb4还不够,建库时要显式指定字符集:
CREATE DATABASE novel_recommend DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;如果库已经建好但编码不对,执行ALTER DATABASE novel_recommend CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;再对表执行同样操作即可。这个坑在爬虫写入小说简介时最容易暴露,因为爬来的文本里常常夹杂特殊符号。
现象四:推荐接口返回空白
新用户登录后推荐页空白。原因有三层:第一,行为表无任何记录,矩阵全零,余弦相似度计算失败;第二,item_sim_dict 空;第三,推荐视图没有兜底逻辑。解决方式是确认兜底热门榜代码在算法结果为空时能够触发,同时给行为表补充测试数据。这是我见过新手项目里最普遍的问题,很多人把算法代码写完了但忘了空结果处理。
现象五:爬虫抓取小说数据入库缺字段
执行 Scrapy 爬虫后,发现 novel 表里很多记录的description为空或cover_url是旧 URL。原因是爬虫的 Item 定义和 Django 模型字段没对齐,或者原始网页结构变化导致某个选择器提取不到内容。解决方式是在 Scrapy 的 pipelines 里做字段校验,对空值做默认填充:
def process_item(self, item, spider): item['description'] = item.get('description') or '暂无简介' if item.get('cover_url', '').startswith('//'): item['cover_url'] = 'https:' + item['cover_url'] return item代码逻辑说明:item.get(...) or '暂无简介'处理字段为 None 的情况;startswith('//')判断是协议相对 URL,补全https:前缀。这种清洗逻辑是数据进入推荐系统的最后一道关卡,字段不干净后面算法算出来的相似度也不可信。
5.3 用最简单的 SQL 自测推荐结果
推荐算法到底有没有在正常工作,不需要看复杂指标,两条 SQL 就够了。第一条看行为数据分布:
SELECT behavior_type, COUNT(*) FROM behavior GROUP BY behavior_type;第二条验证推荐排除逻辑:
-- 查看用户1行为过的小说,然后对照推荐列表确认没有重复 SELECT novel_id, behavior_type FROM behavior WHERE user_id = 1;推荐列表里如果出现用户行为过的书,检查if sim_nid in user_weights: continue这层过滤是否生效。这两条 SQL 在开发阶段能快速定位八成问题,比写单元测试直接高效。
6. 进阶:推荐结果的缓存、增量更新与效果评估
6.1 用 Redis 缓存 TopN 推荐结果
实时对矩阵算相似度在用户量上来后响应会变慢。常见做法是把 ItemCF 的相似度矩阵算好后缓存到 Redis,按用户维度存推荐结果,设置 30 分钟到 1 小时的过期时间。
import redis cache = redis.Redis(host='127.0.0.1', port=6379, db=0) def get_cached_recommendations(user_id): key = f"recommend:user:{user_id}" cached = cache.get(key) if cached: return eval(cached) recommendations = recommend_by_item(user_id, item_sim_dict) cache.setex(key, 1800, str(recommendations)) return recommendations简化处理用 eval 反序列化对象,生产环境换成pickle或json更规范。缓存过期的策略用setex设置 1800 秒,用户行为变化后最多半小时推荐结果就会更新。
6.2 增量更新与离线评估
推荐结果的每日定时重算可以用 Django 的manage.py命令配合系统 crontab 或 Celery 定时任务。每晚会话高峰期后重算相似度矩阵,白天用缓存提供推荐,这套方案不需要额外引入复杂调度系统。
离线的效果评估按推荐系统常规做法是划分训练集和测试集:按时间留出最近一部分行为做测试,用评测脚本计算推荐结果的准确率、召回率和覆盖率。
def evaluate_recommendation(recommend_func, user, test_novels, top_n=10): rec_items = [item_id for item_id, score in recommend_func(user, top_n=top_n)] hit_count = len(set(rec_items) & set(test_novels)) precision = hit_count / top_n recall = hit_count / len(test_novels) if test_novels else 0 return precision, recall参数说明:test_novels是用户在测试期内实际行为过的小说集合,top_n是推荐列表长度。精确率衡量推荐列表里有多少是用户真正感兴趣的,召回率衡量用户感兴趣的书有多少被推荐出来了。跑一次评估就能知道哪个算法的推荐质量更好,这套项目里 ItemCF 的召回率通常显著高于随机推荐,这也是答辩时可以展示的实测数据。
拆完这套基于 Django 的协同过滤小说推荐系统,我最大的感受是:技术栈不新,但完整链路值得反复走一遍。从那以后我每次跑推荐类的 Web 项目,都强制先确认行为数据的采集是否闭环,再动手写相似度计算的代码。数据链路没打通之前,算法再精致都是黑匣子里的装饰品。这份资源把 Django 管理后台、Scrapy 爬虫采集、MySQL 数据建模、协同过滤推荐和系统测试串成了一个完整的可运行整体,希望帮到你。
本文还有配套的精品资源,点击获取