ARTICLE DETAIL

资讯详情

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

Python爬虫+Flask+Vue+ECharts构建旅游数据可视化大屏全解析

Python爬虫+Flask+Vue+ECharts构建旅游数据可视化大屏全解析 前两天帮人调一套系统前前后后折腾了快一下午越调越觉得这题选得挺刁——Python 爬虫、Flask 接口、Vue 大屏、ECharts 图表再叠一个百度地图正好把一整套数据链路完整串了起来。如果你手头也有这类“旅游数据采集与可视化大屏”的毕业设计源码或者正想做一个类似的个人项目那这篇文章值得从头看完。这套系统做出来以后能干什么说白了就是用爬虫把各旅游平台的景点、热度、评分、评论数据抓下来清洗后存进数据库Flask 后台负责把库里数据变成接口Vue 前端拿到接口数据后用 ECharts 和百度地图渲染出一个能展示的大屏。适合谁参考准备交毕设的学生、刚学完 Python 想找综合练习的开发者或者说只要是能把 Flask 和 Vue 连起来完整跑一遍的读者照着做都划算。1. 打地基之前先把架构和选型聊透1.1 这题的真正得分点在哪里同一个题目有人做成“数据表格 柱状图”有人做成“可视化指挥大屏”差距大多不在代码量而在系统意识。毕业设计答辩时老师爱问的其实就几件事数据哪里来、怎么存、怎么给前端、前端怎么展示、系统挂了怎么办。旅游数据采集与可视化大屏这个题目天然把这几个问题全占了所以它最大的价值不是“爬虫写得多炫”而是链路完整。我也见过一些同学上来就埋头写爬虫抓了几万条数据结果卡在接口设计上也有人前端写得特别华丽结果后端只有一份写死的 JSON。这两种都容易在演示环节翻车。做这种系统第一步不是先写代码而是先画一张数据流图把每一层的职责定清楚。1.2 技术栈为什么这么组先说说选型逻辑。后端用 Flask 而不是 Django主要原因是这套系统不需要后台管理的完整权限体系也不需要 Django 自带的 admin 和 ORM 全家桶。Flask 轻路由写起来直观和 SQLAlchemy 搭配起来也不绕对一篇毕业设计来说正好卡在“够用但不臃肿”的位置。前端选 Vue 是因为它的学习曲线相对平缓而且 ECharts 的初始化逻辑和 Vue 的生命周期天然好配合。至于百度地图它的 JS API 在国内访问稳定申请 AK 也不麻烦在地图上做散点、热力、标记点都很顺手。具体分工是这样的技术负责什么为什么是它Python数据采集、清洗、入库生态成熟爬虫相关库丰富Flask提供 REST 接口轻量、路由清晰、适合做演示级后端Vue大屏页面组件化组件化开发效率高数据绑定方便ECharts图表渲染图表类型多地图和业务图表都能做BaiduMap地理信息展示国内常用API 文档友好可视化效果好1.3 从爬虫到大屏的数据流整套系统我建议按“三层四段”理解。三层是数据层、服务层、展示层四段是“采集 → 存储 → 接口 → 大屏”。数据层用的是 MySQL 或 SQLite演示环境用 SQLite 更方便但真跑数万条数据建议还是 MySQL。服务层就是 Flask 应用它负责读数据库、做统计聚合、把结果封装成 JSON。展示层是 Vue 单页应用里面再来划分组件头部指标卡、地图组件、图表组件、轮播表格组件。这条链路上最容易出问题的有两个节点第一是爬虫抓下来数据格式千奇百怪入库前必须清洗第二是 Flask 返回的数据结构和前端 ECharts 期望的数据结构经常不一致前端拿到的data需要转成series数组。后面我会专门展开这两块。2. 数据采集部分爬虫不是闷头抓就完事2.1 数据来源选择与最基本的边界意识做旅游数据大家首先想到的是去主流旅游平台抓景点、评分、销量。思路没问题但我要先泼一盆冷水不是所有页面都能随便抓。先看robots.txt再评估目标网站接口的调用频率别为了毕设把自己 ip 封了更别把平台打挂。正规一点的做法是优先找公开数据集、政府旅游统计网站、或者平台开放的 API。如果确实要爬网页我建议控制抓取频率、设置较长随机延时并且只用在小规模演示数据上。我在源码整理时一般会把数据来源分成三个级别真实采集数据、官方开放数据、模拟数据。系统里同时保留三种入口答辩的时候可以说“真实数据用于离线验证在线演示为了稳定性可以使用预采集后的缓存数据”这样既真实又稳妥。2.2 页面抓取与解析的常见写法这里给一个非常典型的爬虫骨架用requests抓页面用BeautifulSoup做解析。以抓取一个景点列表页为例大致思路是这样import requests from bs4 import BeautifulSoup import random import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_html(url, retry3): for i in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) if resp.status_code 200: resp.encoding resp.apparent_encoding return resp.text except Exception as e: print(f第{i1}次请求失败:, e) time.sleep(random.uniform(2, 5)) return None def parse_attractions(html): soup BeautifulSoup(html, html.parser) items [] for node in soup.select(.attraction-item): name node.select_one(.name) score node.select_one(.score) comment node.select_one(.comment) items.append({ name: name.text.strip() if name else , score: float(score.text.strip()) if score else 0, comment_count: int(comment.text.strip()) if comment else 0 }) return items这就是个通用模板不同平台的选择器不同但你只要按这个结构去改select部分就行。注意两点一个页面里可能有几百个元素但真正要的只有字段别把无关节点也带进来第二是部分平台数据是通过 JavaScript 动态加载的requests抓不到这时候有两种办法用selenium无头浏览器渲染页面或者直接找页面里埋的接口地址。毕业设计阶段优先找接口地址因为接口返回的 JSON 比 HTML 好解析得多。2.3 清洗、去重与入库抓回来的原始数据不能直接入库。比如价格字段可能是“120元起”人数可能是“1.2万”评分可能带“分”这些都是要处理的脏数据。我的习惯是先写一个clean.py把清洗逻辑集中起来import re def clean_price(text): if not text: return 0 return float(re.search(r\d\.?\d*, text).group()) def clean_count(text): if not text: return 0 if 万 in text: return int(float(re.search(r\d\.?\d*, text).group()) * 10000) return int(re.search(r\d, text).group())去重也很有必要。同一个景点不同页面可能抓了两次去重的核心字段是“景点名 城市名”。入库时用数据库唯一索引兜底这样就算爬虫重复执行也不会产生大量重复数据。数据库模型建议参考下面这种结构from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() class Attraction(db.Model): id db.Column(db.Integer, primary_keyTrue) city db.Column(db.String(50), indexTrue) name db.Column(db.String(100)) score db.Column(db.Float) comment_count db.Column(db.Integer) level db.Column(db.String(20)) lat db.Column(db.Float) lng db.Column(db.Float) __table_args__ (db.UniqueConstraint(city, name, nameuniq_city_name),)2.4 反爬应对与抓取节奏写爬虫不要头铁。我见过同学 1 秒发 10 个请求不到半分钟就被封。合理节奏应该是随机延时 2 到 5 秒单日请求数控制在低频范围。如果遇到验证码不硬刚直接把当前任务暂停等页面恢复再抓。抓取过程要写日志每抓一个页面记录状态码和耗时封了也好排查。另外一个容易被忽略的点是编码问题。很多旅游站点的页面是 gbk 编码直接resp.text可能乱码。用resp.encoding resp.apparent_encoding或者根据页面 meta 指定编码能避免后期数据一堆乱码。3. Flask 后端把数据库变成前端能直接用的 JSON3.1 Flask 应用结构与数据库接入后端部分不用写得过度复杂一个app.py加上几个蓝图就够了。但也不要一个文件写上千行建议拆成这样backend/ ├── app.py # 应用入口注册蓝图 ├── config.py # 数据库配置、密钥 ├── models.py # 数据库模型 ├── apis/ │ ├── __init__.py │ ├── overview.py # 顶部指标 API │ ├── map_api.py # 地图分布 API │ └── charts.py # 图表数据 API └── utils/ └── resp.py # 统一响应封装app.py的核心内容大概长这样from flask import Flask from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy def create_app(): app Flask(__name__) app.config.from_object(config.Config) db.init_app(app) CORS(app) return appCORS 必须开因为 Vue 开发服务器跑在 5173 或 8080Flask 跑在 5000跨域不打穿前端根本拿不到数据。3.2 给大屏定制几个关键 API可视化大屏需要的接口不是简单的列表查询而是“聚合统计”。比如顶部指标卡片需要总景点数、总收藏量、平均评分地图组件需要各个城市的热门景点数图表组件需要热门景点 top10 和评分分布。这种接口在 Flask 里可以直接用 SQLAlchemy 做聚合用不着写复杂 SQL。以一个地图接口为例from flask import jsonify from models import Attraction, db def map_data(): rows db.session.query( Attraction.city, db.func.count(Attraction.id).label(total) ).group_by(Attraction.city).all() return jsonify({ code: 200, data: [ {city: r.city, value: r.total} for r in rows ] })前端 ECharts 地图组件需要的正是这种[{city: 北京, value: 20}, ...]格式。你只要保证所有接口都返回{code: 0, data: ...}这种统一结构前端一个拦截器就能处理所有异常不用写一堆重复判断。3.3 接口联调时最容易被卡住的点联调阶段最常见的报错是 404 和跨域。404 大概率是 Vue 请求的后端端口和 Flask 启动端口不一致检查一下VITE_PROXY或 axios baseURL 就好。跨域报错通常是因为没启用 Flask-CORS或者前端没有走代理而直接访问了 5000 端口。还有一个容易被忽略的性能点。如果爬虫积累了 5 万条数据接口里query.all()会把全表数据一次性取出来转成 JSON 会卡。演示环境数据量不大无所谓但如果你真想优化可以加分页或只返回 top100大屏展示用“有限样本”完全够。3.4 后端启动与服务化开发阶段用python app.py跑内建服务器没问题。如果是答辩演示建议用生产级服务器最简单的方案是waitress或gunicorn一个命令起服务避免 Flask 自带服务器在长时间轮询下出幺蛾子。部署时记得debugFalse并把数据库文件路径改成绝对路径不然换台机器运行分分钟找不到库。4. Vue 大屏ECharts 图表与百度地图的配合4.1 大屏布局与缩放方案大屏页面第一重要的是“不溢出”。设计稿一般是 1920×1080但答辩用的笔记本很可能只有 1366×768如果直接把大屏写死 1920 宽下面全是滚动条观感很差。我的做法是根容器固定 1920×1080然后用transform: scale做整体等比缩放。用一个计算函数取window.innerWidth / 1920和window.innerHeight / 1080中的较小值template div classscreen div classscreen-wrapper :stylewrapperStyle !-- 大屏内容 -- /div /div /template script setup import { computed, onMounted, reactive } from vue; const scale reactive({ value: 1 }); function updateScale() { scale.value Math.min( window.innerWidth / 1920, window.innerHeight / 1080 ); } const wrapperStyle computed(() ({ transform: scale(${scale.value}), transformOrigin: left top, width: 1920px, height: 1080px })); onMounted(() { updateScale(); window.addEventListener(resize, updateScale); }); /script4.2 ECharts 组件的封装与数据绑定前端项目里不要每个图表组件都重新写一遍初始化逻辑把 ECharts 封装成一个通用组件会更省事。封装的思路是父组件传入chartOption子组件负责init、setOption和resize。template div refchartRef stylewidth:100%; height:100%;/div /template script setup import { onMounted, onBeforeUnmount, ref, watch, nextTick } from vue; import * as echarts from echarts; const props defineProps({ option: { type: Object, required: true } }); const chartRef ref(null); let chart null; async function render() { await nextTick(); if (chart) { chart.setOption(props.option, true); } } onMounted(() { chart echarts.init(chartRef.value); render(); window.addEventListener(resize, () chart chart.resize()); }); watch(() props.option, render, { deep: true }); onBeforeUnmount(() { window.removeEventListener(resize, chart chart.resize); chart chart.dispose(); }); /script特别注意chart.dispose()。页面里图表多组件销毁时如果不释放实例切页面再回来可能出现屏幕越用越卡、canvas 数量爆炸的情况这在答辩现场非常致命。4.3 地图与散点的组合实现ECharts 的地图组件和百度地图是两种常见展示方案。ECharts 地图偏“数据分布”百度地图偏“地理位置”。ECharts 地图需要先注册地图数据。现在的 ECharts 已经不带默认中国地图你需要单独引入china.json或者china.jsimport * as echarts from echarts; import chinaJson from /assets/china.json; echarts.registerMap(china, chinaJson); const mapOption { tooltip: {}, visualMap: { min: 0, max: 1000, left: 20, bottom: 20, inRange: { color: [#e0f3f8, #74add1, #4575b4] } }, geo: { map: china, roam: true, itemStyle: { areaColor: #1e3a5f } } };百度地图则更适合景点标记和线路展示。你可以直接在 Vue 组件里引入百度地图的 JS API然后用BMap.Marker把景点坐标打在地图上// 初始化地图 const map new BMap.Map(mapContainer); map.centerAndZoom(北京, 10); map.enableScrollWheelZoom(); // 添加标记点 const point new BMap.Point(116.404, 39.915); const marker new BMap.Marker(point); map.addOverlay(marker); const infoWindow new BMap.InfoWindow(p故宫/p, { width: 150, height: 60 }); marker.addEventListener(click, () map.openInfoWindow(infoWindow, point));申请百度地图 AK 时选“浏览器端”域名填localhost和127.0.0.1都要勾上不然开发环境能开、演示时换个域名就白屏这是踩过最多的坑。4.4 饼图中间文字和柱状图渐变的配置技巧经常有同学问“ECharts 饼图中间的字怎么加”。很多人以为要写两个 series其实直接用一个title和三层graphic就能实现。最省事的方式是在饼图组件里配一个居中的 titleoption { title: { text: 5A景区占比, subtext: 总计 126 个, left: center, top: center }, series: [{ type: pie, radius: [45%, 70%], center: [50%, 50%], data: [ { name: 5A, value: 126 }, { name: 4A, value: 348 }, { name: 其他, value: 92 } ] }] };另一种做法是设置label的position: center但那个是显示每个扇区的文字不是总提示两者别混了。柱状图设置渐变色也是高频需求。折线图、柱状图默认都是纯色略显单调改成线性渐变会高级很多itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #83bff6 }, { offset: 1, color: #2f6fdb } ]) }gradient 从上到下offset:0是顶部颜色offset:1是底部颜色。渐变不止可以用于柱状图仪表盘、面积折线都能用。折线图的 x 轴刻度如果太密集把axisLabel.interval设为auto或者改成每两个刻度显示一个、旋转 30 度都比默认结果舒适。5. 大屏交互与细节打磨5.1 视觉组件的层次规划大屏不是一个图表堆砌的页面它需要信息层级。我通常把页面分成四层顶部是标题和日期时间中间左侧放趋势折线图和城市 top 榜中间区域留给地图右侧放景区分类饼图和热门评论。顶部三个 KPI 卡可以单列一行让评审一眼看到最重要的指标。背景处理很关键。用深蓝色渐变背景配半透明卡片加上一点点细边框整体就会很有“指挥中心”的感觉。不要整页花花绿绿一个主题色系就够了。字体上标题用稍大字号数据文字用等宽字体数字跳动会更统一。5.2 大屏数据自动刷新与加载兜底大屏演示时手动刷新页面是件很尴尬的事。可以在前端做一个定时轮询比如每 60 秒重新请求一次接口const refreshTimer setInterval(() { loadOverviewData(); loadMapData(); loadChartData(); }, 60000);但轮询期间接口挂了怎么办最稳妥的做法是前端保存一份mockData.js接口请求失败或者超时的时候直接启用 mock 数据。这样就算答辩现场断网大屏也不会白屏。为保留责任在控制台打印一句“当前使用模拟数据”就行演示时真的出现明显卡顿或空白才有退路。5.3 把大模型聊成加分项标题里既然带了“大模型”那就不妨把这个模块做成人工智能旅游建议。不需要把模型做多复杂在 Flask 端新增一个接口把前端的城市和景点列表拼进提示词调用大模型 API生成一段短推荐语返回给前端展示。def ai_advice(city, attraction_names): prompt f请用200字以内为去{CITY}旅游的用户推荐景点和行程安排重点围绕以下景点{,.join(attraction_names)} # 调用大模型接口返回生成文本 return response_text这套方案的亮点是“系统不止展示数据还能基于数据产生建议”答辩和评优都是直接可用的加分项。唯一提醒调大模型接口需要密钥而且演示现场不一定有网所以一定要做接口超时兜底超时了给一段本地写好的推荐文案。6. 常见问题与避坑经验6.1 高频问题速查表整理了一张我在调试中遇到最多问题对应的排查方向按优先级排好照着排查能省不少时间现象大概率原因解决办法前端请求接口报 404后端端口与前端 baseURL 不一致统一端口检查 proxy 配置接口报跨域错误后端没开启 CORS安装 flask-cors 并注册百度地图白屏AK 未生效或域名未配置检查引用的 AK配置 localhostECharts 图表渲染空白容器高度为 0给外层设置固定 height图表数据不更新watch 不是深监听给 option 加{ deep: true }爬虫抓不到动态内容数据是 JS 异步加载抓取 XHR 接口或使用 Selenium页面在笔记本上看不全分辨率不匹配用 transform scale 等比缩放Flask 无法连接数据库SQLite 相对路径变化使用绝对路径大屏切路由后越来越卡ECharts 实例未销毁在 beforeUnmount 中 dispose数据量几万条接口卡顿全量返回限制返回条数top100 足够6.2 答辩现场最容易翻车的三个细节第一千万记得准备本地离线演示兜底方案。答辩现场网络质量不可控爬虫绝不要现场跑把预采集好的数据存库演示时只展示页面。第二启动顺序要用脚本固化比如写一个start_all.bat依次启动 MySQL、Flask、Vue 服务避免现场手忙脚乱。第三提前测试“刷新大屏”场景刷新后几秒内页面必须恢复完整不然老师让你重新打开一次个别组件就处于加载状态回不来了。6.3 把系统从 60 分做到 90 分的微创新如果时间允许有几个低成本但高感知度的改进方向。一是热词词云把景点评论里的高频词做成词云组件放地图下方技术难度不大但视觉效果明显。二是行程推荐弹窗点击地图标记点时弹出该城市的推荐路线用一个侧边抽屉展示。三是数据大屏边角的“滚动资讯条”轮播各城市的热门景区与当前天气给人一种实时数据流的感觉。6.4 个人项目里的模块复用经验这套系统做完以后代码不要丢。Flask 的统一响应封装、Vue 的 ECharts 通用组件、大屏缩放逻辑、百度地图 API 封装这些模块改改就能复用到别的毕业设计或项目里。我这两年做过几套类似系统最后都会沉淀一批常用组件下次再做只改业务接口。这也是做这种综合项目最值钱的地方——不是一次性的毕业设计而是一套属于自己的工具库。最后说句实在的这类项目看着技术点多其实每一块的难度都不高真正考验人的是“把这些技术拧成一股绳”的过程。我调完那晚自己又跑了一遍全流程从爬虫抓一条数据到最后在大屏上看到对应的柱状图跳起来那种把整条链路走通的感觉远比交一篇论文来得踏实。如果你也正卡在某一步别急着调眼花缭乱的美化先把一条数据从 MySQL 安安稳稳走到 ECharts 尾端后面所有问题都会顺手很多。
返回列表