资讯详情

资讯详情

基于Python与ECharts的黑龙江旅游景点数据分析系统设计与实现

1. 这套系统到底在解决什么问题做毕设选课题最怕的就是“大而空”。去年我带过的几个学弟学妹一开始都想着做电商用户画像、金融风控预测这类题目结果数据拿不到、模型跑不动、页面做出来丑得不敢给导师看最后三五天通宵改方案。后来我给他们推荐了一个思路选一个数据源相对干净、领域足够聚焦、可视化容易出效果的方向——旅游景点数据分析。黑龙江旅游景点数据分析系统就是在这个背景下打磨出来的一个完整毕设方案。它的核心逻辑不复杂把黑龙江各地市的旅游景点、门票价格、评分、热度、地理位置这些数据抓下来用Python做清洗和统计再用数据可视化技术做成一个能看、能查、能下结论的分析平台。听起来好像没什么高深的但这类题目的妙处在于它把数据采集、数据清洗、数据存储、后端接口、前端可视化一整条链路全部串起来了而且每个环节都有可以展开讲的技术点论文有得写答辩有得问演示有得看。为什么选黑龙江原因很实在。第一黑龙江的旅游资源集中度高哈尔滨、牡丹江、大兴安岭这几个地市基本覆盖了大部分知名景点数据量适中不会多到跑不动也不会少到没东西可分析第二冰雪旅游、边境风情、森林生态这几个主题特色鲜明可视化的时候主题色、图表风格都好定出图效果天然好看第三和热门旅游省份相比针对黑龙江旅游数据分析的现成案例相对少反而容易做出新意。这套系统适合谁来参考如果你正在纠结毕设选题动手能力中等偏上会用Python基础语法看过一些前端页面但没系统学过前后端分离开发那这个题目是很合适的难度区间。它不要求你发明新的算法也不要求你训练深度学习模型核心是把一套常规的数据分析流程做得完整、规范、漂亮。如果做得好完全可以包装成“数据采集与可视化分析平台”去投一些大数据相关的比赛也算是一鱼两吃。2. 技术选型凭什么用Python ECharts这套组合2.1 后端为什么选Python系技术选型这部分很多同学容易陷入一个误区想用Spring Boot因为Java“看起来正规”想用MySQL因为数据库课教的就是这个。但毕设项目的核心目的是什么是让你在有限时间内把完整流程跑通并且能讲清楚每个环节的原理。从这个目标出发Python生态的优势非常明显。数据处理这层用Pandas做DataFrame操作groupby、merge、apply这些方法写起来几乎是口语化的表达比如按城市分组统计平均评分一行代码就能算完。如果换Java写光是一个分组聚合就要写十几行Stream代码调试起来也费劲。再说爬虫部分Requests BeautifulSoup的组合写一个景点信息采集脚本核心代码不超过100行而用Java的HttpClient Jsoup虽然也能做但Cookie处理、编码转换这些坑会耗费大量时间。我的建议是后端直接用Flask不用Django。原因很简单Flask轻路由写起来直观新手看代码很容易理解“前端请求哪个地址后端返回什么数据”这个对应关系。Django自带Admin后台、ORM、迁移工具功能全但学习曲线陡而且很多功能在毕设里根本用不上反而增加理解负担。Flask SQLAlchemy的组合既能体现分层设计的思想又不会因为框架太重让人迷失在细节里。2.2 可视化为什么选ECharts而不是Tableau或PowerBI数据可视化这个环节市面上工具很多但适合毕设场景的其实不多。Tableau和PowerBI确实是企业级分析利器拖拽几下就能出图但问题是它们生成的dashboard是封闭的你没法把这个图表嵌到自己的Web系统里也没法秀前端代码能力。而且这类商业软件在论文里只能算“工具应用”难以体现“系统实现”的技术含量。ECharts就是另一个路子了。它本质是一个JavaScript图表库通过配置项控制图表的类型、数据、样式输出的是标准的HTML JS代码可以和Flask后端无缝对接。关键是它上手极快官方文档的示例代码几乎可以直接复制改数据从柱状图、折线图到地图、雷达图覆盖范围非常广。这里我要特别说一句黑龙江旅游景点分析这个主题用ECharts的地图组件来做是有天然优势的。ECharts的map类型支持中国省级地图你只需要准备黑龙江各城市的地理坐标数据就能把景点分布、热度排名直接画到地图上视觉冲击力远强于普通的柱状图。而且这套系统的用户端、管理端、大屏展示端都可以用ECharts来实现保持技术栈统一答辩时讲起来也更有整体感。2.3 数据库怎么选数据库我推荐MySQL或者更轻量的SQLite也可以。MySQL是通用方案安装方便网上教程多出问题好搜SQLite更适合做原型验证一个文件搞定不用装服务但缺点是并发能力弱、可视化管理工具不如MySQL方便而且答辩时老师问“为什么不用MySQL”答不上来会尴尬。实际开发中我建议用MySQL但表结构不要设计得太复杂。景点表、城市表、评论表、用户表四张表足够了。景点表放景点名称、所属城市、等级、门票价格、评分、热度、简介、经纬度城市表放城市名称、所属地区、简介评论表放用户对景点的评价内容、评分、时间用户表就是后台登录用的账号密码。表与表之间用外键关联既能体现数据库设计能力又不会把自己绕晕。3. 系统功能拆解从数据采集到可视化展示的完整链路3.1 整体模块划分这套系统的功能模块我把它分成五个部分每个部分对应一个独立的技术点数据采集模块爬取黑龙江各旅游平台公开的景点信息包括名称、评分、评论数、门票价格、所在城市、景点类型等字段。数据清洗与存储模块对抓取到的数据进行去重、格式统一、缺失值处理然后写入MySQL数据库。后端接口模块Flask提供RESTful API前端通过AJAX请求获取JSON格式的数据。可视化展示模块包括数据概览大屏、景点分布地图、评分与热度分析图表、城市对比分析图。后台管理模块管理员登录、景点信息的增删改查、用户评论管理。这个结构的好处是每一块都可以在论文里单独成章写起来条理清晰。如果你时间紧张甚至可以砍掉后台管理模块这样系统依然能跑只是少了一个加分项。3.2 数据采集爬虫代码的实战写法爬虫是整个项目的第一环也是最容易翻车的一环。很多同学一上来就想爬携程、去哪儿这种大平台结果被反爬策略卡得死去活来。我的建议是分两步走第一步先用一个小众旅游资讯网站练手把整个流程跑通第二步再针对目标平台做精细化处理。这里给出一段核心的爬虫代码示例以爬取某旅游平台的景点列表页为例import requests from bs4 import BeautifulSoup import time import random headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, } def fetch_scenic_spots(city_code, page_num): 抓取指定城市、指定页码的景点列表 city_code: 城市代码 page_num: 页码 url fhttps://example-travel.com/scenic?city{city_code}page{page_num} try: resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 if resp.status_code 200: soup BeautifulSoup(resp.text, html.parser) items soup.select(.scenic-item) spot_list [] for item in items: name item.select_one(.name).text.strip() score item.select_one(.score).text.strip() comment_num item.select_one(.comment-num).text.strip() price item.select_one(.price).text.strip() spot_type item.select_one(.type).text.strip() spot_list.append({ name: name, score: float(score), comment_num: int(comment_num), price: float(price), type: spot_type, city: city_code, }) return spot_list else: print(f请求失败状态码: {resp.status_code}) return [] except Exception as e: print(f抓取异常: {e}) return [] # 主循环遍历城市和页码 city_codes [haerbin, mudanjiang, daqing, qiqihaer] for city in city_codes: for page in range(1, 6): data fetch_scenic_spots(city, page) if data: save_to_db(data) # 延迟1-2秒防止请求过快被封 time.sleep(random.uniform(1, 2))这段代码有几个关键细节值得注意。第一time.sleep(random.uniform(1, 2))这个延迟非常重要不加这个请求频率太快很容易被服务器封IP第二resp.encoding utf-8要主动设置否则中文可能乱码第三解析CSS选择器时一定要先看网页的实际HTML结构不同平台的class命名完全不同不要直接套用其他人的选择器。3.3 数据清洗真实数据永远没你想象的干净爬下来的数据看着能用但一跑统计准出问题。比如“评分”字段有的景点显示“4.5分”有的显示“暂无评分”直接float()转换就会报错再比如“门票价格”有的景点免费价格字段是“免费”两个字不处理就没法做数值运算。这里我分享一下清洗的常见思路。首先是空值处理对于评分、评论数这种核心字段缺失的样本直接删除影响不大对于简介、类型这种描述性字段缺失的填“暂无信息”。其次是类型统一价格字段把“免费”转为0把“起”字去掉把“元”字去掉全部转成浮点数。最后是去重同一个景点可能在多个页面重复出现以景点名称城市作为联合键去重。import pandas as pd def clean_data(df): # 1. 删除完全重复的记录 df df.drop_duplicates(subset[name, city]) # 2. 价格清洗将免费转换为0XX元起提取数字 def parse_price(value): if value in [免费, 暂无, ]: return 0.0 value str(value).replace(元起, ).replace(元, ).strip() try: return float(value) except ValueError: return 0.0 df[price] df[price].apply(parse_price) # 3. 评分清洗转换失败或缺失的填充平均值 df[score] pd.to_numeric(df[score], errorscoerce) df[score] df[score].fillna(df[score].mean()) # 4. 评论数字段转int负数归零 df[comment_num] pd.to_numeric(df[comment_num], errorscoerce).fillna(0).astype(int) df.loc[df[comment_num] 0, comment_num] 0 return df清洗后的数据已经可以入库了。入库前在Python里做一次数据质量检查比如查看缺失值比例、检查字段类型打印几行看看效果确认没问题再写MySQL。3.4 核心分析逻辑你能得出什么有价值的结论数据分析系统不是把图表堆上去就完事了关键是能回答“然后呢”这个问题。针对黑龙江旅游数据有几类分析是必做的也是论文里的核心论点一是景点热度与旅游城市分布分析。通过统计各城市景点数量、平均评分、总评论数可以得出哪些城市的旅游吸引力最强哪些城市是“冷门宝藏”。比如哈尔滨景点数量多、评论量大但平均评分可能不是最高的大兴安岭景点少但单个景点评分可能很高这就能引出“开发程度与游客满意度”的讨论。二是门票价格与热度关系分析。把景点按价格区间划分免费、0-50元、50-100元、100元以上分别计算平均评论数和平均评分观察是否存在“收费越高、预期越高、评分越容易受影响”的规律。这里可以用箱线图展示不同价格区间的评分分布比简单的折线图更有说服力。三是季节性分析。如果爬到的数据里有评论时间字段可以按月统计评论数量画一张季节性热力图直观展示黑龙江旅游的淡旺季。冬季的冰雪旅游和夏季的避暑旅游会是两个明显的高峰这和黑龙江的气候特征高度相关作为论文的背景分析非常有说服力。四是景点类型偏好分析。把景点分成自然风光、历史文化、主题游乐、城市公园等类型统计不同类型景点的平均评分和热度用雷达图展示各城市不同类型景点的分布特征。这些分析不需要复杂的算法Pandas的分组聚合加ECharts的基础图表就能搞定。但它们在论文里构成了完整的“数据分析与结果讨论”章节比单纯堆截图强太多了。4. 可视化大屏与前端实现细节4.1 大屏页面怎么布局可视化大屏是整个系统的门面也是导师第一眼看到的东西。我的建议是采用经典的“总-分-总”布局顶部是标题和核心KPI卡片景点总数、城市数量、平均评分、总评论数中间主体部分左边放景点分布地图右边放热门景点TOP10榜单底部放价格区间分布图和各城市景点数量对比图。布局原理其实很简单人的视线是先看中间、再看两边先看整体、再看局部。所以地图这种“展示整体分布”的图放中间榜单和对比图放两侧和底部数据之间要有呼应关系。比如点击地图上的某个城市底部的柱状图同步筛选显示该城市的景点数据这种联动效果做出来答辩时老师一定会感兴趣。ECharts的联动可以通过事件监听实现。核心代码逻辑是在地图的click事件里更新其他图表的datamyChart.on(click, function(params) { if (params.name) { // 根据点击的城市名筛选其他图表的数据 updateBarChart(params.name); updatePieChart(params.name); updateTopList(params.name); } });4.2 地图组件的配置要点ECharts绘制黑龙江地图首先要准备一份黑龙江各城市的地理坐标数据。代码里用的是registerMap核心配置是geo和series的map类型// 注册地图数据城市坐标数据可以存为JSON文件 $.getJSON(../static/js/heilongjiang.json, function(geoJson) { echarts.registerMap(heilongjiang, geoJson); var chart echarts.init(document.getElementById(mapChart)); chart.setOption({ tooltip: { trigger: item, formatter: function(params) { return params.name br/景点数量: params.value br/平均评分: params.data.score; } }, visualMap: { min: 0, max: 60, left: left, top: bottom, text: [高, 低], inRange: { color: [#e0f3f8, #abd9e9, #74add1, #4575b4, #313695] } }, series: [{ name: 景点数量, type: map, map: heilongjiang, roam: true, label: { show: true, fontSize: 10 }, data: mapData }] }); });这里要提醒几个细节。第一visualMap的渐变色要从浅到深不能反过来否则地图上的颜色传递的信息是反的答辩时会被问住第二roam参数设置为true允许缩放和平移演示的时候可以放大某个城市看细节体验好很多第三tooltip的formatter要返回完整信息包括景点数量、平均评分不能只显示一个数值。4.3 Flask后端接口怎么写前端页面不能写死数据必须从后端接口动态获取。这里用Flask写几个RESTful API核心是返回JSON格式的数据。from flask import Flask, jsonify, request from flask_sqlalchemy import SQLAlchemy app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:passwordlocalhost/travel_db db SQLAlchemy(app) # 景点模型 class ScenicSpot(db.Model): __tablename__ scenic_spot id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(100)) city db.Column(db.String(50)) score db.Column(db.Float) comment_num db.Column(db.Integer) price db.Column(db.Float) spot_type db.Column(db.String(50)) # 接口1获取所有城市景点统计数据 app.route(/api/city_summary) def city_summary(): rows db.session.query( ScenicSpot.city, db.func.count(ScenicSpot.id).label(spot_count), db.func.avg(ScenicSpot.score).label(avg_score), db.func.sum(ScenicSpot.comment_num).label(total_comments) ).group_by(ScenicSpot.city).all() data [ { city: r.city, spot_count: r.spot_count, avg_score: round(r.avg_score, 2) if r.avg_score else 0, total_comments: r.total_comments } for r in rows ] return jsonify({code: 200, data: data}) # 接口2获取热门景点TOP10 app.route(/api/hot_top10) def hot_top10(): rows ScenicSpot.query.order_by(ScenicSpot.comment_num.desc()).limit(10).all() data [ { name: r.name, city: r.city, score: r.score, comment_num: r.comment_num, price: r.price } for r in rows ] return jsonify({code: 200, data: data}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)接口写得越规范前端调起来越省事。统一返回{code: 200, data: ...}这种结构前端只需要判断code再取data省去了一堆异常分支的麻烦。另外要注意Flask的debugTrue在开发时非常好用改完代码自动重启但部署时一定要关掉否则有安全风险。前端用AJAX请求这些接口拿到数据后setOption渲染图表。这里典型的一个问题是跨域。如果前端页面和Flask跑在不同的端口浏览器会拦截跨域请求。解决办法有两种一种是前端页面直接用Flask的render_template渲染让页面和后端同源另一种是给Flask装上flask-cors。毕设场景推荐第一种省事而且逻辑清晰。5. 系统部署与运行指南5.1 本地开发环境搭建整个系统需要用到Python、MySQL、Node可选用于装前端依赖。Python建议直接用Anaconda装自带Pandas、Flask常见库省去pip安装的麻烦。Step 1创建虚拟环境并激活conda create -n travel_analysis python3.9 conda activate travel_analysis pip install flask flask-sqlalchemy pymysql requests beautifulsoup4 pandasStep 2创建MySQL数据库和表结构。可以在MySQL命令行直接执行SQL语句也可以写一个Python脚本用SQLAlchemy建表。推荐后者因为建表逻辑和模型定义保持一致不容易出错python init_db.pyStep 3运行爬虫脚本采集数据python crawler.pyStep 4启动Flask应用python app.py打开浏览器访问http://127.0.0.1:5000如果一切正常你应该能看到可视化大屏页面地图上分布着黑龙江各地的景点数据。整个流程大概需要一天时间来跑通其中一半时间会花在调爬虫上这是正常的。5.2 静态资源路径问题使用Flask的render_template时静态文件要放在static目录下模板文件放在templates目录下。HTML里引用静态资源时使用url_for生成正确的URL路径script src{{ url_for(static, filenamejs/echarts.min.js) }}/script script src{{ url_for(static, filenamejs/map.js) }}/script很多同学习惯直接写相对路径比如static/js/echarts.min.js这在Flask的模板里是行不通的。用url_for生成路径代码更规范也能避免部署到服务器后路径失效的问题。5.3 打包演示环境答辩现场最怕的就是网不好、服务起不来。我的建议是准备一套离线演示方案。把所有依赖库导出到requirements.txt写好一键启动脚本最好再准备一份录制好的演示视频作为备份。如果现场有投影仪直接把笔记本画面投上去就行。演示时候的流程我建议按这个顺序走先展示完整大屏让老师有个整体印象然后点击地图上的两三个城市展示交互联动效果再展示爬取数据的代码说明数据来源的合法性最后展示数据库中的原始记录让老师相信数据是真实抓取的而不是捏造的。6. 常见问题与避坑指南6.1 爬虫被封IP怎么办这是一个高频问题几乎每个做爬虫的人都会遇到。症状就是一开始能正常抓数据过一会儿服务器返回403 Forbidden或者要求输入验证码。解决的思路有几个层次。第一层是降低访问频率把延迟从1秒调到3秒甚至5秒虽然慢了但稳定第二层是更换User-Agent准备一个UA池每次请求随机取一个第三层是使用代理IP池但这会增加系统复杂度毕设阶段一般不推荐除非你时间特别充裕。说句实在话如果目标网站反爬特别严格换个数据源是最务实的做法。很多小型旅游网站或者政府公开的旅游数据平台爬取难度低很多数据质量也不差。不要在爬虫上死磕毕设的核心是数据分析与可视化不是证明你能突破反爬。6.2 图表不显示或数据为空前端图表空白90%的情况是数据格式不匹配。ECharts的data属性要求的是一个数组数组元素是对象或数字。如果你后端的JSON结构里data是一个对象或者字段名对不上图表就会静默失败而且控制台大概率也不报错。排查思路很简单打开浏览器开发者工具切到Network标签看请求API接口的响应体是什么。如果响应体里的字段名和前端取的不一致比如后端返回的是spot_count前端写成了count那图表自然没有数据。字段名统一用驼峰或下划线前后端保持一致并且写一套固定的命名规范能减少大量这类问题。6.3 中文乱码问题中文乱码主要出现在两个地方一是爬虫抓取的数据写入MySQL后乱码二是前端页面显示乱码。数据库乱码的解决办法是在建库时指定utf8mb4字符集CREATE DATABASE travel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;前端页面乱码则是在HTML的head标签里加meta charsetutf-8同时确保Flask返回的数据是UTF-8编码。如果用了Requests库抓数据要记得在Response里设置resp.encoding utf-8。6.4 地图省份边界显示不全偶尔会遇到地图只显示部分区域、边界缺失的问题。这种一般是JSON格式的地图数据不完整或者坐标精度不够。解决办法是重新下载一份完整的中国省级地图GeoJSON数据放到static目录下替换掉原来的文件。GitHub上有很多开源的中国地图GeoJSON数据选个包含黑龙江各市边界坐标的版本即可。7. 论文撰写与答辩准备的实战经验毕设除了系统本身论文分量也很重。我这里分享一些论文写作的干货经验。第一章绪论部分背景调研要尽量用数据说话比如引用近年黑龙江省旅游收入、游客接待量的官方统计数据让选题的意义有理有据。第二章相关技术介绍不要照抄百度百科要结合系统里的实际使用场景来写比如写ECharts时可以提一句“本系统使用ECharts的地图组件实现景点空间分布的可视化展示”让每个技术点都落到实处。第三章系统设计把数据库ER图画清楚。第四、五章是系统实现和测试配图一定要多大屏截图、图表展示、数据库表结构截图多放图能显著降低论文查重率也能让老师觉得工作量足。答辩环节有几个高频问题提前准备好答案就不会慌。老师最常问的是“这个系统的创新点在哪”标准答法是数据来源真实、可视化交互性强、分析维度丰富。再就是“爬虫的合法性怎么理解”提前说清楚只爬取公开数据、遵守robots协议、不涉及个人隐私这个没有问题。还有“系统有哪些不足”可以主动承认数据量有限、未做实时更新后续可扩展方向是接入实时客流数据、构建预测模型。8. 写在最后一些实在的项目升级方向如果你做完基础版还有富余时间我建议朝这几个方向升级。第一个方向是引入机器学习预测模型基于历史月度游客评论数据用Prophet或者LSTM预测下个季度的旅游热度这会把系统的技术层次拔高一大截第二个方向是做一个简单的推荐系统根据用户历史浏览或收藏的景点类型推荐相似景点这能体现协同过滤算法的应用能力第三个方向是把数据源从静态爬取升级为定时增量采集做一个类Scheduled的定时任务体现系统可持续运行的能力。我个人在实际做项目时最大的体会是这类毕设并不需要炫技把基础做扎实反而更容易拿高分。很多同学贪多求全加了一堆功能最后每个都是半成品反而让导师觉得你逻辑混乱。按照“爬数据-清洗-分析-可视化”这条主线把每一个环节都做到能讲清楚、能演示、能回答问题这个项目就已经很完整了。最后再分享一个小技巧整个项目做完后把代码和文档整理到一个目录里按模块建子目录写一个README解释每个文件和目录的作用。这份目录既是你的设计文档也是你答辩时的提词器好好整理它能让你的展示环节特别加分。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →