Python构建电影推荐系统:KNN协同过滤与API接口实战

发布时间:2026/9/7 16:15:35
Python构建电影推荐系统:KNN协同过滤与API接口实战 作为一个靠Python吃饭的人我太清楚“算法”和“API”这两个词对新手意味着什么了。很多人学完基础语法、爬虫、数据分析那一套之后会觉得“我啥都会了但啥也做不出来”。而电影推荐系统这个项目恰好就是打通“理论”到“实战”的那座桥尤其是当你走到Day 90这个节点再回头看自己写的第一行print(Hello World)那种成就感会非常真实。这个项目上下两篇我是一口气做完的。上篇我们处理了数据、做了EDA、搭了简单的规则推荐基线这篇直接上硬货用KNN算法实现协同过滤推荐再写一套能往外抛的API接口。你可以把这个项目理解为“给懂Python语法但没做过完整项目的人准备的毕业设计”它的核心价值不是教你调库而是让你明白一个推荐系统从“算出来”到“用起来”要经历哪些环节以及每个环节里的坑我都替你踩过了。1. 算法选型为什么我最终只用了KNN和协同过滤1.1 推荐不是猜你喜欢而是找到同类如果你去翻市面上讲推荐系统的书上来就是矩阵分解、深度交叉网络、GBDTLR这些确实厉害但对一个刚刚在“99天精通Python”学习路径里走到第90天的人来说是灾难。为什么因为它们背后的数学推导和工程细节太多很容易让你陷入“调包侠”的误区——模型跑通了但完全不知道发生了什么。我个人的建议是项目练手阶段就抓住一条核心主线基于近邻的协同过滤Nearest Neighbor-Based Collaborative Filtering。它的思想说穿了就是一句话人类做决策时最喜欢参考“同类”的意见。看电影也一样如果用户A和用户B过去看过的十部电影里评分趋势高度一致那用户A给一部新片打了高分大概率用户B也会喜欢。这里面有两个分支需要你在“选型”时就心里有数User-Based CF找“和我口味相似的人”把这些人喜欢的电影推荐给我。优点是直观缺点是当用户量巨大时实时计算用户相似度成本极高而且新用户没有任何行为记录时无法计算。Item-Based CF找“和我看过电影相似的电影”也就是“喜欢A的人通常也喜欢B”。这个思路更适合电影这类物品数量相对稳定、用户行为稀疏的场景而且相似度矩阵可以离线算好、缓存住线上只做查表响应快得多。那标题里提到KNN它是干嘛的KNN不是一种独立的推荐算法而是一种“找邻居”的工具。上面两种CF思路都要解决同一个问题给定一个用户或一部电影怎么从几十万用户或几千部电影里挑出最相似的K个这个用暴力计算当然也能做但KNNsklearn.neighbors.NearestNeighbors帮我们把这步包装好还给你提供了多种距离度量方法。1.2 数据集的取舍MovieLens为什么是最好的练手数据算法再漂亮没有数据就是空谈。我强烈建议使用MovieLens 1M数据集做这个项目。原因有三第一它足够干净。数据是从真实电影评分平台匿名化处理过的字段包括userId, movieId, rating, timestamp没有乱七八糟的缺失值和脏数据你不需要把宝贵的时间花在清洗上。第二它足够大。100万条评分、约6000个用户、4000部电影这个量级做KNN协同过滤单机内存完全吃得下又能暴露出真实场景下的性能问题比如相似度矩阵计算慢、用户冷启动等。第三它足够经典。推荐系统领域的论文十篇里八篇拿它当基准数据集。你用这个数据集跑出来的结果是可以和其他公开模型横向对比的这比你自己爬一个野数据集有说服力得多。注意如果你用的是MovieLens最新版如ml-latest-small文件名和字段略有差异但核心的ratings.csv结构基本一致。拿到数据后第一件事不是写代码而是确认数据行数、用户数、电影数这决定了你后续相似度矩阵的大小。1.3 为什么不直接上SVD或深度学习把话说透SVD奇异值分解这类矩阵分解方法在推荐精度上确实通常优于KNN协同过滤尤其处理稀疏矩阵时优势明显。但它有一个“劝退点”——结果不可解释。你推荐了一部电影用户问你为什么你只能说“模型算出来的”这在实际产品里是很尴尬的。而KNN给出的推荐是天然可解释的因为《黑客帝国》和《盗梦空间》在10000个用户的行为里表现得像双胞胎所以你看过前者我就推荐后者。这种可解释性对学习阶段非常重要它让你每一步都能回头验证而不是对着一个黑箱调参。深度学习就更不用说了光环境配置、训练时间、GPU显存这三大坑就够你折腾一周。这个项目的目标是“99天内做出能用的东西”不是“发论文考满分”。把KNN吃透再理解协同过滤的两种玩法你的算法基本功就扎实了。2. KNN算法从数学原理到sklearn落地2.1 KNN找邻居距离度量定生死KNN的全称是K-Nearest NeighborsK个最近的邻居。它的整个逻辑建立在“距离”这个概念上。你得先想明白什么样的两只股票算相似什么样的两个用户算相似什么样的两部电影算相似——在数学上全部转化成“距离远近”的问题。我实现推荐系统时最常用的三种距离度量是这样的切记根据业务场景选欧氏距离最直观想象三维空间里两个点的直线距离。在协同过滤里如果两个用户对同一批电影打分分别是(5, 1, 3)和(4, 2, 3)它们的欧氏距离就是sqrt((5-4)²(1-2)²(3-3)²)。欧氏距离对绝对数值敏感适合维度不高、数值密集的场景但用在评分数据上容易被“评分尺度不同的用户”干扰。比如甲习惯打2分到5分乙习惯打1分到4分明明品味相似欧氏距离也会偏大。余弦相似度计算两个向量的夹角余弦值范围-1到1数值上越接近1越相似。它只看方向、不看长度恰好缓解了上述“评分尺度不同”的问题所以在用户协同过滤里我默认都用余弦相似度。皮尔逊相关系数其实是“中心化后的余弦相似度”它把每个用户的评分减去自己的平均分再算余弦。这样彻底解决了用户评分习惯不同的问题有人严苛有人慷慨但缺点是你需要先对每个用户做一次mean centering计算成本略高。我相信你注意到重点了KNN本身不背锅真正决定推荐质量的是你选的相似度度量。很多人调了半天K值没用其实是距离函数选错了。2.2 sklearn版KNN不是你想的那样直接用网上搜“python knn”相关的内容搜出来全是KNeighborsClassifier做鸢尾花分类。你要是拿它直接套推荐系统会发现根本没法用。因为分类器有标签、有训练集和测试集的概念而协同过滤里的“邻居查找”是无监督的——我们不需要预测类别只需要找到相似的向量。正确做法是用sklearn.neighbors.NearestNeighbors。一段最核心的代码长这样from sklearn.neighbors import NearestNeighbors import pandas as pd import numpy as np # 假设你已经有了 user_item_matrix行为用户ID列为电影ID值为评分缺失为0 # shape 大概长这样(6040, 3706) model_knn NearestNeighbors(metriccosine, algorithmbrute, n_neighbors21, n_jobs-1) model_knn.fit(user_item_matrix) # 核心knn对象fit之后直接query就能返回“最近的邻居索引”和“距离” distances, indices model_knn.kneighbors(user_item_matrix.iloc[user_index:user_index1], n_neighbors21)为什么algorithmbrute这是我在实践中踩过坑之后才明白的。sklearn里的NearestNeighbors有四种算法brute暴力计算、kd_tree、ball_tree、auto。KD树和Ball树在高维数据上可以做剪枝、加速听起来很美好但它们在高维稀疏场景下性能衰减极快甚至可能退化成暴力计算还更慢。而推荐系统的user-item矩阵动辄上千维里面还全是0所以我直接指定brute老老实实全量计算在1M数据集、6000多用户这个量级反而最稳定、最可控。另外注意一个细节n_neighbors21。为什么不是默认的5这里有个容易被忽略的问题——你要做推荐取出来的邻居里通常包含用户自己自己和自己相似度永远是1所以在代码里要把它过滤掉。我习惯取K1个邻居然后indices[0][1:]才是真正的K个邻居。代码里的21对应最终取20个有效邻居。2.3 构建用户-物品矩阵前的关键一步透视表与稀疏化光有原始ratings.csv不能直接喂给KNN必须先转成矩阵。过程很简单但有两个细节会直接影响算法效率user_item_matrix ratings.pivot_table(indexuserId, columnsmovieId, valuesrating).fillna(0)第一这个矩阵默认非常稀疏6040个用户乘3706部电影大约有98%的位置是0。如果直接存成普通numpy数组内存大约 6040×3706×8字节 ≈ 179MB看起来还能接受但当你把电影数扩大到几千部甚至上万部时内存就爆炸了。所以我建议使用scipy.sparse.csr_matrix来存储训练时再转回密集或用稀疏接口。第二.fillna(0)是个危险操作因为0在推荐语义里意味着“没看过”而不是“打了0分”。好在余弦相似度的计算对0值的处理恰好是我们想要的两个用户如果只是在不同的电影上有评分重叠部分少相似度就低。但你要心里清楚0填充不代表真实反馈它只是为了让矩阵不是一堆NaN。实操心得我测试过在1M数据集上用稀疏矩阵做KNN训练大概一两秒单次查询不超过50毫秒。但用稠密矩阵的话查询时间虽然不是瓶颈内存吃紧却会拖慢你后续开发API时开多个进程的脚步。3. 三种推荐玩法我全做了一遍并给了结论3.1 快速出结果的User-Based CF函数封装“物以类聚人以群分。”这句老话就是User-Based CF的注释。我封装了一个函数输入一个用户ID返回推荐电影列表。它的核心步骤拆出来只有四步你可以直接抄走改造def user_based_recommendation(user_id, n_recommendations10, K20): # 1. 计算目标用户和其他所有用户的相似度 # 这里就是靠 NearestNeighbors 已经 fit 好的模型 distances, indices model_knn.kneighbors( user_item_matrix.loc[user_id].values.reshape(1, -1), n_neighborsK1 ) # 2. 剔除自己得到K个相似用户及其相似度 similar_user_ids indices[0][1:] sim_scores 1 - distances[0][1:] # 余弦距离转相似度 # 3. 收集这K个用户评过分的电影按“相似度×评分”加权 candidate_scores defaultdict(float) for neighbor_id, sim in zip(similar_user_ids, sim_scores): neighbor_ratings ratings[ratings[userId] neighbor_id] for _, row in neighbor_ratings.iterrows(): candidate_scores[row[movieId]] sim * row[rating] # 4. 去掉目标用户已经看过的电影按总分排序输出 watched set(ratings[ratings[userId] user_id][movieId]) recommendations [mid for mid in sorted( candidate_scores, keycandidate_scores.get, reverseTrue ) if mid not in watched][:n_recommendations] return recommendations这套代码看起来短但它是标准的UserCF流程。有个地方我要划重点sim_scores 1 - distancessklearn里的kneighbors返回的是距离不是相似度。我当时用余弦距离时距离范围是[0, 2]所以转换成相似度的公式我直接写了1 - 距离。其实更严谨的做法是用cosine_similarity包或者从距离换算相似度的标准公式但实测在推荐排序任务里1-distance已经够用因为KNN只关心相对大小而不是绝对数值。这套流程的缺点也真实存在每次要遍历邻居的评分记录做加权如果K值取大一点整个函数会比较慢。你要是做实时在线推荐就必须把这步的中间结果缓存下来。3.2 更稳的Item-Based CF要离线算物品相似度在真实产品里Item-Based CF是更常见的方案。道理很简单电影的数量几千远小于用户的数量几十万物品间的相似度矩阵计算一次、离线存好之后每次推荐只需要查表速度飞快。物品相似度矩阵的计算思路和用户版几乎对称item_matrix ratings.pivot_table(indexmovieId, columnsuserId, valuesrating).fillna(0) item_knn NearestNeighbors(metriccosine, algorithmbrute, n_neighbors11, n_jobs-1) item_knn.fit(item_matrix) # 推荐时找出目标用户看过的每部电影的相似电影 def item_based_recommendation(user_id, n_recommendations10, K10): user_watched ratings[ratings[userId] user_id][movieId].tolist() user_rated ratings[ratings[userId] user_id].set_index(movieId)[rating].to_dict() candidate_scores defaultdict(float) candidate_count defaultdict(int) for movie_id in user_watched: distances, indices item_knn.kneighbors( item_matrix.loc[movie_id].values.reshape(1, -1), n_neighborsK1 ) similar_movies indices[0][1:] sim_scores 1 - distances[0][1:] for sim_movie, sim_score in zip(similar_movies, sim_scores): if sim_movie movie_id: continue # 加权累积时把用户给原电影的评分作为权重 candidate_scores[sim_movie] sim_score * user_rated[movie_id] candidate_count[sim_movie] 1 # 过滤已看过的按候选得分归一化排序 ...ItemCF的优势还不止响应速度快。它还有一个二阶优势推荐结果更容易解释。“因为你喜欢《流浪地球》而《疯狂的外星人》与它高度相似系统才推荐给你。”这句话在用户侧展示出来比“因为你喜欢”更有说服力也方便做推荐理由的UI展示。它的稍显复杂之处在于item_matrix.loc[movie_id]这一行的维度是用户数比用户版查询时的一行维度是电影数要大不少。但因为是离线批量计算所以成本完全可控。在我做的实际测试里MovieLens 1M数据集上构建物品相似度矩阵只需几十秒查询一次推荐列表CPU时间不到100毫秒。3.3 混合策略加权融合比单一模型更扛揍我做完两种CF之后做了个简单的混合推荐实验结论是不要迷信单一模型加权融合能明显提升推荐列表的“像那么回事”程度。最简单有效的混合方法是加权融合。假设你已经用UserCF算出了一部电影的综合得分score_u用ItemCF算出了同一部电影的得分score_i那最终得分可以这么算final_score alpha * score_u (1 - alpha) * score_ialpha 默认取0.5但要根据场景调整。对新用户行为很少ItemCF的参考价值更高因为用户行为稀疏时基于用户相似度去匹配邻居基本是瞎蒙而对老用户行为很多UserCF能抓到更鲜活的口味变化因为它参考的是和你当前最相似的那些人而不是静态的物品相似关系。我实际项目里会动态设 alpha min(0.8, 用户评分数量 / 100)评分越多越偏重UserCF。还有一种更省事的混合策略叫“切换式混合”用户历史评分少于5条走基于流行度的冷启动补全5到30条走ItemCF超过30条UserCF权重加大。这样做的复杂度最低每个分支的代码都已经单独验证过只在调度层做条件判断非常适合你在这个阶段练手。提示混合推荐的目标不是“精确”而是“稳定”。你会发现单一模型偶尔能给出一两个惊艳的推荐但也会出现一连串垃圾结果。混合之后虽然单次效果可能被平均了但整体不会翻车这在产品里比灵光一现重要得多。4. Flask API开发把模型变成所有人能用的服务4.1 选Flask还是FastAPI我给你的建议这个项目的API开发部分很多人会纠结Flask和FastAPI。我的观点是学习阶段用Flask生产意识用FastAPI。Flask更简单、资料更多、心智负担更低适合你把注意力放在“怎么把推荐函数暴露成HTTP接口”这件事上。FastAPI虽然自带接口文档、异步支持和类型校验确实很香但它引入的概念Pydantic模型、async/await、依赖注入对新手来说是额外的认知负担容易分散精力。但这不代表你应该只写Flask不碰FastAPI。我建议你先用Flask把项目跑通再花半小时用FastAPI重写一遍感受两边的差异。下面的示例我直接用FastAPI写因为代码更现代、更短而且写完自带Swagger文档方便你用浏览器调试。4.2 最小可用API三个接口撑起整个服务我设计的API只有三个接口覆盖了推荐系统的核心交互查询电影、获取推荐、提交评分。from fastapi import FastAPI, HTTPException from pydantic import BaseModel import pandas as pd import uvicorn app FastAPI(titleMovie Recommendation API) # 全局加载模型和数据只在服务启动时加载一次 ratings pd.read_csv(data/ratings.csv) movies pd.read_csv(data/movies.csv) model, user_item_matrix, item_item_matrix load_recommender_models() class RatingInput(BaseModel): user_id: int movie_id: int rating: float app.get(/) def root(): return {message: Movie Recsys API is running, status: ok} app.get(/recommend/{user_id}) def recommend(user_id: int, top_n: int 10): if user_id not in user_item_matrix.index: raise HTTPException(status_code404, detailUser not found) recs get_recommendations(user_id, top_ntop_n) return {user_id: user_id, recommendations: recs} app.post(/rate) def rate_movie(input: RatingInput): # 生产环境会把评分写入数据库这里先返回确认信息 return {message: Rating received, **input.model_dump()}有几点经验必须分享模型和数据的加载必须在模块导入时完成一次不能放在每个请求里加载。我在第一次写的时候把load_recommender_models()写进了recommend函数里结果每个请求都要重新读一遍600MB的数据慢到怀疑人生。FastAPI的BaseModel是Pydantic的模型用来做请求体校验。我故意加了RatingInput这个类就是让你养成“对外暴露的数据结构要明确”的习惯。用户给了一个超范围的评分比如10分Pydantic默认就能拦下来不用你自己写if。不要直接返回numpy.int64、numpy.float32这类类型因为JSON序列化时会报错。我的做法是在get_recommendations函数里统一把结果转成Python原生int和float或者直接一行jsonable_encoder()搞定。所以我在 final 版代码里会写这样的技巧在recommend接口的内部用recommendations [int(i) for i in recs]强制转换别偷懒。4.3 提升API性能的三个关键配置当你用uvicorn main:app --reload启动这个API之后响应速度也许能接受但如果要部署到生产有三件事必须做预计算相似度矩阵并序列化KNN模型和相似度矩阵每次启动时重新训练纯属浪费。我用joblib.dump()把训练好的item_knn和user_item_matrix存成二进制文件服务启动时直接joblib.load()启动时间从十几秒降到不到一秒。给推荐结果加缓存同一个用户短时间内反复请求结果一样的话没必要重算。我用了functools.lru_cache虽然不能跨进程共享但对单进程的API服务已经能挡住一大半重复请求。代码大概是from functools import lru_cache lru_cache(maxsize1024) def cached_recommend(user_id: int, top_n: int 10): ...注意被lru_cache装饰的函数参数必须是可哈希的所以我把user_id和top_n都设成整数完全没问题。接口层做推荐理由拼接ItemCF模型天然适合解释我在API返回结构里加了一个reason字段比如“因为你看过《The Matrix》它与《Inception》的相似度达0.87”。这不增加任何算法复杂度但产品价值能立刻提升一个档次。4.4 别忘了写好启动配置文件这不是“锦上添花”而是“必须品”。我把项目根目录下的启动脚本写成一个Makefile或者简单的shell脚本核心就一句话uvicorn main:app --host 0.0.0.0 --port 8000但经验告诉我生产环境不能裸奔。起码要加两个参数--workers 4启动多个worker进程能扛住并发请求--log-level info输出必要的日志。另外如果需要部署在Docker里记得在镜像里先pip install -r requirements.txt并把数据文件放进镜像不要在容器启动时临时下载数据网络一抖你整个服务就起不来。5. 常见问题与排查技巧实录5.1 冷启动到底怎么破我在开发API的过程中第一个遇到的大坑就是冷启动新用户没有任何评分user_item_matrix.loc[new_user_id]直接KeyErrorAPI返回500错误。冷启动有几种解法从简单到复杂依次是流行度兜底用户行为为空时直接返回全站平均评分最高的电影列表。这个列表可以提前算好存成JSON查询时零成本。注册时收集偏好标签让新用户点选喜欢的类别动作、科幻、喜剧然后用每个类别下的高分电影作为初始化推荐。这需要你在电影数据里维护类别字段MovieLens的movies.csv里的genres字段是竖线分隔的解析一下就行。基于内容的特征匹配如果用户有注册时提交的手机型号、常驻城市等画像数据可以和电影的特征国家、导演、演员做相关性匹配。但这个超纲了本阶段实现前两种即可。在我这个项目里我直接在/recommend/{user_id}接口里先判断if user_id not in user_item_matrix.index: return popular_movies(top_n)这样至少保证新用户调用接口不会报错而且返回的推荐列表有基本质量。5.2 API报500的排查思路开发期最常见的错误类型是数据类型错误。比如ratings.pivot_table得到的列名是movieIdint类型但你在ItemCF计算时取的indices返回的是位置索引0, 1, 2...不是真实的movieId我当时卡了一个多小时推荐出来一堆莫名其妙的电影ID一查发现indices[0][1:]是item_matrix的行号但item_matrix是pivot之后的行和原始movieId已经不对应了必须用item_matrix.index[indices[0][1:]]才能映射回真实的电影ID。排查手段方面我强烈建议在API开发时打印user_id和top_n这两个参数的原始值和类型。另外FastAPI的uvicorn --reload会自动加载你改过的代码但如果你用了lru_cache旧结果可能还会被命中我测试时经常因为缓存没清而看到“旧版本”的推荐结果。这是新手最不容易察觉的坑。我把常见问题整理成一个速查表你可以直接收藏现象根因解决方式接口报500 KeyError用户/电影ID不存在于矩阵中判断ID是否在index中不在则走兜底推荐结果全是电影ID错乱位置索引和真实ID混淆用matrix.index[indices]做映射返回JSON报错numpy类型无法序列化全部转成int/float服务启动慢每次启动都重新训练KNN用joblib.dump/load缓存模型新用户无推荐冷启动问题返回流行度推荐兜底内存占用过高稠密矩阵太大改用scipy.sparse.csr_matrix相似度全是负的余弦距离转相似度公式错误检查1-distance还是1/(1distance)5.3 相似度计算慢到怀疑人生在电影推荐系统的开发中最消耗时间的操作就是计算用户间或物品间的相似度。我实测过用NearestNeighbors的brute模式对6000用户进行全量查询单次请求大约200毫秒但你如果天真地对整个矩阵手动双重for循环那要跑好几个小时。这就是为什么我坚持用sklearn封装好的KNN而不是自己写距离函数的原因——它底层做了批量向量化计算直接用BLAS加速效率差了不止一个数量级。如果你数据量继续增大到10万用户brute模式也会变成瓶颈。我的扩展思路是先降维再用KD树。比如用TruncatedSVD把几千维的user-item向量压缩到50维再输入KD树。但注意这个手段会损失一定精度需要在推荐质量上做一些A/B测试。本阶段项目不推荐搞这么复杂但你可以把这条路记在笔记里作为后续进阶方向。5.4 数据版本控制的严肃提醒很多人忽略一个问题推荐模型是和数据文件强绑定的。如果你把ratings.csv更新了一部分内容但缓存的相似度矩阵还是旧的API返回的结果会和新数据“对不上”。我自己就踩过这个坑白天跑了一个新脚本给ratings.csv追加了5000条数据晚上API还在用旧的joblib模型文件做推荐结果用户明明新打了分推荐列表一点变化都没有。解决办法非常简单粗暴给数据文件加版本号模型文件名带上数据版本例如item_knn_v20240601.joblib。数据的任何更新都生成一个新版本模型文件加载模型时以版本名为准。这听起来低级但能帮你避免一堆摸不着头脑的bug。我建议你在模型文件旁边放一个metadata.json记录数据集版本、训练时间、K值、距离度量等信息将来你想复盘某个推荐结果对不对一查元数据全明白了。6. 部署上线前最后要做的事模型写完了API也通了距离“项目完成”还差一口气那就是测试。不要偷懒至少写三个测试用例测试正常用户ID能返回10条推荐测试不存在用户ID不会崩溃而是走兜底逻辑测试评分提交以后入库成功且能反映到下一次推荐。def test_recommend_normal_user(): response client.get(/recommend/1) assert response.status_code 200 assert len(response.json()[recommendations]) 10 def test_recommend_unknown_user(): response client.get(/recommend/999999) assert response.status_code 200 assert len(response.json()[recommendations]) 10 def test_rate_submission(): response client.post(/rate, json{user_id: 1, movie_id: 1, rating: 5.0}) assert response.status_code 200测试通过后再用gunicorn或者uvicorn启动生产服务。我习惯用gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app因为gunicorn管理worker进程更成熟配合Nginx做反向代理和负载均衡就是一套很标准的Python服务部署方案。等你把这些流程都走完回头再看整个电影推荐系统项目你写了算法、做了API、配了测试、填了部署这已经不是一个“练习题”而是一个可以摆上简历的完整项目了。我个人在实际操作中的体会是这个项目最核心的价值不是推荐算法本身——毕竟KNN在工业界早就不是最优解了——而是你亲身体会了一条完整的产品链路从原始数据到离线模型从离线模型到在线服务从在线服务到对应产品功能。很多人在“学Python”这件事上花了很多时间却一直没有“做出东西”的感觉就是因为他们缺少这条链路的完整闭环。你坚持到Day 90把这个推荐系统做出来后面再学任何高阶算法心里都有一张地图知道自己学的每一块砖该往哪砌。最后再分享一个小技巧如果是自己练手接口文档别只靠Swagger尝试写一个“外部调用示例”的markdown把返回的JSON截图贴进去过一个月再回来看你会感谢当初自己的细致。

相关新闻