简介这是一套面向高校计算机相关专业学生的智能旅游推荐系统完整项目可作为Python毕业设计或课程设计使用帮助解决选题难、代码不完整、环境跑不通等常见问题。资源包共797个文件约20.19MB涵盖前端页面、后端逻辑与数据库脚本其中html、css、vue与js文件构成界面与交互层svg、gif、jpg、png等图片资源用于页面视觉呈现45个py文件承载核心业务逻辑另有sql脚本用于建库建表bat批处理与txt说明辅助环境配置。项目采用Python后台框架建议搭配3.7版本前端为html开发环境为pycharm数据库使用MySQL并通过Navicat管理导入依赖后即可运行。目前已有89人学习下载。读者可获得完整源码、数据库脚本与配套工具便于快速理解推荐算法与前后端交互流程也可在此基础上二次开发或撰写论文具备较高的实际应用与参考价值。1. 从一份毕业设计压缩包说起智能旅游推荐系统到底在解决什么问题每年毕业季计算机相关专业的选题里总有一类经久不衰基于python的智能旅游推荐系统。你拿到手的往往是一个压缩包里面塞着源码、数据库脚本和一份教程文档标题写着“Python毕业设计附源码、数据库、教程”。很多同学第一反应是“能跑就行”但真正答辩时被问一句“你的推荐算法为什么选协同过滤而不是基于内容”就当场卡壳。这篇笔记不打算复述那份教程而是把这类系统从数据到推荐、从建库到跑通的全链路拆开讲清楚让你既能照着复现也能在答辩时讲出每一步的理由。它解决的核心问题很朴素用户面对成百上千个景点、酒店、路线时不知道选哪个系统根据历史行为或偏好把最可能感兴趣的推到他面前。适合谁看正在做计算机毕业设计、需要一套能讲清楚原理又能跑起来的python项目的同学也适合想入门推荐系统、拿旅游场景练手的开发者。数据库在这里不是配角用户、景点、评分、订单全靠它撑起来选型和建表直接决定后面推荐能不能算得动。下面按“先立住原理、再动手复现、最后避坑”的顺序往下走。2. 推荐算法选型协同过滤、基于内容还是混合毕业设计该怎么挑2.1 三类主流算法在旅游场景下的真实差别旅游推荐和电商推荐有个明显区别景点是“低频消费品”一个用户一辈子可能只去一次某个城市评分矩阵极其稀疏。这直接决定了算法选型。协同过滤Collaborative Filtering分两种UserCF 找“和你口味相似的人喜欢什么”ItemCF 找“和你去过的景点相似的景点”。它的优点是实现简单、可解释性强缺点是冷启动严重——新用户没行为、新景点没评分矩阵一稀疏就推不准。基于内容Content-Based靠景点自身的标签城市、类型、季节、票价区间和用户画像做匹配冷启动友好但容易推来推去都是同类缺乏惊喜感。混合推荐就是把两者加权或分阶段组合工业界主流但毕业设计里如果权重调不好反而两头不讨好。我的建议是如果数据集里用户-景点评分记录超过几千条优先做 ItemCF因为景点数量远小于用户数量相似度矩阵更稳定如果数据稀疏到每个用户只有两三条记录老老实实做基于内容用标签匹配答辩时也更好讲。别一上来就上深度学习毕业设计的算力和数据量撑不起反而暴露你对原理理解不深。2.2 用 Python 实现 ItemCF 的最小可跑版本先不急着接数据库用一份内存里的评分字典把算法逻辑跑通确认推荐结果合理再往工程里搬。这是我一贯的做法能省掉大量“到底是算法错还是数据库读错”的排查时间。import math from collections import defaultdict # 用户-景点评分表{用户: {景点: 评分}} ratings { u1: {故宫: 5, 长城: 4, 西湖: 3}, u2: {故宫: 5, 长城: 5, 兵马俑: 4}, u3: {西湖: 4, 兵马俑: 5, 长城: 3}, u4: {故宫: 4, 西湖: 5}, } def item_similarity(ratings): # 统计每个景点的评分用户集合 item_users defaultdict(set) for u, items in ratings.items(): for i in items: item_users[i].add(u) # 计算景点两两之间的余弦相似度 sim defaultdict(dict) items list(item_users.keys()) for i in range(len(items)): for j in range(i 1, len(items)): a, b items[i], items[j] co item_users[a] item_users[b] # 共同评分用户 if not co: continue # 用评分向量算余弦比单纯数共同用户更准 dot sum(ratings[u][a] * ratings[u][b] for u in co) norm_a math.sqrt(sum(ratings[u][a] ** 2 for u in item_users[a])) norm_b math.sqrt(sum(ratings[u][b] ** 2 for u in item_users[b])) s dot / (norm_a * norm_b 1e-9) sim[a][b] sim[b][a] s return sim def recommend(user, ratings, sim, top_n2): scores defaultdict(float) rated ratings[user] for i, r in rated.items(): for j, s in sim.get(i, {}).items(): if j in rated: continue # 跳过已评分的 scores[j] s * r # 相似度加权评分 return sorted(scores.items(), keylambda x: -x[1])[:top_n] sim item_similarity(ratings) print(recommend(u4, ratings, sim))这段代码的逻辑分三步先构建“景点-用户”倒排表再对每对景点用共同评分用户的评分向量算余弦相似度最后对目标用户已评分的景点把相似景点的相似度乘以评分累加得到候选推荐分。参数上top_n控制返回条数实际系统里一般取 10 到 201e-9是防止除零的平滑项别省。跑出来u4应该会推荐“长城”或“兵马俑”因为 u4 喜欢故宫和西湖而这两个景点和长城、兵马俑都有共同评分用户。如果结果明显不合理先检查评分表里有没有用户对同一景点重复评分那会把相似度算歪。2.3 混合策略的加权公式与调参边界当协同过滤和基于内容都要用时常见做法是线性加权final_score α * cf_score (1-α) * cb_score。α 取 0.5 起步然后看离线指标。旅游场景里我一般把 α 设在 0.6 到 0.7因为行为数据比标签更能反映真实偏好但新用户冷启动时把 α 降到 0.2让标签主导。这个切换逻辑要写进代码答辩时是加分项。注意归一化CF 分数和 CB 分数量纲不同必须先各自 min-max 归一化到 [0,1] 再加权否则加权就是玄学。很多同学翻车就翻在这里两个分数直接相加结果某一项永远压过另一项。3. 数据库设计与建库MySQL 表结构、字段类型和索引怎么定3.1 五张核心表的关系与字段设计旅游推荐系统的数据库不用搞太复杂五张表足够撑起完整业务用户表、景点表、评分表、订单表、标签表。关键是字段类型和索引。用户表存 id、用户名、密码哈希、注册时间、偏好标签景点表存 id、名称、城市、类型、票价、描述、标签评分表是推荐算法的数据源存 user_id、spot_id、rating、时间戳必须在这两个字段上建联合索引订单表记录购买行为可作为隐式反馈标签表做多对多关联。表名关键字段类型索引userid, username, pwd_hash, pref_tagsINT, VARCHAR(50), VARCHAR(128), VARCHAR(255)PRIMARY(id), UNIQUE(username)spotid, name, city, category, price, tagsINT, VARCHAR(100), VARCHAR(50), VARCHAR(30), DECIMAL(8,2), VARCHAR(255)PRIMARY(id), INDEX(city), INDEX(category)ratingid, user_id, spot_id, rating, tsINT, INT, INT, TINYINT, DATETIMEPRIMARY(id), UNIQUE(user_id,spot_id), INDEX(spot_id)ordersid, user_id, spot_id, amount, statusINT, INT, INT, DECIMAL(10,2), TINYINTPRIMARY(id), INDEX(user_id)tagid, nameINT, VARCHAR(30)PRIMARY(id), UNIQUE(name)评分字段用 TINYINT 存 1 到 5别用 FLOAT省空间也避免精度问题。UNIQUE(user_id, spot_id)这条约束很重要能防止同一用户对同一景点重复评分否则算法输入就脏了。票价用 DECIMAL 不用 FLOAT金额计算不能有浮点误差这是血泪经验。3.2 建库建表脚本与初始数据导入CREATE DATABASE tourism_rec DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE tourism_rec; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, pwd_hash VARCHAR(128) NOT NULL, pref_tags VARCHAR(255) DEFAULT , created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE spot ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, city VARCHAR(50) NOT NULL, category VARCHAR(30) NOT NULL, price DECIMAL(8,2) DEFAULT 0, tags VARCHAR(255) DEFAULT , INDEX idx_city (city), INDEX idx_category (category) ) ENGINEInnoDB; CREATE TABLE rating ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, spot_id INT NOT NULL, rating TINYINT NOT NULL, ts DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_spot (user_id, spot_id), INDEX idx_spot (spot_id), CONSTRAINT fk_rating_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_rating_spot FOREIGN KEY (spot_id) REFERENCES spot(id) ) ENGINEInnoDB;字符集统一用utf8mb4因为景点描述里可能有生僻字或 emojiutf8存不下会报错。外键约束在毕业设计里建议保留能保证数据一致性答辩时也显得规范但如果你要批量导入几万条测试数据导入前先SET FOREIGN_KEY_CHECKS0导完再打开否则逐条校验慢到怀疑人生。初始数据至少准备 50 个景点、200 个用户、2000 条评分推荐结果才有统计意义太少的话相似度矩阵几乎全是零。3.3 用 Python 连接 MySQL 并做增删改查import pymysql conn pymysql.connect( hostlocalhost, port3306, userroot, passwordyour_pwd, databasetourism_rec, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) def add_rating(user_id, spot_id, rating): with conn.cursor() as cur: # 用 ON DUPLICATE KEY 实现“有则更新、无则插入” sql INSERT INTO rating (user_id, spot_id, rating) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE ratingVALUES(rating), tsNOW() cur.execute(sql, (user_id, spot_id, rating)) conn.commit() def get_user_ratings(user_id): with conn.cursor() as cur: cur.execute(SELECT spot_id, rating FROM rating WHERE user_id%s, (user_id,)) return cur.fetchall() add_rating(1, 3, 5) print(get_user_ratings(1))DictCursor让查询结果直接是字典省去手动映射字段的麻烦。ON DUPLICATE KEY UPDATE配合前面的唯一索引天然实现“重复评分就更新”比先查后插少一次往返。参数化查询%s是防 SQL 注入的底线别用字符串拼接这是数据库增删改查里最容易被忽视又最致命的一点。连接用完记得关或者用连接池mysql的数据库连接池在高并发场景下是必须的毕业设计里用DBUtils简单包一层就够。4. 从数据到推荐结果把算法接进 Flask 接口的完整链路4.1 项目目录结构与模块职责划分一个能讲清楚的毕业设计目录结构要干净。我一般这样分app.py是入口models/放数据库操作algo/放推荐算法routes/放接口static/和templates/放前端data/放初始数据脚本。算法模块只依赖传入的评分字典不直接碰数据库这样单元测试好写也方便你单独验证算法。数据库模块只负责读写不掺业务逻辑。接口层做参数校验和结果组装。三层分开答辩时老师问“你的算法和数据库怎么解耦的”你就有话说了。4.2 推荐接口的请求参数与返回格式from flask import Flask, request, jsonify from algo.itemcf import item_similarity, recommend from models.rating import load_all_ratings app Flask(__name__) app.route(/api/recommend, methods[GET]) def api_recommend(): user_id request.args.get(user_id, typeint) top_n request.args.get(top_n, default10, typeint) if not user_id: return jsonify({code: 400, msg: user_id required}), 400 if top_n 50: top_n 50 # 限制上限防止一次拉太多 ratings load_all_ratings() # {user: {spot: rating}} if str(user_id) not in ratings: return jsonify({code: 200, data: [], msg: new user, use content-based}) sim item_similarity(ratings) result recommend(str(user_id), ratings, sim, top_n) return jsonify({code: 200, data: [{spot: k, score: round(v, 4)} for k, v in result]}) if __name__ __main__: app.run(debugTrue, port5000)接口用 GET参数user_id必填、top_n选填默认 10。这里做了两件容易被忽略的事一是top_n上限截断防止有人传 10000 把内存打爆二是新用户判断如果用户不在评分表里直接返回空并提示走基于内容的冷启动分支而不是硬算出一个空结果让前端一脸懵。返回结构统一成code/data/msg前端好处理。round(v, 4)保留四位小数避免返回一长串浮点数。4.3 离线评估用准确率和召回率验证推荐质量光跑通不算完得证明推荐是有效的。把评分数据按 8:2 切成训练集和测试集对每个测试用户用训练集算出 top-N 推荐看有多少落在他测试集真正评过高分的景点里。准确率 命中数 / N召回率 命中数 / 测试集高分景点数。旅游场景下 N 取 10准确率能到 0.15 以上就算及格0.25 以上算不错。如果准确率低于 0.05先查是不是评分数据太稀疏再查相似度计算有没有把共同评分用户数为 1 的情况也算进去——那种相似度极不稳定应该设个阈值共同用户少于 2 个就置零。这个评估脚本一定要自己写一遍答辩时拿出数字比空谈算法强得多。5. 避坑与排查这类毕业设计最容易翻车的五个地方5.1 中文乱码从数据库到接口一路问号现象是景点名称在网页上显示成???或乱码。原因通常是三处字符集不统一建库时用了utf8而非utf8mb4Python 连接时没指定charset或者 Flask 返回时没设Content-Type。解决是按链路逐段排查建库建表统一utf8mb4pymysql.connect加charsetutf8mb4Flask 的jsonify默认就是 UTF-8 不用额外设但如果手动拼字符串返回记得Response(..., mimetypeapplication/json; charsetutf-8)。三处对齐乱码必消。5.2 推荐结果为空或永远推荐同几个景点现象是接口返回空列表或者不管哪个用户都推同样的东西。空结果多半是新用户没评分记录算法直接跳过这时要接基于内容的兜底分支。永远推同几个通常是热门景点评分人数多相似度累加时天然占优形成马太效应。解决办法是在最终排序前做一次多样性处理对同一城市或同一类别的景点最多保留 2 到 3 个其余降权。这个逻辑不复杂但很多同学不知道要做结果推荐列表全是故宫长城。5.3 数据库连接泄漏导致跑一会儿就卡死现象是系统刚启动正常请求几十次后越来越慢最后报 “too many connections”。原因是每次请求都新建连接却没关闭连接数被耗尽。解决是用连接池DBUtils.PooledDB几行代码就能包起来或者用with上下文确保关闭。另外检查有没有在循环里反复connect那是典型写法错误。这个坑在答辩演示时特别致命演示到一半卡住场面很难看。5.4 相似度矩阵计算太慢页面转圈现象是第一次请求推荐接口要等十几秒。原因是每次请求都重新算全量相似度矩阵景点几百个就是几万次两两计算。解决是把相似度矩阵离线算好存到内存或 Redis定时更新接口只做查表和加权。毕业设计里用模块级变量缓存即可进程启动时算一次。如果景点数量上千考虑只算每个景点的 top-K 相似K 取 50能砍掉大量无用计算。5.5 评分数据重复或越界导致算法结果诡异现象是推荐结果明显违背常理比如给讨厌历史景点的用户狂推故宫。先查评分表有没有重复记录——虽然建了唯一索引但如果导入数据时用了INSERT IGNORE或先删索引再导就可能漏进去。再查评分值有没有超出 1 到 5 的范围前端传参没校验的话一个 100 分的评分会把相似度算飞。解决是在接口层加参数校验评分值不在 1 到 5 直接拒绝数据库层用CHECK约束兜底。数据干净算法才可信。6. 让答辩加分的两个进阶技巧冷启动兜底与可解释推荐走到这里系统已经能跑了但想在一堆毕业设计里被记住还得补两块。第一块是冷启动兜底。新用户注册后没有任何评分协同过滤完全失效。我的做法是在注册时让用户勾选 3 到 5 个感兴趣的标签比如“自然风光”“历史古迹”“美食”存进user.pref_tags推荐接口检测到用户无评分时直接走标签匹配从spot.tags里找包含这些标签的景点按票价或热度排序返回。等用户产生几条评分后再平滑切换到协同过滤。这个切换阈值我一般设 5 条评分低于 5 条时混合权重偏向内容高于 5 条偏向协同。代码上就是一个 if 分支加权重调整但答辩时讲出“冷启动分阶段策略”比只讲一个算法完整得多。第二块是可解释推荐。老师最爱问“你为什么给他推这个”如果你只能答“算法算出来的”分数就上不去。做法是在推荐结果里附带理由如果是协同过滤推出来的找出贡献最大的那个已评分景点返回“因为你喜欢 XX所以推荐相似的 YY”如果是基于内容推的直接返回“符合你选择的‘自然风光’偏好”。实现上在recommend函数里记录每个候选景点的最高贡献来源一起返回。前端展示成一句话用户体验和答辩表现双赢。def recommend_with_reason(user, ratings, sim, top_n10): scores defaultdict(float) reason {} rated ratings[user] for i, r in rated.items(): for j, s in sim.get(i, {}).items(): if j in rated: continue scores[j] s * r # 记录贡献最大的来源景点作为推荐理由 if j not in reason or s * r reason[j][1]: reason[j] (i, s * r) ranked sorted(scores.items(), keylambda x: -x[1])[:top_n] return [{spot: k, score: round(v, 4), reason: f因为你喜欢{reason[k][0]}} for k, v in ranked]这段代码在原来基础上多维护了一个reason字典键是候选景点值是一个元组来源景点贡献分。每次累加时比较贡献分保留最大的那个来源。返回时拼成一句人话。参数上没什么要调的注意reason里可能没有某个候选景点的记录——理论上不会因为能进scores就一定有来源但保险起见可以加个默认值。最后说个我自己的习惯每次改完算法先跑离线评估脚本看准确率有没有掉再手动构造几个典型用户历史控、自然控、新用户看推荐结果是否符合直觉最后才接前端。这个顺序能挡住九成的低级错误。毕业设计拼的不是算法多高级而是每一步都讲得清、跑得稳、数据干净。希望帮到你。本文还有配套的精品资源点击获取
