在旅游信息化快速发展的今天,如何从海量旅游数据中挖掘有价值信息并为用户提供个性化推荐,成为旅游平台提升用户体验的关键技术挑战。本文将完整实现一个基于Python Flask框架的旅游景点推荐系统,涵盖数据爬取、存储、分析、推荐算法和可视化全流程,为计算机专业毕业设计提供完整解决方案。
1. 系统架构与技术选型
1.1 系统整体架构设计
旅游景点推荐系统采用典型的三层架构设计,包括数据层、业务逻辑层和表现层。数据层负责景点数据的采集、清洗和存储;业务逻辑层实现数据分析、推荐算法和用户管理等功能;表现层通过Web界面展示推荐结果和可视化分析。
系统技术栈选择基于Python生态,主要考虑Python在数据处理和机器学习领域的丰富库支持。Flask作为轻量级Web框架,适合快速开发原型系统,同时具备良好的扩展性。数据库选用MySQL,满足结构化数据存储需求。
1.2 核心技术组件介绍
Flask框架:作为系统的Web应用框架,Flask提供了路由、模板渲染、请求处理等核心功能。其轻量级特性使得系统启动快速,开发效率高。
爬虫技术:使用requests库发送HTTP请求,lxml库解析HTML页面,实现旅游网站数据的自动化采集。爬虫模块需要遵守robots协议,控制访问频率,避免对目标网站造成压力。
数据处理库:pandas用于数据清洗和预处理,numpy提供数值计算支持,jieba用于中文文本分词,为后续的文本分析做准备。
推荐算法:采用协同过滤和基于内容的推荐相结合的方式。协同过滤基于用户行为数据,基于内容的推荐利用景点特征信息,两者结合提升推荐准确性。
可视化库:ECharts用于生成交互式图表,WordCloud生成关键词词云,matplotlib用于静态图表绘制。
2. 开发环境搭建
2.1 Python环境配置
首先需要安装Python 3.7及以上版本。建议使用Anaconda发行版,它集成了常用的数据科学库,方便环境管理。
# 创建虚拟环境 conda create -n travel_recommend python=3.8 conda activate travel_recommend # 安装核心依赖 pip install flask==2.0.1 pip install requests==2.25.1 pip install pandas==1.3.3 pip install pymysql==1.0.2 pip install jieba==0.42.1 pip install scikit-learn==0.24.2 pip install wordcloud==1.8.12.2 数据库环境配置
系统使用MySQL 5.7或8.0版本。创建数据库和用户:
CREATE DATABASE travel_recommend CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'travel_user'@'localhost' IDENTIFIED BY 'password123'; GRANT ALL PRIVILEGES ON travel_recommend.* TO 'travel_user'@'localhost'; FLUSH PRIVILEGES;2.3 项目目录结构
建立清晰的目录结构有助于代码维护:
travel_recommend_system/ ├── app.py # Flask主程序 ├── config.py # 配置文件 ├── requirements.txt # 依赖列表 ├── spider/ # 爬虫模块 │ ├── __init__.py │ ├── base_spider.py │ └── travel_spider.py ├── models/ # 数据模型 │ ├── __init__.py │ ├── database.py │ └── entities.py ├── recommender/ # 推荐算法 │ ├── __init__.py │ ├── collaborative_filtering.py │ └── content_based.py ├── static/ # 静态资源 │ ├── css/ │ ├── js/ │ └── images/ └── templates/ # 模板文件 ├── base.html ├── index.html └── recommend.html3. 数据爬取模块实现
3.1 爬虫设计原则
旅游数据爬取需要遵循合法合规原则,严格控制访问频率,设置合理的User-Agent,避免对目标网站造成负担。实现时加入随机延时、异常重试等机制提高爬虫稳定性。
import requests import time import random from lxml import etree import json class TravelSpider: def __init__(self): self.session = requests.Session() self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } self.session.headers.update(self.headers) def get_html(self, url, delay=True): """获取页面HTML内容""" try: if delay: time.sleep(random.uniform(1, 3)) # 随机延时1-3秒 response = self.session.get(url, timeout=10) response.encoding = 'utf-8' if response.status_code == 200: return response.text else: print(f"请求失败,状态码:{response.status_code}") return None except Exception as e: print(f"请求异常:{e}") return None def parse_scenic_info(self, html): """解析景点详细信息""" if not html: return None try: selector = etree.HTML(html) scenic_data = {} # 解析景点名称 name = selector.xpath('//h1[@class="scenic-name"]/text()') scenic_data['name'] = name[0].strip() if name else '' # 解析评分 rating = selector.xpath('//span[@class="rating"]/text()') scenic_data['rating'] = float(rating[0]) if rating else 0.0 # 解析地址 address = selector.xpath('//span[@class="address"]/text()') scenic_data['address'] = address[0].strip() if address else '' # 解析门票价格 price = selector.xpath('//span[@class="price"]/text()') scenic_data['price'] = price[0].strip() if price else '' # 解析景点介绍 description = selector.xpath('//div[@class="description"]/text()') scenic_data['description'] = description[0].strip() if description else '' return scenic_data except Exception as e: print(f"解析异常:{e}") return None3.2 数据存储设计
爬取的数据需要结构化存储到MySQL数据库,设计合理的表结构:
-- 景点基本信息表 CREATE TABLE scenic_spots ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(200) NOT NULL, address VARCHAR(500), price DECIMAL(10,2), rating DECIMAL(3,1), description TEXT, city VARCHAR(100), province VARCHAR(100), tags VARCHAR(500), created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_city (city), INDEX idx_rating (rating) ); -- 用户行为表 CREATE TABLE user_behavior ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, scenic_id INT NOT NULL, behavior_type ENUM('view', 'collect', 'comment', 'rating'), behavior_value TEXT, created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (scenic_id) REFERENCES scenic_spots(id), INDEX idx_user_behavior (user_id, scenic_id) ); -- 用户评分表 CREATE TABLE user_ratings ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, scenic_id INT NOT NULL, rating TINYINT NOT NULL CHECK (rating >= 1 AND rating <= 5), created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_user_scenic (user_id, scenic_id), FOREIGN KEY (scenic_id) REFERENCES scenic_spots(id) );4. 数据处理与分析模块
4.1 数据清洗与预处理
爬取的原始数据往往包含噪声和缺失值,需要进行数据清洗:
import pandas as pd import numpy as np import jieba import jieba.analyse from sklearn.feature_extraction.text import TfidfVectorizer class DataProcessor: def __init__(self): self.stop_words = self.load_stop_words() def load_stop_words(self): """加载停用词表""" try: with open('data/stopwords.txt', 'r', encoding='utf-8') as f: return set([line.strip() for line in f]) except: return set() def clean_data(self, df): """数据清洗""" # 处理缺失值 df['price'] = df['price'].fillna(0) df['rating'] = df['rating'].fillna(df['rating'].mean()) df['description'] = df['description'].fillna('') # 价格格式统一 df['price'] = df['price'].apply(self.standardize_price) # 去除重复数据 df = df.drop_duplicates(subset=['name', 'address']) return df def standardize_price(self, price_str): """标准化价格格式""" if isinstance(price_str, (int, float)): return float(price_str) try: # 处理"免费"、"¥100"等格式 if '免费' in str(price_str): return 0.0 price_str = str(price_str).replace('¥', '').replace('元', '') return float(price_str) except: return 0.0 def extract_tags(self, text): """从文本中提取关键词标签""" if not text: return [] # 使用jieba提取关键词 tags = jieba.analyse.extract_tags(text, topK=10, withWeight=False) return [tag for tag in tags if tag not in self.stop_words]4.2 特征工程
为推荐算法准备特征数据:
from sklearn.preprocessing import MinMaxScaler from sklearn.metrics.pairwise import cosine_similarity class FeatureEngineer: def __init__(self): self.scaler = MinMaxScaler() self.vectorizer = TfidfVectorizer(max_features=1000) def prepare_scenic_features(self, df): """准备景点特征矩阵""" # 数值特征标准化 numerical_features = df[['rating', 'price']].fillna(0) numerical_scaled = self.scaler.fit_transform(numerical_features) # 文本特征向量化 text_features = df['description'].fillna('') + ' ' + df['tags'].fillna('') text_vectors = self.vectorizer.fit_transform(text_features) # 组合特征 from scipy.sparse import hstack feature_matrix = hstack([numerical_scaled, text_vectors]) return feature_matrix def calculate_similarity_matrix(self, feature_matrix): """计算景点相似度矩阵""" similarity_matrix = cosine_similarity(feature_matrix) return similarity_matrix5. 推荐算法实现
5.1 协同过滤推荐
基于用户的协同过滤算法:
import numpy as np from scipy.sparse.linalg import svds from sklearn.metrics.pairwise import cosine_similarity class CollaborativeFiltering: def __init__(self, n_factors=50, n_similar_users=20): self.n_factors = n_factors self.n_similar_users = n_similar_users self.user_factors = None self.item_factors = None def fit(self, user_item_matrix): """训练矩阵分解模型""" # 矩阵分解 U, sigma, Vt = svds(user_item_matrix, k=self.n_factors) sigma = np.diag(sigma) self.user_factors = U self.item_factors = Vt.T return self def predict_ratings(self, user_id, n_recommendations=10): """为用户预测评分并生成推荐""" user_index = user_id - 1 # 假设用户ID从1开始 # 计算用户对所有景点的预测评分 user_predicted = self.user_factors[user_index].dot(self.item_factors.T) # 获取Top-N推荐 top_indices = np.argsort(user_predicted)[::-1][:n_recommendations] return top_indices, user_predicted[top_indices] def find_similar_users(self, user_id, user_item_matrix): """查找相似用户""" user_index = user_id - 1 user_vector = user_item_matrix[user_index] # 计算用户相似度 similarities = cosine_similarity(user_vector, user_item_matrix)[0] # 排除自己,获取最相似的用户 similar_indices = np.argsort(similarities)[::-1][1:self.n_similar_users+1] return similar_indices, similarities[similar_indices]5.2 基于内容的推荐
class ContentBasedRecommender: def __init__(self, similarity_matrix): self.similarity_matrix = similarity_matrix def recommend(self, scenic_id, visited_scenics, n_recommendations=10): """基于内容相似度推荐""" scenic_index = scenic_id - 1 # 获取相似度分数 similarities = self.similarity_matrix[scenic_index] # 排除已访问的景点 visited_indices = [sid-1 for sid in visited_scenics if sid != scenic_id] similarities[visited_indices] = -1 # 设置为负值避免被选中 # 获取Top-N推荐 recommended_indices = np.argsort(similarities)[::-1][:n_recommendations] recommended_scores = similarities[recommended_indices] return recommended_indices, recommended_scores def hybrid_recommend(self, user_id, collaborative_scores, content_scores, alpha=0.7): """混合推荐:结合协同过滤和基于内容的方法""" # 加权融合 hybrid_scores = alpha * collaborative_scores + (1 - alpha) * content_scores # 排序获取最终推荐 final_indices = np.argsort(hybrid_scores)[::-1] return final_indices, hybrid_scores[final_indices]6. Flask Web应用开发
6.1 应用配置和路由设计
from flask import Flask, render_template, request, jsonify, session from flask_cors import CORS import pymysql from config import DevelopmentConfig app = Flask(__name__) app.config.from_object(DevelopmentConfig) CORS(app) # 数据库连接 def get_db_connection(): return pymysql.connect( host=app.config['DB_HOST'], user=app.config['DB_USER'], password=app.config['DB_PASSWORD'], database=app.config['DB_NAME'], charset='utf8mb4' ) @app.route('/') def index(): """首页""" return render_template('index.html') @app.route('/recommend', methods=['POST']) def get_recommendations(): """获取推荐结果""" try: data = request.get_json() user_id = data.get('user_id') city = data.get('city', '') preferences = data.get('preferences', {}) # 调用推荐算法 recommendations = generate_recommendations(user_id, city, preferences) return jsonify({ 'success': True, 'data': recommendations }) except Exception as e: return jsonify({ 'success': False, 'message': str(e) }), 500 @app.route('/scenic/<int:scenic_id>') def scenic_detail(scenic_id): """景点详情页""" conn = get_db_connection() try: with conn.cursor() as cursor: sql = "SELECT * FROM scenic_spots WHERE id = %s" cursor.execute(sql, (scenic_id,)) scenic = cursor.fetchone() if scenic: return render_template('scenic_detail.html', scenic=scenic) else: return "景点不存在", 404 finally: conn.close()6.2 用户界面设计
使用Bootstrap和ECharts构建响应式前端界面:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>旅游景点推荐系统</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js"></script> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <div class="container"> <a class="navbar-brand" href="/">旅游推荐系统</a> </div> </nav> <div class="container mt-4"> <div class="row"> <div class="col-md-3"> <div class="card"> <div class="card-header">筛选条件</div> <div class="card-body"> <form id="recommendForm"> <div class="mb-3"> <label class="form-label">目的地城市</label> <input type="text" class="form-control" id="city" name="city"> </div> <div class="mb-3"> <label class="form-label">价格范围</label> <select class="form-select" id="priceRange" name="priceRange"> <option value="0">不限</option> <option value="1">免费</option> <option value="2">0-100元</option> <option value="3">100-300元</option> </select> </div> <button type="submit" class="btn btn-primary w-100">获取推荐</button> </form> </div> </div> </div> <div class="col-md-9"> <div class="card"> <div class="card-header">推荐结果</div> <div class="card-body"> <div id="recommendations" class="row"> <!-- 推荐结果将通过JavaScript动态加载 --> </div> </div> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="/static/js/recommend.js"></script> </body> </html>7. 数据可视化实现
7.1 景点分布可视化
使用ECharts实现景点地理位置分布图:
// 景点分布地图 function initScenicMap() { const chart = echarts.init(document.getElementById('scenic-map')); fetch('/api/scenic/distribution') .then(response => response.json()) .then(data => { const option = { title: { text: '景点地理分布', left: 'center' }, tooltip: { trigger: 'item', formatter: function(params) { return `${params.name}<br/>景点数量: ${params.value}`; } }, visualMap: { min: 0, max: 100, text: ['高', '低'], calculable: true, inRange: { color: ['#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027'] } }, series: [{ name: '景点分布', type: 'map', map: 'china', roam: true, emphasis: { label: { show: true } }, data: data.distribution }] }; chart.setOption(option); }); }7.2 推荐结果可视化
from wordcloud import WordCloud import matplotlib.pyplot as plt import base64 from io import BytesIO def generate_wordcloud(tags_data): """生成景点标签词云""" wordcloud = WordCloud( font_path='static/fonts/simhei.ttf', width=800, height=400, background_color='white', max_words=100 ).generate_from_frequencies(tags_data) # 转换为base64编码图片 buffer = BytesIO() plt.figure(figsize=(10, 5)) plt.imshow(wordcloud, interpolation='bilinear') plt.axis('off') plt.tight_layout() plt.savefig(buffer, format='png', dpi=300, bbox_inches='tight') plt.close() image_base64 = base64.b64encode(buffer.getvalue()).decode() return f"data:image/png;base64,{image_base64}" @app.route('/api/visualization/wordcloud') def get_wordcloud(): """获取词云可视化数据""" conn = get_db_connection() try: with conn.cursor() as cursor: # 获取景点标签频率 sql = "SELECT tags, COUNT(*) as count FROM scenic_spots GROUP BY tags" cursor.execute(sql) tags_data = {} for row in cursor.fetchall(): tags = row['tags'].split(',') if row['tags'] else [] for tag in tags: tag = tag.strip() if tag: tags_data[tag] = tags_data.get(tag, 0) + 1 wordcloud_image = generate_wordcloud(tags_data) return jsonify({ 'wordcloud': wordcloud_image }) finally: conn.close()8. 系统测试与优化
8.1 功能测试用例
设计完整的测试用例确保系统稳定性:
import unittest from app import app import json class TestTravelRecommendSystem(unittest.TestCase): def setUp(self): self.app = app.test_client() self.app.testing = True def test_home_page(self): """测试首页访问""" response = self.app.get('/') self.assertEqual(response.status_code, 200) def test_recommendation_api(self): """测试推荐API""" test_data = { 'user_id': 1, 'city': '北京', 'preferences': {'price_range': '0-100'} } response = self.app.post('/recommend', data=json.dumps(test_data), content_type='application/json') self.assertEqual(response.status_code, 200) data = json.loads(response.data) self.assertTrue(data['success']) self.assertIn('data', data) def test_scenic_detail(self): """测试景点详情页""" response = self.app.get('/scenic/1') self.assertEqual(response.status_code, 200) def test_invalid_user(self): """测试无效用户处理""" test_data = {'user_id': 999999} response = self.app.post('/recommend', data=json.dumps(test_data), content_type='application/json') self.assertEqual(response.status_code, 200) data = json.loads(response.data) # 系统应该能够处理无效用户,返回默认推荐 self.assertTrue(data['success']) if __name__ == '__main__': unittest.main()8.2 性能优化策略
数据库优化:
- 为常用查询字段建立索引
- 使用连接池管理数据库连接
- 对大数据量表进行分表处理
缓存策略:
from flask_caching import Cache cache = Cache(config={'CACHE_TYPE': 'SimpleCache'}) cache.init_app(app) @app.route('/api/scenic/hot') @cache.cached(timeout=300) # 缓存5分钟 def get_hot_scenics(): """获取热门景点(带缓存)""" # 查询逻辑... return jsonify(hot_scenics)异步处理: 对于耗时的推荐计算任务,使用Celery进行异步处理:
from celery import Celery def make_celery(app): celery = Celery( app.import_name, backend=app.config['CELERY_RESULT_BACKEND'], broker=app.config['CELERY_BROKER_URL'] ) class ContextTask(celery.Task): def __call__(self, *args, **kwargs): with app.app_context(): return self.run(*args, **kwargs) celery.Task = ContextTask return celery celery = make_celery(app) @celery.task def generate_recommendations_async(user_id, preferences): """异步生成推荐结果""" # 推荐计算逻辑... return recommendations9. 部署与运维
9.1 生产环境部署
使用Gunicorn作为WSGI服务器,Nginx作为反向代理:
# 安装Gunicorn pip install gunicorn # 启动命令 gunicorn -w 4 -b 0.0.0.0:8000 app:app # Nginx配置 server { listen 80; server_name your_domain.com; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static { alias /path/to/your/static; } }9.2 监控与日志
配置日志记录和性能监控:
import logging from logging.handlers import RotatingFileHandler def setup_logging(app): """配置日志系统""" if not app.debug: # 生产环境日志配置 file_handler = RotatingFileHandler( 'logs/travel_recommend.log', maxBytes=1024*1024, backupCount=10 ) file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' )) file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO)10. 常见问题与解决方案
10.1 爬虫被封禁问题
问题现象:爬虫频繁被目标网站封禁IP解决方案:
- 使用代理IP池轮换请求
- 设置合理的请求间隔时间
- 模拟真实浏览器行为(User-Agent轮换)
- 遵守robots.txt协议
class ProxyManager: def __init__(self): self.proxies = self.load_proxies() self.current_index = 0 def get_proxy(self): """获取代理IP""" if not self.proxies: return None proxy = self.proxies[self.current_index] self.current_index = (self.current_index + 1) % len(self.proxies) return proxy def load_proxies(self): """加载代理IP列表""" # 从文件或API获取代理IP try: with open('proxies.txt', 'r') as f: return [line.strip() for line in f if line.strip()] except: return []10.2 推荐冷启动问题
问题现象:新用户或新景点缺乏历史数据,推荐效果差解决方案:
- 新用户:基于人口统计学信息推荐热门景点
- 新景点:基于内容相似度进行推荐
- 采用混合推荐策略平衡准确性和多样性
10.3 系统性能瓶颈
问题现象:用户量增大时系统响应变慢解决方案:
- 使用Redis缓存热门推荐结果
- 对推荐算法进行离线计算
- 采用分布式计算框架处理大数据量
- 数据库查询优化和索引优化
本系统完整实现了旅游景点推荐的全流程,从数据采集到推荐生成,再到可视化展示。系统采用模块化设计,便于扩展和维护,为计算机毕业设计提供了完整的技术解决方案。在实际部署时,需要根据具体需求调整参数配置,并持续优化推荐算法以提升用户体验。