2026/10/7 5:35:22

Django + 协同过滤:从零构建电影推荐系统实战

Django + 协同过滤:从零构建电影推荐系统实战 看到这个项目标题我第一反应是“老朋友了”。Django和协同过滤这两样东西在Web开发和推荐算法领域都是教科书级别的经典组合但真正能把两者干净利落地粘合在一起做成一个能跑、能看、能演示的完整系统并不是简单拼凑一个“前后端CRUD 一个算法脚本”那么轻松。这个项目我前前后后搭过不止一版踩过的坑远比写出来的代码多今天就把整套思路、算法落地细节、以及那些文档里不会写的教训一起拆开聊聊。先说清楚这个系统能解决什么问题适合谁参考。如果你正在学Django但练手的项目还停留在博客、投票器这种“增删改查”层面那这个项目能让你真正接触到“业务逻辑变复杂之后数据层和算法层如何分层、Django的ORM场景会怎样被拉伸”的实战体验如果你在看推荐算法但对“协同过滤如何在真实数据上跑起来、效果如何评估、工程上有什么坑”没有直观感觉那这个项目就是一块比较理想的试验田。它选豆瓣电影作为场景是因为电影评分数据稠密、口味特征显著、用户行为维度简单清晰几乎是所有推荐系统教材的首选数据集形态。以下内容按我实际做项目的顺序展开先把方案选型和架构定下来然后深入到算法细节接着看Django如何一步步承接算法最后是调试记录。整个过程尽量还原我当时是怎么想的、为什么这么做以及哪些地方是可以直接拿去抄作业的。1. 项目整体设计与技术选型思路1.1 为什么用Django来做推荐系统先说技术选型。现在要做推荐系统可选的技术栈太多了Python后端框架有FastAPI、Flask算法层面有现成的Surprise库、Spark MLlib甚至各种深度推荐模型。但“Django 自己实现协同过滤”这套组合看起来朴实其实是性价比最高的一个方案。Django自带的ORM、Admin后台、模板引擎、迁移机制能在极短时间内把一套“数据存储 后台管理 前端页面”的完整链路搭起来。做推荐系统最耗时的地方其实不在算法本身而在于数据的获取、清洗、落库、展示和联调Django恰恰把这些环节都封装得很顺手。配合它的Admin后台你甚至不需要写一行前端代码就能可视化管理用户、电影和评分数据。对算法部分自己手写协同过滤而不是直接用Surprise库最大好处是你能真正掌握算法内部每个环节——相似度怎么算、评分怎么预测、冷启动怎么兜底——出了问题能自己揪出来而不是对着黑盒调参。FastAPI性能更好但开发效率和生态成熟度在这个体量下并不比Django占优直接上深度推荐模型复杂度完全没必要。Django的“全家桶”模式让你把精力集中在业务逻辑和算法本身上这一点在个人项目、毕设或者团队内部分享Demo的场景里是最重要的。1.2 协同过滤为什么适合电影场景推荐系统的思路大致分两类基于内容Content-Based和基于协同过滤Collaborative Filtering。基于内容要先给电影打标签、抽取特征比如导演、演员、类型、剧情关键词然后算电影之间的内容相似度。这套方案有个天然短板你需要结构化的特征数据特征工程的工作量很大而且用户一旦看过几部相似风格的片子系统就容易一直推类似的东西多样性很差。协同过滤的核心出发点不一样它不关心电影长什么样只关心“用户的行为数据”。它的底层假设很简单过去口味相似的人未来也大概率喜欢相同的东西或者用户会更喜欢和他已经喜欢过的物品相似的物品。放到豆瓣电影的语境下就更好理解了。系统不需要知道《肖申克的救赎》是一部越狱题材的电影它只需要知道“看过这部片子的人往往也给《教父》打高分”那这两部电影就可以被关联起来。评分数据、收藏行为、点击序列这些都是协同过滤的“燃料”。豆瓣电影的用户评分行为非常典型——用户多、评分多、电影多没有复杂的内容标签体系也能做出相当亮眼的推荐效果。1.3 数据从哪来公开数据集与预处理策略做这个项目第一道坎就是数据。豆瓣官方没有开放的评分API直接去爬的话既涉及合规问题又容易被反爬机制限制而且爬下来还要处理大量的数据清洗工作性价比很低。我当时的选择是使用公开的电影评分数据集最经典的是MovieLens系列。MovieLens是明尼苏达大学GroupLens团队开源的电影评分数据集有100K、1M等多个版本格式很简单userId,movieId,rating,timestamp。还有配套的movies.csv包含电影标题和类型标签。网上有很多网盘和GitHub仓库都提供下载用起来非常省心。数据集下载后就涉及导入Django的环节。需要注意原始数据集的用户ID是整数电影标题是英文居多如果要做成豆瓣风味的中文展示可以把电影标题映射成中文或者混入一部分手工整理的中文电影数据源让效果看起来更贴近“豆瓣电影”的感觉。导入策略上我强烈建议用Django的loaddata命令配合一个自定义的import脚本而不是手工SQL。因为Rating表有一个Django ORM外键关联处理关联ID和批量写入要同时考虑性能。下面这段脚本就是一个很典型的批量导入模板import csv from django.core.management.base import BaseCommand from movies.models import Movie, User, Rating class Command(BaseCommand): help 导入MovieLens数据集 def handle(self, *args, **options): # 先清空旧数据 Rating.objects.all().delete() Movie.objects.all().delete() User.objects.all().delete() # 导入用户 with open(data/users.dat, r) as f: reader csv.reader(f, delimiter:) users [] for row in reader: users.append(User(usernamerow[0])) User.objects.bulk_create(users, batch_size1000) # 导入电影 with open(data/movies.csv, r) as f: reader csv.DictReader(f) movies [] for row in reader: movies.append(Movie( titlerow[title], genresrow[genres] )) Movie.objects.bulk_create(movies, batch_size1000) ...注意几个关键点用bulk_create批量写入避免几千条数据逐条INSERT导致导入脚本慢到怀疑人生数据导入前要清空旧表否则外键冲突会让人一头雾水用户和电影的导入顺序要放在评分之前因为评分记录依赖两边的自增主键。这些细节看着琐碎但每一步都会在实际执行中找上你。2. 核心算法协同过滤的实现细节2.1 评分矩阵的构建从ORM到二维结构所有协同过滤算法的起点都是用户-物品评分矩阵。矩阵的行是用户列是电影单元格是评分。但Django的ORM查询返回的是扁平的对象列表所以第一步要做的是把关系型数据“重塑”成一个更适合算法运算的矩阵结构。我最初直接用嵌套字典来存矩阵外层键是用户ID内层键是电影ID值就是评分。这样做的好处是能保持稀疏结构——真实场景里矩阵绝大多数单元格都是空的用户不可能给所有电影打分。稀疏结构节省内存而且后续计算相似度时可以只对有交集的部分做运算。def build_user_movie_matrix(): 从Rating表构建 用户 - {电影ID: 评分} 的嵌套字典 ratings Rating.objects.all().values(user_id, movie_id, score) user_movie_matrix {} for r in ratings: user_movie_matrix.setdefault(r[user_id], {})[r[movie_id]] r[score] return user_movie_matrix这个函数看起来只有几行但它是整个算法和Django连接的桥梁。我可以负责任地说在把这个矩阵结构理清楚之前后面所有算法代码都处于“看着对、跑起来全是问题”的状态。同样逻辑如果要跑基于物品的协同过滤只需要再把矩阵转置成“电影 - {用户ID: 评分}”的结构。2.2 相似度计算余弦相似度与皮尔逊相关系数矩阵有了下一个核心问题就是如何量化两个用户或两部电影之间的相似程度。这直接决定了推荐质量的上限。我实际对比过两种最主流的方式。余弦相似度看名字可能觉得陌生但它的直观含义就是“两个向量在空间中方向一致的程度”。把用户A对N部电影的评分看成一个N维向量用户B是另一个N维向量两个向量的夹角越小余弦值越接近1说明口味越接近。注意余弦相似度计算时每个用户向量包含的是真实评分值如果两个用户打分习惯差别大——一个全打4分以上一个打分范围在1到5之间——余弦相似度会受这个整体偏移影响。皮尔逊相关系数可以理解为“先把每个用户自己的打分均值中心化再算余弦相似度”。它衡量的是两个变量之间的线性相关程度能有效规避不同用户打分宽严尺度不同带来的偏差。这在电影评分场景里非常管用因为现实中就是有人爱给高分有人手紧。下面是两个函数的核心实现这段代码是整个项目算法层的源头值得仔细看import numpy as np def cosine_similarity(vec1, vec2): 余弦相似度输入两个 {item_id: score} 的字典 common set(vec1.keys()) set(vec2.keys()) if len(common) 2: return 0.0 dot sum(vec1[c] * vec2[c] for c in common) norm1 np.sqrt(sum(v ** 2 for v in vec1.values())) norm2 np.sqrt(sum(v ** 2 for v in vec2.values())) if norm1 0 or norm2 0: return 0.0 return dot / (norm1 * norm2) def pearson_similarity(ratings1, ratings2): 皮尔逊相关系数先中心化再算相似度 common set(ratings1.keys()) set(ratings2.keys()) if len(common) 2: return 0.0 mean1 sum(ratings1[c] for c in common) / len(common) mean2 sum(ratings2[c] for c in common) / len(common) numerator sum((ratings1[c] - mean1) * (ratings2[c] - mean2) for c in common) denom1 np.sqrt(sum((ratings1[c] - mean1) ** 2 for c in common)) denom2 np.sqrt(sum((ratings2[c] - mean2) ** 2 for c in common)) if denom1 0 or denom2 0: return 0.0 return numerator / (denom1 * denom2)这里有个隐藏很深但极其重要的点为什么共同评分项少于2就直接返回0因为如果两个人只共同评过一部电影皮尔逊相关系数计算出的结果恒为满分或零分实际上是数学假象。比如A给电影X打2分、B给X打2分他俩相似度就是100%这显然没有统计学意义。这个阈值不要设在1以下我实测在MovieLens 100K数据集上设为2到5之间效果比较稳定。2.3 UserCF与ItemCF两种风格两套取舍协同过滤算法落地的时候有两个经典路线基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF。我当时把两个都实现了因为两者在电影场景下各有优势而且代码量差别不大。UserCF的核心思路找到和你评分习惯最相似的K个用户然后把这K个用户喜欢但你还没看过的电影推荐给你。它的社交隐喻很像“口味相似的朋友给你安利”。实现时对于目标用户u和一部电影p如果u已经看过p那就不需要预测否则就找所有给p打过分的其他用户计算他们和u的相似度取TopK再基于这些相似用户的评分做加权预测。ItemCF的核心思路则反转了过来如果用户A喜欢《星际穿越》那系统会去找和《星际穿越》最相似的一批电影——评价“喜欢《星际穿越》的人往往也喜欢这些片子”——然后推给A。这种方法在电影这种“物品数量巨大但用户兴趣相对稳定”的场景下推荐结果的稳定性和可解释性都更好这也是亚马逊和豆瓣在商品/电影推荐中更偏好的路线。下面是我写的UserCF预测评分核心实现def predict_user_cf(user_id, movie_id, user_movie_matrix, k10): 基于用户的协同过滤评分预测 if user_id not in user_movie_matrix: return None if movie_id in user_movie_matrix[user_id]: return user_movie_matrix[user_id][movie_id] similarities [] target_ratings user_movie_matrix[user_id] for other_user, other_ratings in user_movie_matrix.items(): if other_user user_id: continue if movie_id not in other_ratings: continue sim pearson_similarity(target_ratings, other_ratings) if sim 0: similarities.append((sim, other_user)) similarities.sort(keylambda x: x[0], reverseTrue) top_k similarities[:k] if not top_k: return None # 均值归一化加权预测 target_mean np.mean(list(target_ratings.values())) numerator 0 denominator 0 for sim, other_user in top_k: other_ratings user_movie_matrix[other_user] other_mean np.mean(list(other_ratings.values())) numerator sim * (other_ratings[movie_id] - other_mean) denominator sim if denominator 0: return None return target_mean numerator / denominator这段代码里最值得注意的不是循环和排序而是最后那几行的“均值归一化加权预测”。如果直接把相似用户的原始评分拿来做权重平均那打分偏高用户的推荐结果会绑架全站。加上“减去该用户均值、预测时再加回目标用户均值”这一层处理相当于把每个用户的打分尺度拉到同一个基准线上再比较效果在实测中会明显上升。ItemCF的实现路径基本对称只是把矩阵换成电影到用户的映射相似度换成“计算电影之间的评分向量相似度”预测公式换成“目标电影已知评分者权重平均”。两个路线选哪种核心取决于数据规模和业务场景。如果用户变动频繁、物品相对稳定ItemCF的计算和更新更轻量如果物品数远大于用户数、且用户口味随群体趋势变化强UserCF可能更能捕捉潮流。实际项目里两者已经衍生出很多混合加权方案可以都跑一遍再对比效果。2.4 协同过滤的三大经典问题冷启动、稀疏性、扩展性算法能做出来是一回事能上线扛住场景是另一回事。做这个项目时我几乎同时撞上了协同过滤的三个经典问题也说下我的应对方式。冷启动新用户没有任何评分记录算法拿什么算相似度新电影刚开始没有人给它打过分它就不会被推荐。最简单的兜底策略是对新用户直接推荐全站热度最高的Top20电影对评分记录少于某个阈值的用户直接走热门推荐不进入协同过滤流程。这个策略虽然“暴力”但至少保证用户打开首页时不会看到一片空白。稀疏性用户-物品矩阵中已评分的元素占比往往只有个位数百分比。两个用户共同评过分的电影少相似度计算就不可靠。缓解方式可以是“填充中性评分”或者在相似度计算时要求共同评分数量不少于5否则就降低该相似度的权重。还有一种思路是先用热门电影作为“锚点”扩大用户共同评分的概率但这会牺牲长尾多样性需要权衡。扩展性假设有1万用户、1万部电影构建一个用户间两两相似度矩阵就是1万乘1万量级的运算量。协同过滤的在线计算代价随着矩阵规模呈平方级增长。我当时的方案是把相似度矩阵的计算从“每次请求时实时算”改成“离线预计算 存入Redis缓存”。Django服务启动时或定时任务里先算好相似度矩阵推荐请求直接查缓存拿TopK邻居在线响应时间能缩短一个数量级。3. Django项目落地从数据模型到推荐接口3.1 数据模型设计三张表撑起整个系统数据库是推荐系统的底座。这个项目只需要三张核心表用户表、电影表、评分表。模型代码如下from django.db import models class User(models.Model): username models.CharField(max_length50, uniqueTrue) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.username class Movie(models.Model): title models.CharField(max_length200) genres models.CharField(max_length200, blankTrue) release_year models.IntegerField(nullTrue, blankTrue) def __str__(self): return self.title class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameratings) movie models.ForeignKey(Movie, on_deletemodels.CASCADE, related_nameratings) score models.FloatField() created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, movie) indexes [ models.Index(fields[user, movie]), ]这里有几个设计细节值得展开。unique_together是必须加的保证同一个用户对同一部电影只有一条评分记录避免数据重复导致算法出现莫名其妙的结果。related_name是给自己方便的因为后续Orm查询会频繁用到user.ratings.all()这样反向关联的写法。indexes是给查询提速用的当评分表涨到几十万行时没有索引的Rating.objects.filter(userxxx)会慢到让你怀疑Django的水平。3.2 推荐引擎模块与Django解耦是关键做这个项目最容易犯的错误是把算法逻辑直接写进views.py。一旦视图里塞满了相似度计算、排序逻辑项目很快就会失控——因为视图函数同时承担了HTTP请求处理、算法计算、业务编排三件事改算法会影响页面展示调样式又会碰算法。我后来把推荐核心逻辑抽成了一个独立的服务模块recommend_engine.py放在项目的utils目录下视图只负责调用。这个模块大致长这样import hashlib import json from django.core.cache import cache class RecommendEngine: def __init__(self, matrix_builderbuild_user_movie_matrix): self.user_movie_matrix matrix_builder() self.item_user_matrix self._transpose(self.user_movie_matrix) self.user_sim_cache {} self.item_sim_cache {} def _transpose(self, matrix): 转置用户-电影矩阵为电影-用户矩阵 result {} for user, movie_ratings in matrix.items(): for movie_id, score in movie_ratings.items(): result.setdefault(movie_id, {})[user] score return result def recommend_for_user(self, user_id, top_n20): ...核心思路是把相似度计算、矩阵构建、TopN推荐流程都封装在引擎内部对外只暴露一个recommend_for_user接口。Django视图拿到用户ID调用引擎的接口拿到电影ID列表再去数据库查询电影详细信息传给模板。这样以后哪怕把引擎整体替换成基于矩阵分解的实现页面代码都不需要改动。3.3 视图与模板让推荐结果用户可见接口和引擎都齐了需要一个实际可访问的页面来承载推荐结果。我设计了三个主要页面首页展示热门电影用户登录后看到针对他的个性化推荐电影详情页展示“看过这部片子的用户还喜欢什么”。视图函数的核心逻辑from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required from .models import Movie from .utils.recommend_engine import RecommendEngine engine RecommendEngine() login_required def recommend_view(request): user_id request.user.id # 用户没有评分记录时走热门兜底 has_ratings request.user.ratings.exists() if not has_ratings: hot_movies Movie.objects.annotate( avg_scoremodels.Avg(ratings__score) ).order_by(-avg_score)[:20] return render(request, recommend.html, { movies: hot_movies, is_personalized: False }) movie_ids engine.recommend_for_user(user_id, top_n20) movies Movie.objects.filter(id__inmovie_ids) # 自定义排序保持算法输出的顺序 order_map {mid: idx for idx, mid in enumerate(movie_ids)} movies sorted(movies, keylambda m: order_map.get(m.id, 999)) return render(request, recommend.html, { movies: movies, is_personalized: True })这段代码里有三个十分关键的隐藏细节。第一个判断用户有没有评分记录用request.user.ratings.exists()而不是Rating.objects.filter(useruser_id).count() 0。exists()在ORM层面会翻译成LIMIT 1性能远超count()统计全表行数。第二个从引擎拿到电影ID列表后不能直接filter(id__inmovie_ids)就交给模板因为“IN查询”返回的结果顺序不保证和传入的ID顺序一致。必须用order_map重新排序把算法精心计算出来的推荐顺序保留住。第三个视图里声明了一个全局单例的engine RecommendEngine()。这一个设计让矩阵和相似度缓存在进程生命周期内复用避免每个请求都去数据库拉全量数据重建矩阵否则并发上来极易内存溢满或CPU打满。3.4 Django Admin后台免费的算法调试工具Django Admin在这个项目里不是摆设它是极其得力的调试工具。把Movie、Rating、User三个模型注册进Admin后你能直接在后台看到某部电影有多少人评过分、某个用户的评分分布、单条评分记录长什么样所有数据一目了然。更重要的是Admin的自动后台可以用来做“手动干预”。比如某一个冷门电影想让它被推荐曝光直接在后台批量给几个“种子用户”添加评分。这在调试展示效果时非常有用比在Python Shell里手动敲数据直观得多。4. 实操中的坑常见问题与排查记录4.1 ORM查询效率坑N1查询怎么绕开项目刚跑通时页面加载一次竟然要好几秒。查下来发现罪魁祸首是模板里循环渲染每部电影时都调用了movie.ratings.all()去查评分。Django ORM遇到这种场景会在循环内逐条发起SQL查询产生经典的N1问题1次主查询加N次关联查询。这个坑几乎所有Django开发者都会踩只是体感在数据量上来之后才特别明显。解决方式很简单用prefetch_related或select_related。对于外键关联用select_related对于反向外键集合用prefetch_related。比如查热门电影加平均分时movies Movie.objects.annotate( avg_scoremodels.Avg(ratings__score) ).prefetch_related(ratings)[:20]这个改动之后SQL从“查询20次”降为“查询2次”页面响应时间直接腰斩。4.2 相似度计算内存爆掉的经历我的第一个版本是在视图函数里实时计算用户相似度矩阵数据量到了几千用户、几百部电影时就明显卡顿。用户间两两组合的计算量是平方级的在Python纯循环下慢得令人崩溃。后来我把相似度矩阵的构建放到Django的定时任务里我用的django-crontab计划任务每小时重建一次计算完成后存入缓存在线请求只从缓存读取。同时对全量用户做了一次筛选只保留评分数量大于等于5的“活跃用户”参与计算把相似度矩阵的规模限制在10万以内。如果你在本地复现建议先从MovieLens 100K数据集的1/10开始跑通全流程再逐步放大数据。不要一上来就全量跑否则相似度计算这一步就会让你失去耐心。4.3 冷启动与稀疏矩阵下相似度失真项目上线后用几个测试账号测试发现一个新注册的账号点开推荐页系统直接返回了空列表。排查之后发现推荐引擎对“没有评分的用户”没有做任何兜底用户评分矩阵里找不到该用户就直接返回空。这让我加上了前面提到的“热门推荐兜底”逻辑。另一个比较隐蔽的问题是两个用户只在某一部超级热门的电影上共同打过分皮尔逊相关系数算出来是1.0导致他们被误判为口味完全相同。解决方式是把“共同评分数量不足”的相似度直接置0或降低权重避免一部电影的“巧合”影响全局判断。4.4 常见问题速查表这个表是我在实际调试过程中反复用到的重要记录直接放出来问题现象可能原因解决方案推荐结果全为空用户无评分记录热门推荐兜底推荐结果顺序每次都变集合型查询未附加排序使用order_map调整顺序页面加载耗时长N1查询 / 在线计算相似度prefetch_related 离线计算算法效果比对随机还差相似度计算中未做均值归一化使用皮尔逊相关系数某些用户永远拿不到推荐评分太稀疏找不到近邻设定最小共同评分数量新电影不参与推荐无评分冷启动热门榜基于内容的简单兜底这张表的核心价值在于“看到现象能快速定位原因”。推荐系统是一个由数据、算法、工程三个层面串联起来的整体任何一层出问题最终的呈现都是“推荐不准”或“推荐不出来”所以排查时要学会分层定位。5. 效果评估与优化方向5.1 算法效果怎么量化MAE与RMSE在做完一个推荐系统后不能只说“感觉效果还行”需要一套客观的量化指标。我的做法是把评分数据集按8:2拆分成训练集和测试集训练集用于计算相似度和做预测测试集用来检验系统预测的评分和用户真实评分之间的误差。最常用的两个指标是MAE平均绝对误差和RMSE均方根误差。MAE计算的是预测值和真实值差值的绝对值平均RMSE则对“离谱的误差”惩罚更重——大误差会被平方放大。举例来说如果系统预测《霸王别姬》的评分是4.8而用户真实给了3.5那单个误差就是1.3。MAE会更直观RMSE则能暴露“偶尔预测得很离谱”的问题。实测中基于皮尔逊相关性的UserCF在MovieLens 100K上MAE能做到0.75左右已经属于不错的水平。5.2 还有哪些优化空间矩阵分解与混合推荐当协同过滤框架跑通后你可以把它当作一个很好的起点继续往更高级的方向演进。如果厌倦了手写UserCF和ItemCF可以往两个方向升级一是矩阵分解SVD用隐向量的方式将巨大且稀疏的评分矩阵分解成两个低维稠密矩阵能在降维的同时更精准地捕捉用户和物品的隐藏特征二是混合推荐把基于内容的特征如电影类型、导演、演员与协同过滤的推荐结果做加权融合用内容特征解决冷启动问题。这些方向都不是重新造轮子而是在现有Django项目的引擎模块里做替换。推荐引擎接口一旦设计得干净换成什么算法都不会伤筋动骨。这也是我在项目里坚持“把引擎单独抽出来”的根本原因。5.3 实际心得推荐系统是“数据工程”多于“算法”最后想分享一个比较真诚的感受。很多刚接触推荐系统的人都以为核心是算法模型但当你亲手做一遍这个项目你就会发现最耗时、最麻烦的其实不是算法推导而是整个数据链路的建设和管理数据导入是否可靠、评分记录的更新是否及时、相似度缓存何时失效、冷启动策略如何优雅地兜底。推荐系统本质上是一个数据驱动的工程问题。算法只是整个系统的“最上层建筑”底下越是扎实上层越能发挥威力。这个项目做完后我最大的收获不是背熟了协同过滤的公式而是理解了“数据质量决定推荐上限工程架构决定推荐下限”这句话的分量。数据量小、质量差时再好的算法都是空中楼阁数据链路通顺了哪怕只用一个基础的协同过滤也足以做出让用户觉得“确实懂我”的推荐体验。如果你也想练手这个项目我的建议是先把数据跑通、把Django的模型设计好、把相似度计算逻辑吃透再逐步优化算法本身——这条路走一遍比看十篇教程都有用。