ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python的智能旅游推荐系统:协同过滤与Flask毕设实战

基于Python的智能旅游推荐系统:协同过滤与Flask毕设实战 简介基于Python的智能旅游推荐系统完整毕业设计项目包面向计算机相关专业本科生与专科生适用于毕业设计、课程设计或期末大作业场景。项目采用Django框架构建后端前端以HTML、Vue、CSS为主配合JavaScript实现页面交互并内置数据库脚本前后端代码、运行脚本与所需软件工具均已打包在内。压缩包共797个文件约20.19MB以JS、SVG、VUE、CSS、HTML、Python源码、SQL脚本及图片资源为主目录清晰便于按模块查阅与二次开发。目前已有227人学习/下载可作为智能旅游推荐类毕业设计选题的完整参考方案。资源包含可直接运行的Django项目源码、数据库初始化脚本、安装运行批处理文件以及关键文件备份可在PyCharm中直接导入调试功能涵盖旅游推荐系统的前后端核心模块界面美观、管理便捷适合用于快速理解项目全貌并在此基础上扩展功能或撰写设计文档。1. 基于python的智能旅游推荐系统毕业设计项目怎么从跑通到答辩打开这个基于python的智能旅游推荐系统的压缩包你会看到典型的毕设项目结构Python后端代码、前端模板、SQL数据库脚本和一份说明文档。这类项目的核心价值不在智能二字有多深而在于它把推荐算法、Web框架和数据库三件事完整串了起来——用户登录后能看到猜你喜欢的景点列表管理员能维护景点数据后台有真实的评分记录在支撑推荐计算。适合拿来做毕设或课程设计的人尤其是想在简历里写我独立完成了一个推荐系统的初学者。我拿到这类项目的第一件事不是读代码而是先把数据库跑起来再顺着一条推荐链路去理解代码最后才谈得上改算法、加功能、准备答辩。2. 先看懂项目的三层结构与数据模型动手改代码前的必修课2.1 为什么这类毕设普遍选Flask而不是Django旅游推荐系统的毕设项目最常见的技术组合是Flask MySQL Jinja2模板部分版本会用Django重写。Flask出现频率更高的原因很实际框架本身轻一个app.py就能承载路由和业务逻辑对毕设查重和答辩讲解都友好。Django的ORM和Admin后台虽然强大但电池全带的代价是代码量翻倍导师问起这个中间件是干嘛的时容易答不上来。从分层看典型结构是三层表现层用Jinja2渲染HTML页面业务层用Python写推荐算法和用户操作逻辑数据层用MySQL存用户、景点、评分、收藏四类核心数据。有的项目会把推荐算法单独拆成recommend.py在app.py里import调用这是比较好的习惯——答辩时你可以直接说推荐模块独立封装便于替换算法。2.2 四张核心表的设计用户、景点、评分、收藏数据库是整个项目的地基。打开SQL脚本通常是project.sql或travel.sql你最需要关注的是这四张表-- 用户表存账号信息和注册时间 CREATE TABLE user ( uid int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(255) NOT NULL COMMENT 密码md5加密后存储, register_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (uid), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 景点表核心推荐对象字段越全越好 CREATE TABLE scenic ( sid int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 景点名称, city varchar(50) DEFAULT NULL COMMENT 所在城市, category varchar(50) DEFAULT NULL COMMENT 类别自然/人文/主题乐园等, score decimal(3,1) DEFAULT 0.0 COMMENT 平台评分, price decimal(10,2) DEFAULT 0.00 COMMENT 门票价格, description text COMMENT 景点描述可用于基于内容的召回, PRIMARY KEY (sid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 评分表推荐算法的数据来源 CREATE TABLE rating ( id int(11) NOT NULL AUTO_INCREMENT, uid int(11) NOT NULL COMMENT 用户ID, sid int(11) NOT NULL COMMENT 景点ID, rating tinyint(4) DEFAULT 5 COMMENT 评分1-5, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_uid (uid), KEY idx_sid (sid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 收藏表虽然没有评分但能反映隐式反馈 CREATE TABLE favorite ( id int(11) NOT NULL AUTO_INCREMENT, uid int(11) NOT NULL, sid int(11) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_uid_sid (uid,sid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明rating字段类型用tinyint而不是int是为了限制取值0-5并节省存储uk_uid_sid联合唯一索引防止重复收藏utf8mb4是必选项否则插入emoji表情或生僻地名时会报Incorrect string value错误。密码字段看到md5加密时答辩时要能说出它的局限——现在更推荐用werkzeug.security的generate_password_hash。2.3 启动项目的第一步建库、导数据、配置连接拿到项目后别急着跑python app.py先确认数据库就绪mysql -u root -p source /path/to/project.sql; show tables;之后修改db.py或config.py里的连接参数import pymysql db_config { host: localhost, port: 3306, user: root, password: 你的密码, database: travel_db, charset: utf8mb4, cursorclass: pymysql.cursors.DictCursor } def get_conn(): # 每次请求创建连接用完即关避免连接泄漏 return pymysql.connect(**db_config)逻辑说明每个请求独立创建连接是Flask小项目的常见做法简单但不适合高并发。如果导师问性能瓶颈在哪答案就在这里——下一步优化方向是使用DBUtils.PooledDB连接池。数据库连接池的核心参数是maxconnections最大连接数和blocking连接耗尽时是否等待对毕设流量来说maxconnections10足够。注意source导入SQL时如果报错八成是脚本里有DROP DATABASE语句且当前用户权限不足。用mysql -u root -p进root账户操作能解决绝大多数导入问题。3. 推荐算法怎么落地协同过滤的Python实现与参数调优3.1 选UserCF还是ItemCF旅游场景的答案很明确推荐算法是这类项目的灵魂也是答辩时导师必问的地方。基于Python的旅游推荐系统里最常见的算法是实现UserCF基于用户的协同过滤——它的逻辑是和你相似的人喜欢什么就推荐给你。选它不是因为效果最好而是因为实现直观、代码量适中、讲起来容易。但要从原理上讲清楚你得知道为什么旅游场景更适合UserCF而不是ItemCF旅游决策是低频、高消费行为用户一年可能只去几个地方但同一个城市或类别的景点之间有很强的替代性。UserCF能捕捉到品味相似的用户这个群体特征对发现新目的地更有效。ItemCF擅长的是新闻、电商这类物品重复消费高的场景——没有用户会反复买同一件衣服或反复看同一条新闻但用户确实会反复去同一个景区。3.2 皮尔逊相似度与评分预测核心代码拆解看项目里的recommend.py大概率能看到这段逻辑的变体import math from collections import defaultdict def user_similarity(ratings): 计算用户之间的皮尔逊相关系数 ratings: dict, {uid: {sid: rating}} 返回: dict, {(uid1, uid2): similarity} # 构建用户-景点倒排表只保留双方都评过分的景点 sim_dict {} user_list list(ratings.keys()) for i in range(len(user_list)): for j in range(i 1, len(user_list)): u1, u2 user_list[i], user_list[j] common set(ratings[u1].keys()) set(ratings[u2].keys()) if len(common) 2: # 共同评分的景点太少相似度无意义 continue # 分别计算两个用户的评分均值 avg1 sum(ratings[u1].values()) / len(ratings[u1]) avg2 sum(ratings[u2].values()) / len(ratings[u2]) # 皮尔逊相关系数公式 numerator sum((ratings[u1][sid] - avg1) * (ratings[u2][sid] - avg2) for sid in common) denom1 math.sqrt(sum((ratings[u1][sid] - avg1) ** 2 for sid in common)) denom2 math.sqrt(sum((ratings[u2][sid] - avg2) ** 2 for sid in common)) if denom1 0 or denom2 0: continue sim numerator / (denom1 * denom2) sim_dict[(u1, u2)] sim return sim_dict def recommend(uid, ratings, top_k5): 为指定用户推荐景点 top_k: 返回前N个推荐结果 sim_dict user_similarity(ratings) # 找到与目标用户最相似的K个用户 related {k: v for k, v in sim_dict.items() if uid in k} sorted_users sorted(related.items(), keylambda x: x[1], reverseTrue)[:3] # 收集相似用户评分过的景点按相似度加权汇总 score_dict defaultdict(float) for (u1, u2), sim in sorted_users: neighbor u2 if u1 uid else u1 for sid, rating in ratings[neighbor].items(): if sid in ratings[uid]: continue # 用户已经去过/评过分的不推荐 score_dict[sid] sim * rating # 排序取Top-N ranked sorted(score_dict.items(), keylambda x: x[1], reverseTrue)[:top_k] return [sid for sid, score in ranked]这段代码里有三个关键参数答辩和调优都围绕它们展开第一个是len(common) 2的阈值。毕设自带的评分数据通常只有几十条如果阈值设为3或5能算出来的用户对会非常少推荐列表直接为空。设成2是保证至少有两个共同评分项的最低要求实际工程里这个值通常需要根据数据量动态调整。第二个是相似用户数[:3]。这是推荐系统里的经典超参K。K值太小推荐结果的覆盖度差K值太大噪声用户会稀释推荐的准确性。对毕设的数据量级比如20个用户、50个景点K3到K5是经验区间。第三个是评分预测的加权方式。代码里用了sim * rating的累加没有做归一化。更严谨的做法是除以相似度绝对值之和防止高相似度用户对结果的主导过强。面试或答辩时主动提这一点会显得你真的理解公式而不是背代码。3.3 冷启动的兜底方案基于内容的推荐纯协同过滤有个致命伤——新用户没有评分记录新景点没有用户评分算法直接失效。导师大概率会问新用户登录后推荐什么。看项目里是不是有这段兜底逻辑def content_based_recommend(uid, conn, top_n6): 冷启动兜底按用户注册时选择的偏好城市类别推荐 with conn.cursor() as cursor: # 用户没有评分时先看注册时填的偏好城市 cursor.execute(SELECT prefer_city FROM user WHERE uid%s, (uid,)) result cursor.fetchone() prefer_city result[prefer_city] if result else 北京 # 按城市过滤再按平台评分降序 sql SELECT sid, name, score FROM scenic WHERE city%s ORDER BY score DESC, price ASC LIMIT %s cursor.execute(sql, (prefer_city, top_n)) return cursor.fetchall()逻辑说明基于内容的推荐本质是做特征匹配——用户偏好城市匹配景点城市再按平台评分排序。这个方案没有个性化但能保证新用户登录后页面不空。毕设项目里如果看到热搜榜、热门景点列表本质也是内容推荐的简化版。参数top_n控制初始展示数量设6个比较合适——一屏展示两行视觉效果好且不会让用户觉得选择困难。4. 把推荐结果接到Web界面Flask路由与数据库增删改查4.1 登录注册与Session推荐系统怎么认出你是谁推荐系统要个性化先要解决当前用户是谁的问题。Flask里用Session存用户状态核心逻辑看这段from flask import Flask, render_template, request, redirect, session import hashlib app Flask(__name__) app.secret_key your_secret_key_here # 必填否则session无法工作 app.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form.get(username) password request.form.get(password) # 实际项目中用werkzeug的check_password_hash更安全 md5_pwd hashlib.md5(password.encode(utf-8)).hexdigest() conn get_conn() with conn.cursor() as cursor: cursor.execute( SELECT uid, username FROM user WHERE username%s AND password%s, (username, md5_pwd) ) user cursor.fetchone() conn.close() if user: session[uid] user[uid] session[username] user[username] return redirect(/) else: return render_template(login.html, error用户名或密码错误) return render_template(login.html)逻辑说明session[uid]是后续所有推荐逻辑的入口。登录成功后跳转首页首页视图函数里读session.get(uid)有值就走个性化推荐没值就走热门榜单。这里有个细节SQL语句用%s占位符传参千万不要用fSELECT ... WHERE username{username}——那是SQL注入的重灾区导师看到会直接扣分。4.2 推荐接口的完整链路从数据库读取到页面渲染首页推荐是项目的门面这段代码串起了数据库查询和推荐算法app.route(/) def index(): uid session.get(uid) conn get_conn() if not uid: # 未登录展示热门景点 with conn.cursor() as cursor: cursor.execute( SELECT sid, name, city, score FROM scenic ORDER BY score DESC LIMIT 8 ) hot_scenics cursor.fetchall() return render_template(index.html, scenicshot_scenics) # 已登录先查用户评分记录 with conn.cursor() as cursor: cursor.execute(SELECT sid, rating FROM rating WHERE uid%s, (uid,)) rating_rows cursor.fetchall() conn.close() # 把SQL结果转成算法需要的dict结构 user_ratings {row[sid]: row[rating] for row in rating_rows} if len(user_ratings) 2: # 评分样本太少走内容推荐兜底 return redirect(/?cold_start1) # 从全局评分表构建用户-评分矩阵 all_ratings build_rating_matrix(conn) recommend_ids recommend(uid, all_ratings, top_k6) # 用推荐出的ID列表回查景点详情 with get_conn() as conn: cursor conn.cursor() placeholders ,.join([%s] * len(recommend_ids)) cursor.execute( fSELECT sid, name, city, score FROM scenic WHERE sid IN ({placeholders}), recommend_ids ) recommend_scenics cursor.fetchall() return render_template(index.html, scenicsrecommend_scenics)这段代码演示了完整链路查用户评分 → 判断能否走协同过滤 → 构建全局评分矩阵 → 算法推荐 → 回查景点信息 → 渲染模板。注意最后一处查询用IN语句时占位符数量必须和列表长度一致f前缀的SQL要用列表参数配合不能直接拼字符串。参数top_k6要和前端模板里展示位数对齐。提示build_rating_matrix这个函数在各版本项目里写法差异很大有的是全表扫描后用defaultdict组装有的直接查SELECT uid, sid, rating。数据量几百条时怎么实现都行但答辩时要能说清楚这是把关系型数据转成算法输入的内存结构。4.3 数据库增删改查管理后台的必备操作毕设的系统二字还体现在管理端。景点管理模块覆盖了SQL的增删改查四种操作也是导师验证你是否真的会写数据库的试金石app.route(/admin/scenic/add, methods[POST]) def add_scenic(): name request.form.get(name) city request.form.get(city) category request.form.get(category) score request.form.get(score, 0) price request.form.get(price, 0) conn get_conn() try: with conn.cursor() as cursor: cursor.execute( INSERT INTO scenic (name, city, category, score, price) VALUES (%s, %s, %s, %s, %s), (name, city, category, score, price) ) conn.commit() # 增删改必须commit否则数据不落库 return redirect(/admin/scenic/list) except Exception as e: conn.rollback() # 出错回滚保持数据一致性 return f添加失败: {str(e)} finally: conn.close()逻辑说明commit()和rollback()是初学者最容易漏的两行。pymysql默认不开启自动提交不commit的话页面显示成功数据库里却没有数据这种假成功在毕设答辩现场翻车率极高。另外注意finally里的conn.close()——连接不关闭跑几次就会报Too many connections。5. Python毕设旅游推荐的5个必踩坑从启动失败到推荐结果不合理5.1 现象一Flask启动报错ModuleNotFoundError: No module named pymysql原因当前Python环境里没有安装项目依赖的第三方库。打开压缩包里的requirements.txt常见的依赖有flask、pymysql、werkzeug个别项目还会用到pandas或numpy。解决在项目目录下执行pip install -r requirements.txt。如果是在vscode里运行先确认右下角的Python解释器是你装库的那个环境CtrlShiftP选Python: Select Interpreter切换。配好环境这一步熟练工两分钟新手可能卡半小时——尤其当电脑里同时装了Python 3.8和3.12时pip装到的和运行用的经常不是同一个。5.2 现象二导入SQL脚本后中文乱码原因SQL文件本身是UTF-8编码但mysql命令行客户端默认用的是系统字符集Windows下往往是gbk导入时中文变成了锟斤拷。解决导入前先执行SET NAMES utf8mb4;或者用带编码参数的导入命令mysql -u root -p --default-character-setutf8mb4 travel_db project.sql建表时看到DEFAULT CHARSETutf8mb4还不够连接层也要统一。Python代码里db_config的charset参数、MySQL表定义、SQL文件编码三者必须一致缺一个就是乱码。这算是数据库操作里最玄学的一类问题实际排查时先看的是连接参数其次才是文件编码。5.3 现象三推荐结果永远是空的原因协同过滤算法的输入是评分矩阵而项目自带的评论数据可能只有一个管理员账号的评分。len(user_ratings) 2判断直接拦截或者相似度计算里common 2把用户对全部跳过结果自然为空。解决往rating表里多插几条测试数据至少要覆盖3个用户、10个景点、每个用户评过5个以上景点。批量插入的SQL写法INSERT INTO rating (uid, sid, rating) VALUES (1, 1, 5), (1, 2, 4), (1, 3, 3), (2, 1, 4), (2, 2, 5), (2, 4, 5), (3, 1, 3), (3, 3, 5), (3, 4, 4);5.4 现象四点击收藏按钮后页面报Duplicate entry原因favorite表有uk_uid_sid联合唯一索引用户对同一个景点点了两次收藏第二次插入时主键或唯一键冲突。解决收藏接口里先查后插或者用INSERT IGNORE吞掉重复更标准的写法是INSERT INTO favorite (uid, sid) VALUES (%s, %s) ON DUPLICATE KEY UPDATE create_time CURRENT_TIMESTAMP;5.5 现象五换了台电脑项目跑不起来原因这是毕设项目的通病——config.py里写死了数据库密码图片路径用了绝对路径C:\Users\xxx\...代码在别人的机器上必然翻车。解决把绝对路径改成相对路径用os.path.join(os.path.dirname(__file__), static/images)定位资源文件。数据库连接参数放到独立的config.py里提交到压缩包前把密码改成root级别的通用值并注释说明。6. 让答辩站得住脚用离线评估证明你的推荐有效推荐系统最怕的不是跑不出来而是跑出来了却说不清为什么推荐得好。准备答辩时用这段离线评估代码给自己一个数字答案def evaluate_recall(ratings, test_ratings, k5): 在测试集上计算召回率K hit_count 0 test_count 0 for uid, sid in test_ratings: test_count 1 rec_list recommend(uid, ratings, top_kk) if sid in rec_list: hit_count 1 recall hit_count / test_count if test_count 0 else 0 return recall # 使用示例留一法评估每次拿掉一条评分当测试集 ratings_all load_all_ratings(rating表数据) test_cases load_all_ratings(临时挑出的5条评分) score evaluate_recall(ratings_all, test_cases, k5) print(fRecall5: {score:.2f})评估逻辑走的是留一法把用户的一条评分当作未来行为藏起来用剩余评分做推荐看推荐列表里是否包含这条被藏起来的景点。k5表示推荐列表长度为5。毕设数据量下Recall5在0.2到0.5之间都算正常不用追求高数字关键是能解释清楚指标含义。答辩时还有一个高频追问这个推荐系统和直接按评分排序有什么区别 准备一句老实的回答协同过滤考虑了用户之间的相似性能看到用户自己没去过的景点类型热门榜单只是全局统计没有个性化。 如果导师追问冷启动把第3.3节的兜底方案讲明白就够了。最后说一个我的习惯交项目前把requirements.txt、数据库初始化脚本、README运行说明三件套检查一遍。README里写清楚Python版本推荐3.8到3.10、MySQL版本5.7或8.0、依赖安装命令、测试账号密码。因为毕设项目不只是给导师看的也是你三个月后自己回头看时最快的恢复记忆方式。这个习惯帮我避过很多次代码还在但想不起怎么跑的尴尬希望帮到你。本文还有配套的精品资源点击获取
返回列表