
携程美食数据推荐系统说实话第一次在毕设题目里看到这个组合的时候我第一反应是这到底是Python项目还是Java项目因为Django和SSM同时在标题里挂着怎么看怎么别扭。等真正把项目做完、论文写完、调试跑通之后我才理解这个题目的意思核心系统用PythonDjango实现而SSM那套是作为对比方案或者扩展说明写在文档里的本质上是一个完整的推荐系统课题技术栈只是手段推荐算法和数据流才是真正的灵魂。这篇文章我就以“做过一遍的人”的视角把这个项目从算法原理、数据准备、Web端实现到部署避坑完整拆一遍。不管你是在做毕设、课设还是单纯想练手搞一个数据推荐系统的完整案例这篇都能当成一份靠谱的参考。我会把核心代码思路、表结构设计、踩过的坑都贴出来照着做基本能跑通。1. 项目拆解这东西到底做了什么1.1 标题里的技术栈怎么理解先把这个很多人一上来就懵的点说清楚。标题里“PythonDjangoSSM”三个词放一起乍一看像是同一个项目同时用了Python和Java两套技术。实际做的时候主流方案是这样的核心系统基于Python 3.8以上版本后端Web框架用Django数据库用MySQL推荐算法用Python自己写协同过滤。SSM是SpringSpringMVCMyBatis的Java框架组合在这个项目里的角色通常有两种理解方式。第一种你可以在论文的“技术选型对比”章节里把Django和SSM做对比分析说明为什么选Python系第二种如果你精力够可以用SSM做一个简单版本的管理后台跟Django主系统形成对照。我当时选的是第一种因为把两个框架都完整实现一遍工作量太大而且容易顾此失彼。论文里把对比写清楚答辩老师反而会觉得你考虑全面。更重要的是推荐系统本身才是核心Django只是承载推荐的壳子把精力花在算法和数据处理上性价比高得多。1.2 核心功能模块和整体数据流向这个系统的功能说白了就是“让用户打开美食推荐页面看到自己可能喜欢的餐厅和菜品”。它拆成模块大概是这么几个数据采集模块从公开渠道获取携程美食频道的数据包括餐厅名称、所在城市、人均消费、评分、评论内容、标签等。这一步可以选择真实爬虫也可以用现成数据集。数据存储模块把采集到的数据和用户行为数据按设计好的表结构存进MySQL包括用户表、餐厅表、评分表、评论表、推荐结果表等。推荐引擎模块核心部分基于协同过滤算法计算用户之间的相似度找出相似用户爱吃的餐厅生成Top-N推荐列表同时处理冷启动场景。Web展示模块用Django写前台页面包括首页推荐流、餐厅详情、评分评论、用户登录注册以及最简单的后台管理功能。Django Admin作为轻量级后台管理餐厅数据、用户数据、推荐结果的展示和人工干预。数据流向可以理解成一条链子用户注册登录以后产生行为数据浏览、评分、收藏这些行为数据实时写进MySQL推荐引擎定时或者实时从库里取最新的用户-餐厅评分矩阵跑协同过滤算法算出每个用户的候选餐厅列表结果再写回推荐结果表用户刷新首页的时候Django视图直接查推荐结果表把Top-N餐厅展示出来。这里有个关键点推荐结果不要每次都实时全网重新算一遍那样响应时间扛不住尤其是在数据量上去以后。合理的做法是离线计算在线读取也就是定时任务跑算法结果落表页面只做查询。后面讲到实现的时候会再说这个。2. 推荐算法选型和核心原理2.1 为什么是协同过滤而不是深度学习现在的推荐算法看着很热闹深度学习、知识图谱、大模型什么的都有。但作为毕设或练手项目协同过滤依然是性价比最高的选择。原因很简单数据量不够深度学习根本训不动协同过滤对数据量的要求低得多而且可解释性强——你可以在页面上写“因为你看过XX餐厅所以推荐你XX”这种直观逻辑在答辩时也容易讲清楚。协同过滤分两种基于用户的UserCF和基于物品的ItemCF。我做这个美食项目时两者都撸了一遍最后线上用的是基于用户的协同过滤因为美食推荐有个特点口味这东西人的圈子属性特别强。你身边喜欢吃辣的朋友推荐的火锅店大概率比全网热门榜更适合你——这就像现实生活中你问朋友“附近有什么好吃的”一样UserCF就是在模拟这个“问朋友”的过程。当然纯UserCF也有问题就是用户量少、行为稀疏的时候相似用户算不准。所以我在最终方案里做的是一个简单的混合策略冷启动用户走热门榜随机探索老用户走UserCF结果如果UserCF算出来结果太少再补充热门榜垫底。这个“保底”逻辑在实际效果上比纯算法重要得多。2.2 基于用户的协同过滤代码实现核心逻辑分三步构建用户-餐厅评分矩阵计算用户间相似度找Top-N相似用户并生成推荐列表。代码大概是这样我先用纯Python实现一遍方便理解算法本身import pandas as pd import numpy as np from collections import defaultdict # 1. 读取评分数据 # 数据格式: user_id, restaurant_id, rating ratings pd.read_csv(ratings.csv) # 2. 构建用户-餐厅评分矩阵 user_item_matrix ratings.pivot_table( indexuser_id, columnsrestaurant_id, valuesrating, fill_value0 ) # 3. 计算用户之间的皮尔逊相似度 def pearson_similarity(matrix): # 矩阵转置后计算列之间的相关性 # 每一列对应一个用户但为了方便计算我们先构建用户相似度矩阵 user_sim np.corrcoef(matrix) return pd.DataFrame( user_sim, indexmatrix.index, columnsmatrix.index ) user_sim_df pearson_similarity(user_item_matrix) # 4. 为目标用户生成推荐列表 def recommend_for_user(user_id, user_sim_df, user_item_matrix, top_n10, k5): # 找到与目标用户最相似的K个用户 sim_scores user_sim_df[user_id].sort_values(ascendingFalse) sim_scores sim_scores.drop(indexuser_id) # 去掉自己 top_k_users sim_scores.head(k) # 统计这K个用户评分过的餐厅按相似度加权计算得分 scores defaultdict(float) for other_user, sim_score in top_k_users.items(): # 获取相似用户评分过的餐厅 other_user_ratings user_item_matrix.loc[other_user] rated_items other_user_ratings[other_user_ratings 0].index for item in rated_items: # 目标用户没吃过的才推荐 if user_item_matrix.loc[user_id, item] 0: scores[item] sim_score * other_user_ratings[item] # 排序取Top-N sorted_scores sorted(scores.items(), keylambda x: x[1], reverseTrue) return [item for item, score in sorted_scores[:top_n]]这个实现很好理解难点其实不在代码本身而在两个容易被忽略的地方。第一个是评分矩阵的稀疏性问题。比如1000个用户、2000家餐厅全矩阵就是200万个格子但实际有评分的可能只有几万条大部分是0。用pivot_table直接生成的稠密矩阵光存储就浪费很多内存。数据量小的时候无所谓数据量一大你就会发现矩阵膨胀得离谱计算也慢。优化思路是改用稀疏矩阵存储比如scipy.sparse的csr_matrix或者干脆只存非零评分计算用户相似度的时候只取共同评分过的餐厅。第二个是相似度算法选择。皮尔逊相关系数适合评分数据是连续值的情况但如果你拿到的数据只有“喜欢/不喜欢”这种布尔值更合适的是Jaccard相似度。Jaccard算的是两个用户共同评分过的餐厅数除以他们评分过的餐厅总数公式简单效果也直观。我当时两种都试了最后发现皮尔逊系数在连续评分数据上表现更好Jaccard在数据稀疏时更稳。2.3 相似度计算和阈值的确定这里有个实操细节值得单独说。计算用户相似度的时候并不是所有相似用户都值得信任。比如一个用户只评分过一家餐厅跟另一个用户碰巧都评过这家算出来的相似度可能是1.0但这种相似完全没有统计意义。所以在算相似度之前我会加一道过滤两个用户共同评分的餐厅数量必须大于等于3否则相似度直接置0。阈值定多少得看你的数据分布。我当时数据集里有3万多条评分记录覆盖1000多位用户和2000多家餐厅把共同评分数量阈值设为3比较合适。如果你数据更稀疏就降到2数据很稠密可以提高到5。这个参数没有绝对标准但一定要做。不然推荐结果会被那些“只评过一两家餐厅的边缘用户”污染看起来特别不靠谱。另一个容易踩坑的地方是相似度分布不均。有些明星用户评了几百家餐厅几乎跟所有人都有共同评分算出来跟大家的相似度都比较高。这种情况下如果直接按相似度排名取Top-K推荐结果会被少数几个活跃用户主导。我的做法是结合活跃度惩罚相似用户评分过的餐厅数量太多时对相似度乘以一个衰减系数比如sim_score * (1 / (1 log(1 rated_count)))让大众用户的声音也能被听到推荐结果会更有个性化。3. 数据从哪来、怎么洗才能用3.1 数据采集爬虫、公开数据集还是模拟数据做推荐系统数据是绕不开的一道坎。我当时在三条路线之间纠结了很久这里把优劣都说清楚。第一条是写爬虫爬携程美食数据。优点是数据真实、贴近题目、有说服力答辩的时候拿出来很有面子。缺点也明显携程有反爬机制需要处理验证码、请求头、频率限制这些东西耗时耗力。我最后花了大概一周时间才拿到了相对完整的公开页面数据包括餐厅名称、评分、城市、人均消费、评论摘要这些字段。第二条是用公开数据集。GitHub和Kaggle上有不少旅游美食相关的推荐数据集例如一些旅游网站的评论数据、大众点评或美团相关的公开数据集结构通常比较规范拿来就能用。缺点是可能跟“携程”这个主题结合不够紧密答辩时需要解释数据来源。第三条是模拟数据。自己写脚本生成用户行为记录逻辑上可行但有一个致命问题模拟数据往往太规整缺乏真实数据的噪声算法跑出来效果“假好看”答辩时容易被追问。我的建议是模拟数据只用来做功能测试最终论文和展示尽量用真实或半真实数据。我当时实际采用的是混合方案爬虫拿到一部分真实餐厅和评论数据约500家餐厅再用脚本在这些真实餐厅基础上生成模拟的用户评分行为约2万多条凑成一个可用的实验数据集。这样既保留了真实餐厅信息的可信度又解决了真实用户行为数据不足的问题。如果你打算照做几条经验供参考爬虫部分注意遵守网站的robots协议控制请求频率别给目标站点压力太大模拟数据生成时评分分布尽量做成偏态——大部分集中在3到5分1分2分很少这样更接近真实点评习惯。3.2 数据清洗与用户-物品矩阵构建数据拿到手不是直接能用的必须先清洗。我的清洗流程分成四步第一步去重。同一个用户在同一家餐厅重复评分只保留最新一条。这个看似简单实际操作时坑很多比如用户ID在不同表里格式不一致有的一串数字有的是字符串需要统一处理。第二步过滤噪声。餐厅名缺失的删掉评分为空或者超出1到5范围的修正或删除人均消费是负数的删掉。另外把“僵尸用户”——只有一条评分记录的用户——单独拎出来。这类用户不是完全没用他们可能代表冷启动场景但参与相似度计算会拉低整体效果我建议在主推荐流程中排除单独做冷启动测试集。第三步文本数据清洗。评论内容要处理HTML标签、emoji、空格、特殊字符中文分词这一步在数据预处理阶段可以先不做但在特征分析和标签展示时需要用到。我用了jieba对评论做了分词提取高频词生成餐厅标签比如“环境优雅”“分量足”“排队久”这些标签直接展示在页面上效果很好。第四步构建评分矩阵。这一步就直接对应前面算法部分说的pivot_table过程。矩阵的行是用户列是餐厅值是对应的评分。注意这里有一个设计决策如果用户对一家餐厅同时有评分和评论评分数值用分数本身评论行为本身不再额外加分避免数据膨胀。完成清洗后数据集就变成了三个文件restaurants.csv餐厅信息、users.csv用户信息、ratings.csv评分行为。这三个文件基本就是整个系统的数据基石。3.3 数据库表结构设计数据库设计看着简单其实很少有人一次设计到位。我的核心表有六张结构如下用户表tb_user用户id、用户名、密码摘要、头像、注册时间、用户城市、口味偏好标签。餐厅表tb_restaurant餐厅id、名称、城市、地址、经度纬度、人均消费、综合评分、评分人数、菜系标签、营业时间。菜品表tb_dish菜品id、餐厅id外键、菜品名、价格、图片地址、销量、推荐flag。评分表tb_rating记录id、用户id、餐厅id、评分值、评论内容、创建时间。用户和餐厅联合唯一索引实现去重。用户行为日志表tb_behavior记录id、用户id、餐厅id、行为类型浏览/评分/收藏/下单、行为时间。这个表不参与推荐算法主流程但用来支撑冷启动和后续数据分析。推荐结果表tb_recommendation记录id、用户id、餐厅id、推荐分值、推荐来源user_cf/popular/random、生成时间。这个表就是前面说的“离线算好、在线读取”的落点。两张表之间的关系要建好外键和索引尤其是评分表的user_id和restaurant_id以及行为日志表的user_id这几处不加索引的话数据量稍大查询就明显变慢。Django里建表走ORM模型类定义好以后运行makemigrations和migrate就能自动建表不用手写SQL。但要注意字符集设置MySQL的默认字符集在处理中文时很容易出现乱码。我踩过的坑是Django连接MySQL如果表字符集是latin1中文数据写入就直接变问号。解决办法是在新建数据库时统一用utf8mb4然后在Django的settings.py里显式配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: food_recommend, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }如果你在安装mysqlclient这一步卡住Windows下最常见的做法是先装好Visual C Build Tools再pip install mysqlclientmacOS下则可能要brew install mysql-client并配置pkg-config路径。这个我在后文部署部分还会再提。4. Web端怎么把推荐结果用起来4.1 Django项目搭建思路算法和数据搞定了接下来就是把它包成一个能点的网站。Django这一块我按下面的思路组织项目结构manage.pyDjango项目入口一切管理和启动都靠它。config/项目配置目录包含settings.py、urls.py、wsgi.py。apps/业务应用目录下面有users用户模块、restaurants餐厅模块、recommend推荐模块、rating评分模块。static/静态资源目录放css、js、图片。templates/Django模板目录。scripts/离线推荐算法脚本推荐任务跑完把结果写回数据库。Django流程不复杂用户访问首页urls路由到views里的视图函数视图查数据库或调用推荐服务拿到数据渲染模板返回给浏览器。我建议先创建项目再创建应用django-admin startproject config . python manage.py startapp users python manage.py startapp restaurants python manage.py startapp recommend python manage.py startapp rating在settings.py里把应用注册进去配置好数据库、模板目录和静态文件目录再配置一个登录跳转相关的参数基础架子就起来了。4.2 推荐接口的封装与逻辑推荐结果不要散落在各个页面里我建议封装成一个独立的推荐服务模块。写一个recommend_service.py对外只暴露一个方法def get_recommendations(user_id, limit10): # 1. 先从推荐结果表查离线计算结果 recs Recommendation.objects.filter( user_iduser_id, is_activeTrue ).order_by(-score)[:limit] # 2. 如果结果不足补充热门餐厅保底 if len(recs) limit: hot_recs Restaurant.objects.order_by(-total_score)[:limit] # 合并并去除重复 ... # 3. 返回餐厅列表 return restaurant_list视图层调用这个服务模板渲染就完了def home(request): user request.user if request.user.is_authenticated else None if user: restaurants recommend_service.get_recommendations(user.id) else: restaurants Restaurant.objects.order_by(-total_score)[:10] return render(request, home.html, {restaurants: restaurants})这套设计的好处是推荐逻辑和页面逻辑完全解耦。后面你想换算法比如换成ItemCF或者干脆上深度学习模型只需要改recommend_service内部实现视图层完全不用动。还有一点要注意的是登录用户和匿名用户的区分。匿名用户没有历史行为数据计算不了协同过滤所以直接走热门榜。游客首次访问也直接给热门榜。这个逻辑看着简单但它就是冷启动问题最朴素的解法。系统里注册用户以后“猜你喜欢”栏目才开始个性化。4.3 前端页面与后台管理前端部分没有用特别复杂的前端框架Django模板自带的能力就能满足需求。我做了四个核心页面首页/推荐流展示推荐餐厅卡片包含餐厅图片、名称、评分、人均消费、标签。个性化推荐和热门榜分成两个区块展示。餐厅详情页展示餐厅信息、菜品列表、用户评价同时支持用户评分和评论。核心交互都在这个页面产生它也是推荐系统行为数据的主要来源。城市/分类浏览页按城市、菜系筛选餐厅方便用户主动探索。这个页面的意义是给推荐系统提供“非评分行为”——浏览行为也会写进行为日志表。个人中心展示我的评分记录、收藏列表、推荐历史。页面样式我用了Bootstrap 5框架保持简洁清爽不需要自己手写复杂CSS。图片占位可以用本地静态图或者一些免费图库的图片链接不用太纠结。Django Admin也强烈建议用起来。默认的后台管理界面其实已经很能打了只需要在admin.py里注册模型from django.contrib import admin from .models import Restaurant, Dish admin.register(Restaurant) class RestaurantAdmin(admin.ModelAdmin): list_display (name, city, avg_price, total_score, recommend_flag) list_filter (city, recommend_flag) search_fields (name,)如果你想进一步美化后台界面可以用simpleui或者django-admin-interface这类第三方主题包装好以后后台马上变样。花不了多少时间但答辩演示的时候效果差很多看起来专业多了。后台管理实际上对应题目里“管理”这个维度管理员可以手动把某个餐厅置顶推荐推荐flag也可以人工干预某个用户的推荐结果这些都是论文里可以展开写的亮点。5. 调试、部署与避坑实录5.1 Python环境和依赖管理这个项目从开发到部署Python环境问题是最折磨人的。我一开始在Windows本地开发后来部署到Linux服务器版本差异导致各种意想不到的问题。Python版本我建议直接用3.8到3.10之间的稳定版本。太新的版本个别依赖库可能还没适配太老的版本Django新特性又用不上。Django版本用3.2 LTS或者4.2 LTS都行LTS版本官方维护周期长遇到问题搜到的解决方案也最多。依赖管理统一用requirements.txt锁定版本生成方式很简单pip freeze requirements.txt装依赖的时候pip install -r requirements.txt这里有个小坑pip freeze会把环境里所有包都导出来有些跟项目无关。更好的做法是手动维护一个干净的项目依赖清单或者用pipenv、poetry这类工具管理虚拟环境。我自己的习惯是先用venv建独立环境再装依赖这样requirements.txt干净得多。Windows上如果遇到mysqlclient装不上的情况很多人会临时换成pymysql在项目settings.py里加一段import pymysql pymysql.install_as_MySQLdb()但我要提醒一句这个方案适合快速跑通正式部署说实话还是建议把mysqlclient搞定。pymysql在个别Django版本下会有一些兼容性问题比如Decimal字段精度、日期格式化等。与其后期排雷不如一次性把mysqlclient装好。5.2 推荐结果为空或效果异常的排查项目跑起来以后大概率会碰到推荐结果为空或者结果很离谱的情况。这里分享几个我实际遇到并且排查了很久的问题。第一个是推荐结果为空。最常见的原因是评分矩阵太稀疏目标用户跟其他用户的共同评分餐厅数为0算出来没有相似用户。这时页面上才会一直空白。排查思路是先确认这个用户有没有评分记录如果没有那是冷启动场景走热门榜本来就没问题如果有评分记录但还是空那就要看相似度阈值设置是不是太高了。我当时把共同评分数量阈值从3降到2以后很多边缘用户的推荐就出来了。第二个是推荐出来的餐厅“不够个性”。比如一个喜欢吃川菜的用户推荐结果里全是粤菜馆看起来完全没有相关性。这个问题的根源是评分数据太少或者相似用户的行为过于分散。可以先把相似用户数量K从5调到10让更多“口味相近”的用户参与生成推荐如果还不行就在算法里加一个候选集约束只从目标用户所在城市或者常去城市的餐厅里推荐。第三个是实时性幻觉。我开始是每次请求都现算推荐结果响应时间最慢到四五秒而且数据库压力很大。把算法跑批搬成离线任务、结果落推荐表之后页面响应时间降到一百毫秒以内体感完全不一样。推荐结果更新的频率没关系每天跑一次或者每周跑一次都行美食数据本身变化也不快。第四个坑是py文件编码。写推荐算法脚本时如果直接从网上复制代码有时候会遇到UnicodeDecodeError。解决办法是在Python文件头部加一行# -*- coding: utf-8 -*-5.3 Django部署到服务器宝塔踩过的坑部署这块现在用宝塔面板操作确实省很多事。流程是服务器安装宝塔装好Nginx、MySQL、Python项目管理器把项目代码上传创建Python项目配置好入口文件和静态文件再用Nginx反向代理转发请求。几个必踩的坑先列出来静态文件404Django的静态文件在调试模式由Django自己提供但部署后必须执行python manage.py collectstatic把静态文件收集到一个指定目录然后Nginx配置alias指向这个目录。数据库连接不上注意数据库主机名别写127.0.0.1然后让Django用localhost这两个在socket连接上可能行为不一样。更稳妥的做法是统一用127.0.0.1并确保MySQL允许本地连接。ALLOWED_HOSTS报错Django的settings里需要把服务器IP或域名加进去ALLOWED_HOSTS [127.0.0.1, localhost, your_domain.com]迁移报错服务器上先执行python manage.py migrate建表再跑启动命令。经常有人只上传代码忘了迁移结果页面打开全是表不存在的报错。我当时部署完以后还做过一个优化用crontab定时跑推荐脚本每天凌晨两点更新一次推荐结果表。这不光让系统显得更“完整”也可以作为论文里“系统运行与维护”章节的素材。0 2 * * * cd /www/wwwroot/food_recommend /www/wwwroot/food_recommend/venv/bin/python scripts/run_recommend.py logs/recommend.log 21这样每天自动更新后台管理员只需要偶尔看一眼日志确认没有报错就行。最后再分享一个这个项目后续值得扩展的方向如果你手头有带地理位置的餐厅数据可以考虑基于位置的协同过滤给推荐结果加一个“距离优先”的排序因子如果你想往深度学习方向靠可以做一个基于嵌入的矩阵分解模型或者用用户浏览序列做行为序列推荐。作为课程设计这些不是必须的但真想做深数据是够用的算法替换也就那么回事难的永远是数据和业务场景的匹配。这次做下来最大的体会是推荐系统在教材上是一堆公式在公司里是一套工程在毕设里其实就是把公式落到场景里跑起来。把用户行为跟业务数据串起来跑通比纠结用哪个算法重要得多。照这个思路做你的系统至少不会只是个花架子。