携程美食推荐系统开发实战:Django+协同过滤算法全解析
这两年无论是毕设还是课程设计餐饮美食类推荐系统的出镜率一直很高。而“携程美食”这个场景又把OTA平台的数据、推荐系统和Web全栈开发揉在了一起算是比较典型的综合性练手项目。如果你手头正好拿到这个题目或者正打算往这个方向做那这篇文章应该能帮你把整条技术路线和落地细节理顺。我会直接从项目定位、技术选型、数据获取、推荐算法实现到最后的答辩准备把值得注意的坑和核心逻辑都过一遍。先说清楚这套东西不是简单的CRUD它是带着数据采集、清洗、推荐策略和可视化展示的一整套闭环这才是它真正的难点和价值所在。1. 内容整体设计与思路拆解1.1 项目定位这不是一个普通的增删改查系统先把这个项目看透。表面上看它是一个围绕“美食”主题的推荐系统有用户登录、美食列表、评分、收藏和推荐结果展示。但如果你只把它做成一个管理后台加前台列表那在评委眼里它是拿不到高分的因为这里面完全没体现“推荐”两个字的分量也浪费了“携程美食”这个数据场景。真正的定位它应该具备三条链路:数据链路通过爬虫采集携程平台的美食商户数据包括店铺名称、评分、人均消费、菜系分类、评论摘要、地理位置等并经过清洗和结构化处理导入MySQL。推荐链路基于用户行为浏览、收藏、评分构建个性化推荐服务至少融合两种以上推荐策略并能解释推荐结果。展示链路基于Django实现前端页面与后台接口把数据、推荐结果和简单的数据分析图表可视化形成一个“看得到结果”的产品闭环。这三点确定后你的系统边界就清楚了。它不是简单的“DjangoMySQL”作业而是一个数据驱动的小型应用。带着这个思路去做后面的开发方向和模块拆分自然就有谱了。1.2 技术选型Python预测分析的核心工具这个项目有意思的地方在于它的标题里同时出现了Django和SSM。很多同学一看就懵了“这俩不是一套技术栈啊一个Python一个Java怎么凑到一起”我的理解是这样的Django是项目的主体Web框架负责后端逻辑、页面渲染和接口提供SSM在这类毕设项目中更多是作为前期原型的参考实现或者是你参考了别人基于SSM做美食推荐系统的设计思路用Django重新落地。还有一种情况是打包资料里整合了参考代码方便评审对比。但不管怎样最终系统以PythonDjango为主栈完全可以满足需求。那为什么用Django而不是Flask原因很简单Django自带Admin后台做数据管理和用户管理非常方便能省下大量重复工作Django的ORM对MySQL支持完善迁移、查询、建表都很顺手模板系统、表单处理、分页组件这些在项目里几乎全都能用到属于现成的轮子社区资料丰富遇到问题基本都能搜到解决方案。我在实际做这类项目时Django版本一般选3.2或4.x系列的稳定版。Python环境建议用3.8到3.10太新的Python版本偶尔会遇到一些第三方库还没适配的尴尬情况。推荐算法部分可以先自己写纯Python实现也可以用scikit-learn做近邻计算看你的要求决定。1.3 系统角色与功能边界系统按用户身份拆分成两个主要角色普通用户注册登录、浏览美食列表、查看美食详情、给美食评分、收藏美食、查看系统推荐的美食列表、查看自己的收藏与评分记录、搜索和筛选。管理员用户管理、美食商户数据管理、评论数据管理、推荐参数配置、核心数据统计、系统日志查看。从功能边界来说推荐引擎是核心模块但它不追求极致的推荐精度而是追求逻辑清晰、可解释、可演示。这一点在答辩时很加分因为评委更愿意看到一个你完全理解并且能讲清楚来龙去脉的功能模块。我建议在功能设计上再加一个“推荐结果解释”的功能。比如系统给用户推荐了“全聚德前门店”旁边附带着一句“因为你曾经收藏过北京菜所以为你推荐这家店铺”。这样既直观又容易展示推荐系统的逻辑不需要复杂的解释模型做起来也不难。2. 核心细节解析与实操要点2.1 数据采集如何爬取携程美食数据这个项目的数据基础是携程美食数据所以爬虫是绕不开的一个环节。我先说一下大思路再补充实操细节。首先爬虫的目标数据字段至少包括餐厅名称所属城市和具体地址菜系类型如川菜、粤菜、西餐、日料等人均消费综合评分评分人数营业时间推荐菜或招牌菜用户点评可选字段用于文本分析爬虫部分我用过两种方案都可行方案一直接爬取携程网页版的数据通过requests请求HTML页面再用BeautifulSoup或lxml解析表格和标签。这种方案容易实现适合数据量不大几百到几千条的场景。比较麻烦的是要处理携程的反爬机制比如User-Agent伪装、请求频率控制、Cookie处理。方案二查找是否有App端的接口可以进行请求。这种方法更高效通常返回JSON数据但接口参数比较复杂需要配合抓包工具分析而且反爬策略更严格。针对这个项目我更推荐方案一但要注意以下操作要点用time.sleep(random.uniform(1, 3))控制请求间隔模拟人工访问设置完整的请求头包括User-Agent、Referer、Accept-Language使用requests.Session保持会话避免频繁登录校验适当用代理IP池如果反爬严重的话但考虑到课设场景基本不需要。爬下来的数据不能直接用必须做清洗。我这里列一下我在实际项目中常用的清洗规则人均消费字段清理掉“”符号转换成浮点数评分为空或非数字的填充为None推荐算法里做缺失值处理菜系字段做标准化比如“川菜”“四川菜”统一成“川菜”去重逻辑按餐厅名称地址精确去重。清洗完成后把数据通过Django的ORM批量导入MySQL。这里有个小技巧先用pandas读取Excel或CSV做初步处理再转成Django model实例列表用bulk_create批量插入速度能快不少。2.2 数据结构设计九大美食类型与用户行为我参考了不少同类项目的数据库设计一般会把美食按类型划分为九大类中餐、西餐、日韩料理、东南亚菜、快餐简餐、咖啡甜品、火锅、烧烤、其他。这九类字段可以设计成美食主表里的一个分类字段也可以单独拆出美食分类表。我建议拆表这样后续做推荐策略和分类筛选会更灵活。核心数据表我建议这样规划表名核心字段作用说明usersid, username, password(哈希), email, created_at用户基础信息categoryid, name, sort_order美食分类restaurantid, name, category_id, city, address, avg_price, rating, rating_count, hours, tags美食商户信息reviewid, user_id, restaurant_id, rating, content, created_at用户评分与评论favoriteid, user_id, restaurant_id, created_at用户收藏记录behaviorid, user_id, restaurant_id, behavior_type, created_at浏览/点击行为日志看到这里你可能发现了我把用户行为单独拆了一张表。没用过的同学可能觉得多余但做过推荐系统的都知道用户行为数据是推荐算法最核心的养料。没有行为数据协同过滤根本跑不起来。直接把行为记录在日志表里后续做“基于用户的协同过滤”或者“基于物品的协同过滤”都有数据基础。这里我要特别强调数据库表结构的一个细节外键关联。如果你在Django里定义了ForeignKey字段Django会自动在数据库层面建立约束。但在实际运行中如果数据量比较大频繁的外键检查会影响性能。我的做法是ORM模型里保留ForeignKey关系方便查询但数据库迁移时设置db_constraintFalse相当于只保存关联ID不建立物理外键。这样既方便ORM操作又避免了性能瓶颈。这个细节在答辩时提出来会让评委觉得你确实是有实战经验的而不只是照着教程敲代码。2.3 推荐算法选型三种主流策略的组合方案推荐系统是整套系统的灵魂这部分我需要重点讲因为大多数人卡在这一块。推荐算法有很多种但考虑到是毕设项目我不建议直接把深度学习那一套搬上来——数据量不够训练周期长不好解释。合理的做法是组合三套算法根据用户行为数据的情况自动切换。第一种基于用户的协同过滤算法User-Based CF核心逻辑是“相似的人喜欢相似的东西”。具体实现构建用户-美食评分矩阵行是用户列是美食值是评分没有评分就记为0用皮尔逊相关系数或余弦相似度计算用户之间的相似度找到当前用户的K个最近邻一般取K10到20把这些近邻评分过但当前用户没看过的美食按加权平均分降序排名取TopN。第二种基于物品的协同过滤算法Item-Based CF核心逻辑是“喜欢这个物品的人也喜欢类似的物品”。具体实现构建美食-用户倒排表计算美食之间的相似度矩阵这个矩阵可以提前离线算好存入缓存根据用户的历史评分/收藏记录把相似度最高的美食推荐给用户。第三种基于内容的推荐Content-Based核心逻辑是“推荐和你喜欢的美食有相似属性的美食”。这里用到的属性就是菜系分类、人均价格区间、评分档次。比如用户经常收藏川菜那就多给他推荐川菜再结合价格带做精细调优。这三套算法我建议在系统里这样做切换策略新用户无任何行为数据用流行度推荐即推荐全站评分最高、热度最高的美食再根据注册时选的偏好分类做筛选老用户有行为数据但比较少少于5条用基于内容的推荐因为数据量不够算协同过滤用属性匹配最稳妥活跃用户行为数据多用基于用户和基于物品的协同过滤综合打分再加一点流行度做多样性补充。推荐结果列表最终用一个综合得分公式score 0.5 × 协同过滤得分 0.3 × 内容匹配得分 0.2 × 流行度得分这个公式可以调但三个维度的设定逻辑要能在文档里写清楚。答辩时能把这个公式讲明白项目水平立刻上一个档次。2.4 系统核心实现构建可运行的推荐服务Django里的推荐引擎不一定要做成独立服务可以做成一个Python模块放到底层通过View调用。这里我给出核心实现思路和关键代码逻辑。先建一个recommend模块结构如下recommend/ ├── __init__.py ├── algorithms.py # 三种推荐算法实现 ├── data_loader.py # 从数据库加载用户行为与美食数据 ├── recommend_service.py # 统一入口调度算法 └── utils.py # 相似度计算等工具函数在algorithms.py中基于用户的协同过滤关键代码如下简化版def user_based_cf(user_id, user_item_matrix, similarity_matrix, top_n10): 基于用户的协同过滤推荐 :param user_id: 目标用户ID :param user_item_matrix: 用户-物品评分矩阵 :param similarity_matrix: 用户相似度矩阵 :param top_n: 推荐数量 :return: 推荐结果列表 [(item_id, score)] # 获取目标用户的评分记录 target_user_ratings user_item_matrix.get(user_id, {}) # 初始化得分字典 scores {} # 遍历其他用户 for other_user, similarity in similarity_matrix.get(user_id, {}).items(): if other_user user_id or similarity 0: continue # 遍历其他用户评分过的物品 for item_id, rating in user_item_matrix.get(other_user, {}).items(): if item_id in target_user_ratings: continue # 过滤掉目标用户已经评分过的物品 scores[item_id] scores.get(item_id, 0) similarity * rating # 排序并返回TopN sorted_scores sorted(scores.items(), keylambda x: x[1], reverseTrue) return sorted_scores[:top_n]注意这里的关键点是过滤掉用户已经评分或收藏过的美食避免重复推荐。很多首次做推荐系统的同学会忽略这一步导致推荐列表里全是用户已经看过的看起来就非常不智能。相似度计算我用的方法是皮尔逊相关系数它在评分尺度不一致的场景下比余弦相似度更稳定。def pearson_similarity(user1_ratings, user2_ratings): 计算两个用户评分向量的皮尔逊相关系数 # 找出两个用户都有评分的物品 common_items set(user1_ratings.keys()) set(user2_ratings.keys()) if len(common_items) 2: return 0 sum1 sum(user1_ratings[key] for key in common_items) sum2 sum(user2_ratings[key] for key in common_items) sum1_sq sum(user1_ratings[key] ** 2 for key in common_items) sum2_sq sum(user2_ratings[key] ** 2 for key in common_items) sum_product sum(user1_ratings[key] * user2_ratings[key] for key in common_items) n len(common_items) numerator sum_product - (sum1 * sum2 / n) denominator sqrt((sum1_sq - sum1 ** 2 / n) * (sum2_sq - sum2 ** 2 / n)) if denominator 0: return 0 return numerator / denominator在实际项目中我不建议每次请求都现算相似度矩阵因为用户一多计算量就上来了。正确做法是定时比如每天一次离线计算用户相似度矩阵存入缓存Redis或数据库表里在线推荐时只做查询和排序。这是真实工业界的做法放到毕设里也完全说得通。2.5 Django 后台管理大幅提升开发效率Django自带的Admin后台是这个项目提效的利器。你只需要在admin.py里注册模型就能获得一个可以增删改查的后台管理界面。from django.contrib import admin from .models import Restaurant, Category, Review, Favorite admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display (id, name, sort_order) admin.register(Restaurant) class RestaurantAdmin(admin.ModelAdmin): list_display (id, name, category, city, avg_price, rating) list_filter (category, city) search_fields (name,) list_per_page 20Django Admin还有一个好处你不需要再单独开发一套后台管理页面了。用户管理、数据维护、统计报表都可以直接在Admin里完成省下的时间全部用来打磨推荐算法和前端展示非常划算。唯一需要调整一下的是Admin的样式默认的界面太素了。可以用django-simpleui或者django-jazzmin这两个第三方库几分钟就能让后台的逼格提升不少截图放到论文里也更体面。这两个库的安装使用方法网上的教程很多我就不在这里展开了。3. 实操过程与核心环节实现3.1 环境准备与项目初始化先说一下基础环境配置我这里给的是我自己跑通过的组合Python 3.9Django 4.1MySQL 8.0PyCharm 或 VS Codepandas、numpy、scikit-learn、requests、BeautifulSoup4创建项目时我习惯这样操作# 创建虚拟环境 python -m venv venv # 激活虚拟环境Windows venv\Scripts\activate # 激活虚拟环境Mac/Linux source venv/bin/activate # 安装依赖 pip install django4.1 mysqlclient pandas numpy scikit-learn requests beautifulsoup4 # 创建Django项目和App django-admin startproject food_recommend cd food_recommend python manage.py startapp recommend python manage.py startapp users要不要把users单独放在一个App里我的建议是“用户相关功能”单独一个App“推荐相关核心逻辑”单独一个App。这样代码组织清晰论文里画系统架构图也方便。接着配置MySQL数据库连接打开settings.pyDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: food_rec_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, } } }这里有个坑要先提示一下MySQL 8.0默认的认证插件是caching_sha2_passwordmysqlclient有时候会连不上。解决办法是创建用户时指定mysql_native_password或者在MySQL配置里调整。这个问题很常见网上一搜就有答案我在这不多讲了。3.2 数据爬虫落地与数据入库爬虫部分我来写一个实际可用的代码框架import requests import random import time import pandas as pd from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } def fetch_restaurant_list(city, page): 抓取携程美食列表页 url fhttps://you.ctrip.com/restaurant/{city}/s0-p{page}/ resp requests.get(url, headersHEADERS, timeout10) soup BeautifulSoup(resp.text, lxml) # 解析逻辑视页面结构而定提取店铺名称、评分、地址、菜系、价格等字段 items [] # ... 解析逻辑 return items def crawl_main(): all_data [] for city in [beijing, shanghai, guangzhou, shenzhen]: for page in range(1, 20): items fetch_restaurant_list(city, page) if not items: break all_data.extend(items) time.sleep(random.uniform(1, 3)) df pd.DataFrame(all_data) df.to_csv(ctrip_food.csv, indexFalse, encodingutf-8-sig)注意这里有个细节携程的URL规则有两种一种是拼音城市名一种是数字代码。而且页面结构改版频繁标签选择器可能变化。所以我在实际抓取时都会先打印resp.text到本地用浏览器开发者工具对照分析确认选择器没问题再批量跑直接硬写很容易翻车。数据入库的代码我直接写在Django的management command里这样可以直接用python manage.py import_food_data触发# recommend/management/commands/import_food_data.py from django.core.management.base import BaseCommand import pandas as pd from recommend.models import Restaurant, Category class Command(BaseCommand): help 从CSV文件导入美食数据 def add_arguments(self, parser): parser.add_argument(csv_file, typestr) def handle(self, *args, **options): df pd.read_csv(options[csv_file]) restaurants [] for _, row in df.iterrows(): # 获取或创建分类 category, _ Category.objects.get_or_create(namerow[category]) restaurants.append(Restaurant( namerow[name], categorycategory, cityrow[city], addressrow[address], avg_pricerow[avg_price], ratingrow[rating], rating_countrow[rating_count], tagsrow[tags], )) Restaurant.objects.bulk_create(restaurants, ignore_conflictsTrue) self.stdout.write(self.style.SUCCESS(数据导入成功))3.3 推荐服务的接口设计与页面展示系统最终需要给前端提供推荐接口我这里推荐一个简单但完整的接口设计请求方式接口路径功能说明GET/api/recommend/popular热门美食推荐流行度排名GET/api/recommend/cf协同过滤推荐需登录GET/api/recommend/content基于内容的推荐需登录GET/api/recommend/hybrid混合推荐结果综合评分GET/api/restaurant/list美食列表支持分类、城市过滤POST/api/behavior/record记录用户行为浏览/评分/收藏接口实现的核心逻辑在recommend_service.py里def hybrid_recommend(user_id, top_n20): 混合推荐综合三种算法结果并加权打分 weights {cf: 0.4, content: 0.3, popular: 0.3} cf_results user_based_cf_recommend(user_id, top_n30) content_results content_based_recommend(user_id, top_n30) popular_results popular_recommend(top_n30) # 合并得分 final_scores {} for item_id, score in cf_results: final_scores[item_id] final_scores.get(item_id, 0) weights[cf] * score for item_id, score in content_results: final_scores[item_id] final_scores.get(item_id, 0) weights[content] * score for item_id, score in popular_results: final_scores[item_id] final_scores.get(item_id, 0) weights[popular] * score sorted_results sorted(final_scores.items(), keylambda x: x[1], reverseTrue) return sorted_results[:top_n]前端页面我会用Django模板Bootstrap搭建首页做三块内容轮播图推荐位、个性化推荐列表、热门美食Top10。美食详情页展示店铺信息、评分、用户评论和“相似推荐”模块。推荐的展示位置用卡片式布局每个卡片上展示图片、名称、评分、人均价格和标签整体清爽就好不追求复杂交互。关于前端要不要用Vue我的建议是看你的项目定位。如果论文里重点在推荐算法和数据分析那Django模板Bootstrap就够了如果你想额外展示前端能力可以用Vue写一个单页应用Django后端只提供API。但我要提醒一句纯前后端分离方案会明显加大工作量调试起来也更繁琐如果时间紧张不要为了炫技给自己挖坑。3.4 项目目录结构推荐做项目之前目录结构得先立好不然代码写到后面就乱成一锅粥了。我推荐的目录结构是这样的food_recommend/ ├── manage.py ├── requirements.txt ├── README.md ├── db.sqlite3 ├── food_recommend/ # Django项目配置 │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── recommend/ # 推荐系统核心应用 │ ├── __init__.py │ ├── models.py │ ├── views.py │ ├── admin.py │ ├── urls.py │ ├── algorithms.py │ ├── recommend_service.py │ ├── data_loader.py │ ├── utils.py │ ├── management/ │ │ └── commands/ │ │ ├── import_food_data.py │ │ ├── crawl_ctrip.py │ │ └── train_model.py │ ├── migrations/ │ └── templates/ ├── users/ # 用户模块 │ ├── __init__.py │ ├── models.py │ ├── views.py │ ├── admin.py │ └── urls.py ├── static/ # 静态资源 └── media/ # 上传文件这种结构把推荐算法和Django的业务逻辑分开了算法部分做到“不要被框架绑死”以后想换框架或者单独测试算法都非常方便。论文里的系统架构图也更容易画得清晰。4. 常见问题与排查技巧实录4.1 协同过滤推荐效果差推荐出来的东西完全不相关这是我被问到过最多的问题也是实战中最容易翻车的地方。排查顺序是这样的第一步检查用户行为数据是否充足。如果全系统只有十几个用户每个人行为记录不超过3条那协同过滤的效果一定非常差。解决办法造一批模拟数据写一个脚本生成100个用户、每人评分10-20家餐厅的测试数据集。这不是作弊而是为了让算法跑起来毕业论文里可以说“通过模拟数据验证算法的可行性”。第二步检查相似度计算是否出了问题。皮尔逊相关系数在没有公共评分时返回0是正常的但如果大量用户之间都是0说明数据太稀疏了。解决办法是引入“基于物品的协同过滤”或降低相似度矩阵的稀疏度阈值。第三步检查推荐结果是否过滤了已评分的物品。忘记过滤这个问题在高频用户身上特别明显推荐列表里总是用户已经收藏或评分过的店铺体验很差。这一点要写进单元测试里作为基本校验项。4.2 爬虫数据不完整或者直接爬不动爬取携程数据时最容易遇到的问题是页面结构改版之前写的选择器全部失效。我的经验是分步调试先解析一个页面打印出所有关键信息确认无误后再批量跑。另外如果请求太频繁触发了反爬页面会直接跳转到验证码页面这种情况就不要再暴力抓取了适当调大sleep间隔同时给请求加上Referer和Cookie信息。有些同学爬到的数据只有几十条这其实不够支撑推荐系统的效果演示。如果实在爬不动了有个备选方案手动构造一部分合理的模拟数据。结合携程公开的店铺信息按城市、菜系、价格带均匀分布生成几百条“仿真数据”。毕设的本质是验证系统架构和推荐算法数据量级不够时用模拟数据补足是完全合理的做法但要在文档里如实说明数据的来源和构造过程不要隐瞒。4.3 Django后台登录不了或者页面报错Django Admin登录不了90%的情况是账号密码不正确或者没有创建超级用户。用这个命令创建python manage.py createsuperuser还有一种情况是登录时报CSRF验证失败在模板里加{% csrf_token %}就能解决。如果后台页面样式丢失检查一下STATIC_URL和STATICFILES_DIRS的配置确保静态文件目录指向正确并且python manage.py collectstatic已执行过。4.4 数据库乱码问题如果是中文乱码检查两处一是数据库表编码必须为utf8mb4二是Django里MySQL连接的OPTIONS要设置charset: utf8mb4。如果爬虫导入时乱码先检查CSV文件本身的编码导出时统一用utf-8-sigExcel打开就不容易乱Django读取也不会出问题。养成习惯所有文件统一UTF-8处理乱码这类问题基本能一次消灭。4.5 推荐接口响应太慢如果每次打开推荐页都要等好几秒大概率是相似度矩阵现算的。我的解决办法很简单第一次计算后把用户相似度矩阵保存到数据库或者本地文件每天更新一次推荐时只查询不做实时计算。同时给Django接口加上缓存用django.core.cache设置一个5分钟的缓存同一用户短时间内重复请求直接走缓存响应时间能降低到毫秒级。最后的经验分享这套系统做下来我个人最大的体会是推荐系统项目的难点从来不在算法本身而在“数据”和“工程落地”这两个环节。数据没做好算法再漂亮也是空中楼阁工程落地方案不清晰代码写到后面就是一团乱麻。如果你正在做这个课题我建议按这样的优先级投入时间数据爬取与清洗30%时间、推荐算法实现与调优30%时间、Django后台与页面30%时间、论文与文档10%时间。别把时间全花在调页面样式或者纠结算法精度上把精力放在“系统能完整跑通、逻辑清晰能讲明白”这件事上。答辩时我会重点准备这三个问题的答案三种推荐算法各自的原理和适用场景是什么、为什么选择混合推荐而不是只用一种算法、系统如何解决冷启动问题。这三问能答利索这个项目的深度就已经能立住了。至于“为什么要用Django MySQL”“爬虫被反爬了怎么办”这种问题都属于送分题代码里和文档里都有答案好好过一遍就能从容应对。