简介这是一套基于Flask框架开发的旅游网站数据分析与可视化系统源码面向Python初学者及Web开发入门者聚焦餐饮旅游领域的真实数据处理与前端展示场景可支撑课程设计、毕业设计或小型商业分析原型开发。资源包共251个文件涵盖20个核心Python后端脚本、11个HTML页面、41个JS交互逻辑、20个CSS样式文件及76个GIF动效资源辅以MySQL数据库脚本.sql、模型文件.pkl/.npy和配置说明readme/ini整体压缩包达79.34MB结构完整、模块清晰。已有145人学习下载体现了其在教学实践中的实用价值。用户可直接运行Flask服务获得含用户行为分析、景点热度统计、评论情感可视化等多维度图表的Web界面代码注释充分前端集成Bootstrap、Layui、jQuery UI及Font Awesome等主流库便于理解前后端协同逻辑与数据流闭环设计。1. 一个跑在本地的旅游数据驾驶舱Flask MySQL 构建轻量级分析看板你不需要部署到云服务器也不用配 Nginx 或 Gunicorn——这个 Python Flask 项目开箱即用python app.py启动后浏览器访问http://127.0.0.1:5000就能看到完整的旅游网站数据分析界面热门目的地词云、用户地域分布地图、订单金额趋势折线图、景区评分热力图、评论情感倾向柱状图。它不是教学 Demo而是真实可运行的最小可行系统MVP数据库结构已预置 5 张表users,orders,scenic_spots,reviews,regions含 3000 条模拟数据覆盖用户注册、下单、浏览、评价全流程。适合刚学完 Flask 基础、想立刻上手「真实业务场景」的开发者也适合旅游类中小企业快速搭建内部数据看板原型无需对接 BI 工具即可完成基础归因分析。所有前端资源Bootstrap 4、Layui 2.8、Layer 弹层、jQuery UI全部内联或本地引用不依赖 CDN断网也能加载页面。2. 从零启动Flask 后端服务与 MySQL 数据库初始化实操2.1 环境准备与依赖安装避开 pip 版本冲突陷阱该项目基于 Python 3.7–3.9 兼容设计强烈建议使用虚拟环境隔离依赖。不要直接pip install -r requirements.txt—— 原始源码未提供requirements.txt需手动确认核心依赖版本# 创建并激活虚拟环境推荐 Python 3.8 python3.8 -m venv tourism_env source tourism_env/bin/activate # Linux/macOS # tourism_env\Scripts\activate # Windows # 安装明确版本组合经实测兼容性最佳 pip install flask2.2.5 pip install flask-sqlalchemy3.0.5 pip install pymysql1.1.0 pip install pandas1.5.3 pip install matplotlib3.7.1 pip install jieba0.42.1 pip install wordcloud1.9.2注意matplotlib必须锁定3.7.1版本。高版本如 3.8在 Flask 模板中渲染中文会报Font family [sans-serif] not found错误导致词云和图表乱码。jieba低于 0.42 会导致旅游景点名切词不准如“张家界国家森林公园”被错误切为“张家界/国家/森林/公园”。2.2 MySQL 数据库建库与表结构导入Navicat / SQLyog 图形化操作指南项目要求 MySQL 5.7必须启用utf8mb4字符集否则评论中的 emoji 和生僻地名如“乌鲁木齐全疆”会存储失败。在 Navicat 中执行以下操作新建数据库tourism_db排序规则选utf8mb4_unicode_ci右键该库 → “运行SQL文件” → 选择项目根目录下的schema.sql若无此文件则按以下 DDL 手动创建-- users 表记录用户注册信息 CREATE TABLE users ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, province varchar(20) DEFAULT NULL, city varchar(30) DEFAULT NULL, reg_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- scenic_spots 表景区基础信息含经纬度用于地图渲染 CREATE TABLE scenic_spots ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, province varchar(20) DEFAULT NULL, lat decimal(10,8) DEFAULT NULL, lng decimal(11,8) DEFAULT NULL, score decimal(3,2) DEFAULT 0.00, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- orders 表订单流水关联用户与景区 CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, spot_id int(11) NOT NULL, amount decimal(10,2) NOT NULL, order_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_spot (user_id,spot_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;提示scenic_spots表中lat/lng字段精度设为decimal(10,8)和decimal(11,8)是关键。低精度如float会导致百度地图 JS API 渲染时坐标偏移超 500 米尤其在西部高原地区误差放大。2.3 Flask 配置文件解析DATABASE_URI 的三种写法与连接池设置项目核心配置位于config.py其中数据库连接字符串决定能否成功读取数据# config.py import os class Config: SECRET_KEY os.environ.get(SECRET_KEY) or dev-key-for-tourism # 方式一明文写死仅开发测试用 SQLALCHEMY_DATABASE_URI mysqlpymysql://root:password127.0.0.1:3306/tourism_db?charsetutf8mb4 # 方式二环境变量注入推荐 # SQLALCHEMY_DATABASE_URI os.environ.get(DATABASE_URL) or \ # mysqlpymysql://{}:{}{}:{}/{}.format( # os.environ.get(DB_USER, root), # os.environ.get(DB_PASS, password), # os.environ.get(DB_HOST, 127.0.0.1), # os.environ.get(DB_PORT, 3306), # os.environ.get(DB_NAME, tourism_db) # ) # 方式三PyMySQL 连接池高并发必备 SQLALCHEMY_ENGINE_OPTIONS { pool_size: 10, pool_recycle: 3600, pool_pre_ping: True, max_overflow: 20 }pool_size10初始连接数旅游系统日活 1000 时足够pool_recycle3600每小时重连一次避免 MySQLwait_timeout默认 28800 秒导致空闲连接失效pool_pre_pingTrue每次取连接前执行SELECT 1检测存活防止网络闪断后报Lost connection to MySQL server during query启动前务必检查app.py中是否启用配置# app.py from flask import Flask from config import Config from flask_sqlalchemy import SQLAlchemy app Flask(__name__) app.config.from_object(Config) # ← 关键必须调用此行加载配置 db SQLAlchemy(app)3. 前端可视化模块拆解Layui 地图 Bootstrap 词云 Layer 弹窗联动3.1 Layui 地图组件集成百度地图 API 密钥申请与坐标转换项目前端使用 Layui 封装的百度地图非高德需在templates/index.html中替换ak参数!-- templates/index.html 第 123 行附近 -- script typetext/javascript srchttps://api.map.baidu.com/api?v3.0akYOUR_BAIDU_AK/script申请 AK 步骤访问 百度地图开放平台 → 控制台 → 创建应用应用类型选「浏览器端」Referer 白名单填127.0.0.1:*和localhost:*获取 AK 后必须开启「地图 JavaScript API」服务否则控制台报BMap is not defined地图初始化代码在static/js/main.js// static/js/main.js function initMap() { var map new BMap.Map(allmap); map.centerAndZoom(new BMap.Point(104.114, 37.55), 4); // 中国中心点 map.enableScrollWheelZoom(true); // 从 Flask 接口获取景区坐标数据关键后端返回 JSON 格式 $.get(/api/spots, function(data) { data.forEach(function(spot) { var point new BMap.Point(spot.lng, spot.lat); var marker new BMap.Marker(point); var label new BMap.Label(spot.name, {offset: new BMap.Size(20, -10)}); marker.setLabel(label); map.addOverlay(marker); }); }); }注意/api/spots接口返回的lng/lat字段顺序必须是经度在前、纬度在后百度地图要求。MySQL 表中lng字段存的是经度-180~180lat存的是纬度-90~90顺序颠倒会导致全国景区标记全部挤在非洲。3.2 Bootstrap 词云生成Jieba 分词 WordCloud 渲染中文避坑指南词云数据来自reviews表的content字段后端处理逻辑在routes.py# routes.py from jieba import cut from wordcloud import WordCloud import matplotlib.pyplot as plt from io import BytesIO import base64 app.route(/wordcloud) def generate_wordcloud(): # 1. 查询所有评论内容 reviews db.session.execute(text(SELECT content FROM reviews WHERE content IS NOT NULL)).fetchall() texts [r[0] for r in reviews] # 2. Jieba 精确模式分词 过滤停用词项目自带 stopwords.txt with open(static/stopwords.txt, r, encodingutf-8) as f: stopwords set([line.strip() for line in f]) words [] for text in texts: seg_list cut(text, cut_allFalse) # 关键必须用精确模式 words.extend([w for w in seg_list if w not in stopwords and len(w) 1]) # 3. 生成词云指定中文字体路径 wc WordCloud( font_pathstatic/fonts/SimHei.ttf, # ← 必须存在该字体文件 width800, height400, background_colorwhite, max_words100 ).generate( .join(words)) # 4. 转为 base64 嵌入 HTML img BytesIO() wc.to_image().save(img, formatPNG) img_b64 base64.b64encode(img.getvalue()).decode() return jsonify({image: fdata:image/png;base64,{img_b64}})提示SimHei.ttf字体文件必须放在static/fonts/目录下。Windows 系统可从C:\Windows\Fonts\simhei.ttf复制macOS 需下载「黑体-简」字体并重命名为SimHei.ttf。缺失字体将导致词云全为方框。3.3 Layer 弹窗交互点击景区 Marker 触发详情弹窗的完整链路Layui 的layer.open()与 Flask 接口联动实现点击地图标记查看实时数据// static/js/main.js 中 marker 点击事件 marker.addEventListener(click, function() { // 获取景区 ID绑定在 marker 的自定义属性上 var spotId this.spotId; // 调用 Flask 接口获取该景区详情 $.get(/api/spot/ spotId, function(res) { layer.open({ title: res.name 数据详情, content: pstrong评分/strong${res.score}/5.0/p pstrong订单量/strong${res.order_count} 单/p pstrong好评率/strong${res.positive_rate}%/p pstrong热门评论/strong${res.hot_comment}/p , area: [400px, 250px] }); }); });对应后端路由/api/spot/int:spot_id在routes.py中app.route(/api/spot/int:spot_id) def get_spot_detail(spot_id): # 使用原生 SQL 避免 ORM 性能损耗单次查询 sql SELECT s.name, s.score, COUNT(o.id) as order_count, ROUND(AVG(CASE WHEN r.sentiment 0 THEN 1 ELSE 0 END) * 100, 1) as positive_rate, (SELECT content FROM reviews WHERE spot_id %s ORDER BY sentiment DESC LIMIT 1) as hot_comment FROM scenic_spots s LEFT JOIN orders o ON s.id o.spot_id LEFT JOIN reviews r ON s.id r.spot_id WHERE s.id %s GROUP BY s.id result db.session.execute(text(sql), [spot_id, spot_id]).first() return jsonify({ name: result.name, score: float(result.score), order_count: result.order_count or 0, positive_rate: float(result.positive_rate or 0), hot_comment: result.hot_comment or 暂无评论 })4. 数据分析逻辑实现Pandas 聚合 Matplotlib 绘图 情感分析模型轻量化4.1 用户地域分布热力图Pandas 分组统计与 GeoJSON 匹配/api/user_province接口返回各省份用户数量用于渲染中国地图热力层app.route(/api/user_province) def user_province_stats(): # 使用 Pandas 进行高效分组比纯 SQL 更易扩展维度 df pd.read_sql( SELECT province, COUNT(*) as count FROM users WHERE province IS NOT NULL GROUP BY province , db.engine) # 关键省份名称标准化匹配 GeoJSON 中的 name 字段 province_map { 北京市: 北京, 上海市: 上海, 天津市: 天津, 重庆市: 重庆, 内蒙古自治区: 内蒙古, 广西壮族自治区: 广西, 西藏自治区: 西藏, 宁夏回族自治区: 宁夏, 新疆维吾尔自治区: 新疆, 香港特别行政区: 香港, 澳门特别行政区: 澳门, 台湾省: 台湾 } df[province] df[province].map(province_map).fillna(df[province]) # 转为字典列表供前端消费 return jsonify(df.to_dict(records))前端使用echarts的geo组件加载china.json项目已内置热力值绑定count字段。4.2 订单金额趋势图Matplotlib 时间序列绘图与 Flask 缓存优化趋势图需按月聚合订单金额但频繁查询影响性能采用functools.lru_cache缓存 1 小时from functools import lru_cache from datetime import datetime, timedelta lru_cache(maxsize128) def get_monthly_revenue(hours1): 缓存 1 小时内的月度营收数据 cutoff datetime.now() - timedelta(hourshours) df pd.read_sql( SELECT DATE_FORMAT(order_time, %Y-%m) as month, SUM(amount) as revenue FROM orders WHERE order_time %s GROUP BY DATE_FORMAT(order_time, %Y-%m) ORDER BY month , db.engine, params[cutoff]) return df.to_dict(list) app.route(/api/monthly_revenue) def monthly_revenue(): data get_monthly_revenue() # 生成 Matplotlib 图像不保存文件直接返回 PNG fig, ax plt.subplots(figsize(10, 4)) ax.plot(data[month], data[revenue], markero, linewidth2) ax.set_title(月度订单金额趋势, fontsize14) ax.grid(True, alpha0.3) img BytesIO() plt.savefig(img, formatpng, bbox_inchestight) plt.close(fig) img_b64 base64.b64encode(img.getvalue()).decode() return jsonify({image: fdata:image/png;base64,{img_b64}})注意plt.savefig()必须调用plt.close(fig)释放内存否则连续请求 100 次后内存泄漏导致 Flask 进程崩溃。4.3 评论情感分析TextBlob 简化版 vs 自训练模型选型对比项目采用轻量级TextBlob实现情感极性判断非深度学习方案平衡准确率与部署成本from textblob import TextBlob def analyze_sentiment(text): 中文评论情感分析TextBlob 中文支持有限需预处理 # 步骤1繁体转简体使用 opencc try: from opencc import OpenCC cc OpenCC(t2s) text cc.convert(text) except ImportError: pass # 步骤2TextBlob 分析对中文效果一般但胜在快 blob TextBlob(text) polarity blob.sentiment.polarity # -1 ~ 1 if polarity 0.2: return positive elif polarity -0.2: return negative else: return neutral # 批量分析示例用于初始化 reviews 表的 sentiment 字段 def batch_analyze_reviews(): reviews Review.query.filter(Review.sentiment None).limit(1000).all() for r in reviews: r.sentiment analyze_sentiment(r.content) db.session.commit()对比说明若需更高准确率可替换为SnowNLP专为中文优化或微调BERT-Base-Chinese。但TextBlob启动耗时 100ms而SnowNLP加载模型需 1.2sBERT需 3.5s —— 对实时弹窗场景响应延迟是首要约束。5. 生产就绪技巧静态资源压缩、SQL 查询优化、异常日志追踪5.1 前端资源合并压缩减少 HTTP 请求数至 12 个以内原始项目包含 7 个 CSS 文件bootstrap.min.css,layui.css,fontawesome.min.css,jquery-ui.css,jquery-ui.min.css,jquery-ui.theme.css,layer.css,templatemo-style.css实际可合并为 3 个# Linux/macOS 下合并压缩Windows 用 PowerShell 替代 cat static/css/bootstrap.min.css \ static/css/layui.css \ static/css/font-awesome.min.css \ static/css/all.css # 使用 cssnano 压缩需 npm install -g cssnano cssnano static/css/all.css -o static/css/all.min.css # 同理合并 JSjQuery Layui Layer 自定义脚本 cat static/js/jquery.min.js \ static/js/layui.js \ static/js/layer/layer.js \ static/js/main.js \ static/js/all.js terser static/js/all.js -o static/js/all.min.js --compress --mangle修改templates/base.html中的引用!-- 替换前 -- link relstylesheet href{{ url_for(static, filenamecss/bootstrap.min.css) }} link relstylesheet href{{ url_for(static, filenamecss/layui.css) }} !-- 替换后 -- link relstylesheet href{{ url_for(static, filenamecss/all.min.css) }} script src{{ url_for(static, filenamejs/all.min.js) }}/script5.2 SQL 查询性能诊断EXPLAIN 分析高频接口慢查询/api/spot/id接口在数据量 10 万时响应超 2s用EXPLAIN定位瓶颈EXPLAIN SELECT s.name, s.score, COUNT(o.id) as order_count, ROUND(AVG(CASE WHEN r.sentiment 0 THEN 1 ELSE 0 END) * 100, 1) as positive_rate, (SELECT content FROM reviews WHERE spot_id 1 ORDER BY sentiment DESC LIMIT 1) as hot_comment FROM scenic_spots s LEFT JOIN orders o ON s.id o.spot_id LEFT JOIN reviews r ON s.id r.spot_id WHERE s.id 1 GROUP BY s.id;优化方案为orders.spot_id和reviews.spot_id添加索引CREATE INDEX idx_orders_spot ON orders(spot_id); CREATE INDEX idx_reviews_spot ON reviews(spot_id);将子查询(SELECT ... FROM reviews ...)改为 JOINSELECT s.name, s.score, COUNT(o.id) as order_count, ROUND(AVG(CASE WHEN r.sentiment 0 THEN 1 ELSE 0 END) * 100, 1) as positive_rate, r_hot.content as hot_comment FROM scenic_spots s LEFT JOIN orders o ON s.id o.spot_id LEFT JOIN reviews r ON s.id r.spot_id LEFT JOIN ( SELECT spot_id, content FROM reviews WHERE (spot_id, sentiment) IN ( SELECT spot_id, MAX(sentiment) FROM reviews GROUP BY spot_id ) ) r_hot ON s.id r_hot.spot_id WHERE s.id 1 GROUP BY s.id, r_hot.content;5.3 异常日志捕获Flask 全局错误处理器与 Sentry 集成模板在app.py中添加全局异常捕获避免用户看到Internal Server Errorimport logging from logging.handlers import RotatingFileHandler # 配置日志每天轮转保留 7 天 if not app.debug: handler RotatingFileHandler(logs/tourism.log, maxBytes10*1024*1024, backupCount7) handler.setLevel(logging.ERROR) formatter logging.Formatter(%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]) handler.setFormatter(formatter) app.logger.addHandler(handler) app.errorhandler(500) def internal_error(error): app.logger.error(Server Error: %s, str(error)) return render_template(500.html), 500 app.errorhandler(Exception) def unhandled_exception(e): app.logger.error(Unhandled Exception: %s, str(e)) return render_template(500.html), 500若需远程错误追踪可集成 Sentry需安装sentry-sdkimport sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration sentry_sdk.init( dsnhttps://your-dsnsentry.io/your-project-id, integrations[FlaskIntegration()], traces_sample_rate0.2 # 采样 20% 请求性能数据 )提示Sentry 的traces_sample_rate设为0.2是平衡监控精度与带宽消耗的关键值。过高如 1.0会导致大量冗余数据上报过低如 0.01则难以定位偶发性问题。本文还有配套的精品资源点击获取
