☰
Python携程旅游数据分析大屏系统:爬虫、Django与机器学习完整实战
2026/10/2 18:44:41 网站建设 项目流程

每年到毕业设计答辩季,总有人跑来问我:有没有那种“看起来有技术含量、又能在短时间内做出来、答辩还不容易翻车”的项目?我的回答里,旅游数据分析大屏几乎每次都上榜。这个方向既能用上 Python、Django、selenium 爬虫、机器学习这些常见标签,又能把最终成果做成一个漂亮的展示大屏,评委一眼就能看到你在干什么。

我见过太多人下载了一堆“计算机毕业设计源码”,最后发现要么跑不起来,要么只能演示固定页面,稍微一问就露馅。问题从来不是代码量不够,而是做的人没搞懂整个系统是怎么串起来的。这篇文章就以“Python 携程旅游数据分析大屏系统”为例,把从数据采集、数据清洗、机器学习建模,到 Django 后端接口和大屏可视化这条完整链路拆开讲清楚。无论你是准备做毕设、想找项目练手,还是准备把这套方案改写成其他行业的分析系统,这都会是一篇值得收藏的实操参考。

1. 这个项目到底在做什么:需求拆解与技术栈选型

1.1 先别急着写代码,把功能边界画清楚

很多人拿到项目第一反应是打开 IDE 开始写,这其实是最大的坑。我习惯先把功能边界画出来:这个系统不是“爬虫项目”,不是“可视化项目”,也不是“算法项目”,而是一套数据驱动的旅游分析平台。它要能回答三个问题:哪些目的地热门?旅游价格随时间和出发地怎么变化?用户评论里大家真正关心什么?

围绕这三个问题,系统可以拆成五个模块:

  • 数据采集模块:采集旅游平台公开的景点、攻略、线路、评论数据。
  • 数据存储模块:清洗后的结构化数据存入 MySQL,方便后续查询统计。
  • 数据分析模块:用统计学和机器学习方法挖掘价格趋势、热度排行、情感倾向。
  • 后端服务模块:采用 Django 框架提供 API 接口,处理前端请求。
  • 可视化大屏模块:基于 ECharts 展示地图分布、趋势折线图、热力图和排行榜。

画完功能边界你会发现,项目的工作量其实没那么可怕。市面上很多付费毕设源码标价几百元,本质上就是把这五块拼起来。你自己理解清楚每一块的职责后,完全能实现同等效果,而且答辩时你能讲出每行代码的“为什么”。

1.2 技术栈选型:不是越新越好,而是越讲得清楚越好

技术选型是答辩时第一个会被问的话题,所以我通常建议选“经典但不过时”的组合。Python 3.10 作为开发语言,Django 4.x 作为 Web 框架,selenium 负责动态页面数据的采集,pandas 做数据处理,scikit-learn 或 XGBoost 做机器学习建模,MySQL 做存储,ECharts 做可视化。这套组合好处是:每个组件都有大量学习资料和轮子,出了问题网上随便一搜就有答案。

有人会问:为什么不用 Scrapy?Scrapy 确实是专业的爬虫框架,性能也更好,但它面对现代旅游网站常见的 JS 动态渲染页面时需要额外配置中间件,调试成本更高。而 selenium 通过模拟真实浏览器点击和滚动,对新手和答辩演示都比较友好。同理,为什么不用 Flask?Flask 轻量灵活,但如果涉及后台管理、数据库迁移、用户认证,Django 的“全家桶”模式能帮你减少大量重复设计工作。

还有一个加分项是标题里提到的“大模型”和“agent”。这部分不会成为系统主链路,但可以做成亮点模块,比如大模型驱动的智能问答助手,或者基于 Agent 思路的行程规划服务。接入方式不用很复杂,关键是把业务逻辑说清楚,这在第 3 章单独展开。

1.3 整体架构:数据怎么流动的

整个项目的数据流向可以用一句话概括:selenium 从旅游网站获取页面数据,经过解析和清洗后写入 MySQL,Django 后端读取 MySQL 并通过 API 输出给前端,前端大屏完成可视化渲染,机器学习模块在模型训练完成后通过接口提供预测能力,大模型 Agent 再基于这套数据做自然语言交互。

按这个流向,开发顺序应该是:先做爬虫和数据存储,再做数据处理和建模,然后写 Django 接口,最后搭大屏页面。这个顺序很重要,因为它保证每一层都有真实数据可以验证,而不是等到最后才发现前面采集的数据缺字段、格式混乱,回头大改。

2. 数据采集层:用 Selenium 抓取旅游公开数据

2.1 先分析页面结构,再决定是接口还是页面渲染

很多教程会让你启动浏览器直接开抓,但其实第一步应该是在浏览器里按 F12 看 Network 面板。以旅游网站的景点列表页为例,你先观察:页面数据是直接写在 HTML 里的,还是通过 XHR/JSON 接口异步加载的?

如果发现页面底部有一个接口返回 JSON 数据,那优先用 requests 库直接请求接口,效率高、解析简单,代码量还少。只有遇到以下情况才考虑上 selenium:

  • 接口参数中包含加密签名,逆向成本较高。
  • 数据需要模拟登录后可见,且登录逻辑比较复杂。
  • 页面通过滚动加载更多内容,且接口难以定位。

我实测下来,旅游类网站的景点列表、攻略列表往往可以直接用接口请求拿到,而个别详情页的评论数据是动态渲染的,这种场景用 selenium 更直接。最稳妥的方案是两者结合:能走接口的用 requests,走不通的交给 selenium。

2.2 Selenium 环境搭建与实战代码

Selenium 的环境搭建并不复杂,但有几个细节会影响稳定性,我先把完整的搭建过程列出来:

  1. 安装 Python 依赖:pip install selenium
  2. 下载对应浏览器版本的 WebDriver,以 Chrome 为例是 chromedriver。
  3. 写一个浏览器配置封装,包含无头模式、窗口大小、页面加载策略和用户代理设置。

看一下核心的配置代码:

from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def create_driver(): options = Options() options.add_argument("--headless=new") # 无头模式 options.add_argument("--disable-gpu") options.add_argument("--window-size=1920,1080") options.add_argument("--no-sandbox") options.add_argument( "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" ) options.page_load_strategy = "eager" # 不等所有资源加载完 driver = webdriver.Chrome(options=options) return driver

注意page_load_strategy这个细节。默认的normal策略会等待页面所有资源加载完毕,包括图片、广告脚本等,非常影响效率。改成eager后,只要 DOM 就绪就开始解析,速度能快不少。在采集场景中,你真正关心的往往是 DOM 里的文本数据,图片资源加载与否影响不大。

页面加载完成后,最常遇到的坑是元素还没出现就去找它。旅游网站的景点卡片通常需要滚动才能触发懒加载,所以我一般这样处理:

from selenium.webdriver.common.action_chains import ActionChains import time driver.get("https://example.com/scenic") driver.implicitly_wait(5) for i in range(5): driver.execute_script("window.scrollBy(0, 800);") time.sleep(1) wait = WebDriverWait(driver, 10) cards = wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.scenic-card")) ) data_list = [] for card in cards: name = card.find_element(By.CSS_SELECTOR, ".scenic-name").text score = card.find_element(By.CSS_SELECTOR, ".score").text price = card.find_element(By.CSS_SELECTOR, ".price").text data_list.append({"name": name, "score": score, "price": price})

关于滚动,我建议不要一次滚到底,分段滚动更接近真实用户行为,也能降低被站点风控系统识别为脚本的概率。每次滚动后加一个 0.5 到 1.5 秒的随机等待,这种“随机延迟”比固定延迟更接近人类操作节奏。

2.3 数据字段设计与落库方案

爬虫产出的数据不能直接倒进数据库,需要先设计表结构。以这个旅游分析系统为例,我建议设计四张核心表:景点信息表、景点评论表、线路产品表、攻略文章表。每张表只保留分析需要的字段,不要贪多。

我给出景点信息表的参考结构:

字段名类型说明
scenic_idint主键,唯一标识
namevarchar(100)景点名称
cityvarchar(50)所在城市
provincevarchar(50)所在省份
scoredecimal(2,1)评分,如 4.8
comment_numint评论数量
ticket_pricedecimal(10,2)门票价格
hot_levelint热度等级,1到5
longitudedecimal(10,6)经度
latitudedecimal(10,6)纬度
update_timedatetime采集时间

用 MySQL 建表时,我有个经验:价格字段一定不要用 float,用 decimal。float 在计算平均值时会出现精度问题,比如 4.58 变成 4.58000001,排队求和后结果给答辩老师看就很尴尬。decimal 是定点数,计算稳定。

写数据时用 pandas 的to_sql或者 Django ORM 的bulk_create都可以。数据量不大的情况下,我更推荐bulk_create,因为可以在入库前做更深度的校验。

2.4 合规与稳定性策略

爬虫的道德和合规问题必须放在台面上说。我个人的实践原则是:只采集公开可见的数据,不采集用户隐私信息;控制请求频率,不搞并发轰炸;定期检查目标站点的规范声明,并设置清晰的 User-Agent 标识身份。毕业设计是学术用途,更应该带头遵守这些规则。

实际采集时,稳定性往往比速度更重要。Selenium 常见的失败场景包括:页面超时、弹窗遮挡点击、元素偶尔找不到、登录态过期。我的通用处理方式是统一封装一个“重试装饰器”,对抛出异常的函数自动重试 3 次,每次等待时间递增。这样整体采集任务挂机跑几小时也不会轻易中断。

至于验证码,我的建议是:遇到就停止重试,等待一段时间再继续,或者预留一个人工介入的间隔。不要在毕设里引入灰色手段来对抗验证码,风险大且没有实际意义。

3. 从原始数据到业务指标:数据清洗与特征工程

3.1 数据清洗的几个高频场景

爬下来的数据90%是“脏数据”,这是所有数据类项目都要面对的真相。旅游数据常见的脏问题我遇见不少,这里挑几个典型的讲。

第一个是缺失值。很多景点的评论数可能为空,可能是页面渲染时值为空,也可能是景点刚上线还没有评论。处理策略很简单:数值型字段用中位数填充,类别型字段填充“未知”,但如果某个关键字段缺失比例超过 30%,我倾向于直接删除这条记录,因为填充后引入的噪声可能比缺失本身更严重。

第二个是重复记录。同一个景点可能因为爬虫多次运行而重复入库,判断重复的标准不能只看名称,因为全国很多地方都有“人民公园”,要采用“城市+名称”的组合键去重。去重后保留 update_time 最新的记录。

第三个是文本噪声。评论内容里经常混入表情符号、HTML 标签、还有“该用户未填写评价”之类的占位文本。清洗时统一用正则过滤掉特殊符号和占位文本,再把空字符串转成 NaN。

这里给一段清洗代码作示例:

import pandas as pd df = pd.read_csv("scenic_raw.csv") # 1. 去重 df["dup_key"] = df["city"] + "_" + df["name"] df = df.drop_duplicates(subset=["dup_key"], keep="last") # 2. 文本噪声清理 import re def clean_text(s): if pd.isna(s): return "" return re.sub(r"<.*?>|[\u4e00-\u9fff0-9a-zA-Z,。!?、;:,.!?;:]+", "", str(s)) # 3. 数值字段处理 df["ticket_price"] = pd.to_numeric(df["ticket_price"], errors="coerce") df["comment_num"] = df["comment_num"].fillna(df["comment_num"].median()) df = df.dropna(subset=["ticket_price"])

清洗完后建议把中间结果单独存一份 CSV,目的是在答辩时展示“数据前后对比”,这其实比模型本身更打动评委。你可以做一张清洗前 vs 清洗后的对照表,展示缺失值占比从 20% 降到 0%,重复率从 8% 降到 0%,这样的数据故事很有说服力。

3.2 特征工程与业务指标定义

清洗后的数据还不能直接喂给机器学习模型,必须先做特征工程。特征工程的本质是把业务理解转换成模型能懂的数值向量。

以价格预测任务为例,原始特征可能有出发地、目的地、行程天数、出发日期、跟团类型。这些字段大多不是数值型,所以要处理。出发日期可以拆出月份、星期几、是否节假日三个衍生特征,比如五一、国庆的属性权重就完全不同。组团类型可以用 one-hot 编码,出发地和目的地可以做标签编码。

业务指标方面,我定义了几个在平台分析中最常用的:

  • 目的地热度分 = 景点数量 0.3 + 平均评分 0.2 + 评论量对数归一化 0.3 + 线路数量 0.2。
  • 价格性价比 = 平均评分 / 价格,衡量一个目的地花同样的钱能获得多好的体验。
  • 情感指数 = 一段时间内正面评论占比减去负面评论占比,取值范围在 -1 到 1 之间。
  • 出游旺季度 = 某月订单量或评论量占全年比例。

这些指标建好后可以直接作为大屏展示的“核心指标卡”。我希望你特别注意:指标定义要能讲出“为什么这样算”,比如热度分为什么评论量要取对数,是因为评论量分布是长尾的,少数热门景点挤占了大部分数量,取对数后再归一化能让指标分布更平滑。

3.3 机器学习建模:价格预测与情感分析

毕设里的机器学习模块,不建议追求复杂模型,而要追求“能讲清楚原理 + 效果可视化”。我通常建议做两个任务:旅游产品价格预测和评论情感分析。

价格预测可以用 Gradient Boosting 模型。特征就是上一节构造的特征集合,目标字段是线路价格。划分训练集和测试集时要注意:不要把同一条线路的不同日期混进训练集和测试集,否则会造成数据泄漏,评估指标虚高。正确做法是按照时间划分,比如 2023 年数据训练,2024 年数据测试。

模型评估用均方根误差和 R²,我会在系统里保留一个“模型效果页”,展示 R² 值和预测价格 vs 真实价格的散点图。答辩时老师看到这个页面,基本上就能确认你真的训练过模型。

情感分析相对简单。如果评论量不大,可以直接用 SnowNLP 计算一个情感分数,但这套模型对旅游文本的适配度一般。想做得更专业一点,可以基于标注数据训练一个朴素贝叶斯或者逻辑回归分类器,特征用 TF-IDF。在大模型时代,还可以调用大模型 API 做情感判断,效果更好,但速度慢成本也高。

我做对比实验时发现:经典机器学习模型在这个场景下完全够用,准确率能做到 80% 左右,而且方便在答辩时演示训练过程。大模型方案可以作为“锦上添花”的对比模块,不必须用它作为最终生产方案。

3.4 大模型与 Agent 能力的接入方式

标题里带“大模型”和“agent”,很多人不知道怎么接,我介绍一种不夸张、能落地的方式。

大模型在系统里有两种角色。角色一是智能问答接口:用户输入“我想找适合亲子游且性价比高的目的地”,系统先通过关键词识别意图,从数据库取出符合条件的景点列表,再交给大模型整理成自然语言回答。角色二是 Agent 旅行规划助手:用户给出大致出行计划,Agent 通过工具调用查询实时数据、景点热度、价格区间,再分步骤生成行程方案。

Agent 的实现可以用很简单的 ReAct 思路:给大模型一个“函数列表”,比如search_scenic(city, tag)、get_price_trend(scenic_id),大模型根据用户问题决定调用哪个函数、传什么参数,得到函数结果后再组织回答。

这套方案的核心价值不在算法本身,而在于把非结构化问题和结构化数据库连接起来。在毕设答辩里,你可以这样介绍:传统系统只能看固定图表,加了 Agent 后,系统能听懂自然语言并自主完成数据查询,这是“交互方式”维度的创新。

4. Django 后端与大屏可视化:把数据变成可看的业务故事

4.1 Django 项目结构与数据模型设计

Django 项目建议按照“项目 + 应用”的经典结构组织。创建一个名为travel_analysis的项目,下面分datacenter负责爬虫数据管 理,analysis负责算法任务,api负责接口输出,dashboard负责大屏页面。这种分模块的做法能让你在答辩时说清楚每个代码文件属于哪一层。

在models.py中,景点信息表的 ORM 模型可以这样写:

from django.db import models class ScenicInfo(models.Model): scenic_id = models.AutoField(primary_key=True) name = models.CharField(max_length=100, verbose_name="景点名称") city = models.CharField(max_length=50, verbose_name="所在城市") province = models.CharField(max_length=50, verbose_name="省份") score = models.DecimalField(max_digits=2, decimal_places=1, verbose_name="评分") comment_num = models.IntegerField(default=0, verbose_name="评论数量") ticket_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="门票价格") longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True) latitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True) class Meta: db_table = "scenic_info" verbose_name = "景点信息"

我推荐在模型里就把verbose_name定义好,因为 Django Admin 后台会直接展示这些名称,管理数据时一目了然,也方便演示给老师看。

4.2 API 接口设计与权限控制

大屏前端需要的接口通常有这几个:目的地热度榜单、价格趋势、城市分布地图、评论词云、核心指标卡。按 RESTful 习惯,接口路径可以设计为/api/scenic/rank/、/api/trend/price/、/api/geo/distribution/。

我建议直接用 Django 的 JsonResponse 配合@csrf_exempt装饰器写简单接口,不需要引入 Django REST Framework。毕设场景下引入 DRF 虽然也很好,但如果你时间紧张,没必要为了“看起来高级”增加额外复杂度。

举个例子,热度排行榜接口的代码大致是这样的:

from django.http import JsonResponse from datacenter.models import ScenicInfo from django.db.models import F def scenic_rank(request): top_scenic = ( ScenicInfo.objects.all() .order_by("-comment_num", "-score")[:20] .values("name", "city", "score", "comment_num", "ticket_price") ) return JsonResponse({"data": list(top_scenic)}, safe=False)

接口写好之后,我用 Postman 做一轮接口自测,确认状态码、字段名、数据结构全都符合前端预期。这里有个容易被忽略的坑:接口返回的字段名修改后,前端经常忘记同步,我习惯把接口定义写在同一份文档里,前端和后端共用。

4.3 大屏可视化:ECharts 方案与页面布局

旅游数据分析大屏的灵魂在可视化,但很多人只关注图表效果,忽略了布局逻辑。大屏的视觉层级应该是:中央区域放全国地图或热力迁徙图,左侧放目的地热度榜和核心指标,右侧放价格趋势和情感比例,底部放评论词云。

ECharts 引入方式很简单,直接在 Django 模板中通过 CDN 引入,或者下载 echarts.min.js 放 static 目录。地图数据如果用中国地图,需要在 ECharts 之外引入对应地图 JSON 注册。展示热门目的地分布时,我用的是地理坐标散点图,配合 visualMap 控件,让分数高的目的地亮红色、分数低的显示淡蓝色。

价格趋势图则用折线图,展示某条热门线路在未来几个月的价格走向。因为价格数据是模型预测出来的,可以在折线图上把“历史真实价”和“预测价”用不同颜色标注,答辩时一眼就能看出模型效果。

建议你花时间打磨的一个细节是数据自动刷新。在页面中用一个定时器,每 30 秒重新请求一次接口,图表动态更新。这里要注意 ECharts 的setOption调用时设置notMerge: true,避免新旧数据合并后出现残影。

4.4 定时任务让数据“活”起来

毕业设计最怕“死数据”:答辩那天展示的还是两个月前爬的数据,老师随便一查实时信息就对不上。解决办法是给系统加上定时采集任务。

Django 中实现定时任务最常用的方案是 django-celery-beat 配合 Redis 作为 broker,功能强大但配置略多。如果只是让数据每周更新一次,我建议用轻量方案 Apscheduler,配合 Django 的AppConfig.ready()在应用启动时加载调度器。

定时任务的核心工作流是:

  1. 调用爬虫模块,增量采集最新评论和价格数据。
  2. 数据进入清洗流程,更新数据库。
  3. 完成后重新训练模型或更新预测结果。
  4. 更新后的数据通过 API 输出,前端大屏自动拉取新数据。

这个自动化闭环可以在答辩时演示:你现场修改数据库里的某条价格,过 30 秒后图表更新,非常直观。

5. 我在实操中踩过的坑与排查技巧

5.1 Selenium 不稳定与元素定位问题

Selenium 最常见的问题是“时好时坏”,同一个脚本今天跑通,明天就报找不到元素。我踩过最典型的坑是:页面存在多个同 class 元素,find_element默认只返回第一个,但第一个元素可能在可视区域之外,无法读取文本值。

解决方法有两条。一是改用显式等待 + 自定义条件,等目标元素的文本不再变化或已经出现预期内容;二是用 XPath 结合文本定位,比如//div[contains(@class,'scenic-card') and .//span[contains(text(),'评分')]],定位更精准。

还有一种情况是页面弹窗遮挡元素,比如旅游网站偶尔会弹出登录引导框。处理方式很简单:进入页面后先尝试点击关闭按钮,如果不存在就跳过,不要一味等待元素出现。这里我一般用 try-except 包裹关闭操作,保证主流程不被次要弹窗打断。

5.2 数据质量引发的连锁问题

数据清洗的疏漏会导致模型的预测结果非常离谱。我曾经遇到过一个问题:评论数字段混入了“条”这个中文字符,pandas 读取时整列变成了字符串,模型训练时直接报错。

这类问题的排查思路是先对字段做类型检查:

print(df.dtypes) print(df["comment_num"].apply(lambda x: type(x)).value_counts())

建议你在数据入库前统一做一个 schema 校验:姓名、景区名等主键字段不能为空;价格字段必须在合理区间(比如 0 到 99999);评分必须在 1 到 5 之间。超出合理范围的记录直接写入脏数据表存证,而不是默默丢弃,这样你能知道原始数据到底发生了什么。

5.3 答辩时容易被问到的高频问题清单

我把带学生做这类项目时评委经常追问的问题整理了一份清单,你提前准备就不慌:

高频问题建议回答思路
为什么选 Selenium 而不是 Scrapy?目标页面数据是动态渲染的,Selenium 能直接执行 JS 获取最终 DOM,开发周期短,且本系统数据量可控,运行效率不是主要瓶颈。
你的模型效果怎么评估?用 RMSE 和 R²,并且按时间划分训练集和测试集,避免数据泄漏,展示训练集、测试集上的表现对比。
爬虫的合法性怎么保证?只采集公开展示的非隐私数据,限制请求频率,遵守目标站点的访问规范,代码中为抓取任务设置了明确的时间间隔。
如果数据量到千万级,系统会怎么优化?增加 Redis 缓存热点数据、MySQL 分表分库、爬虫改异步框架、增加消息队列削峰填谷。
大模型 Agent 在系统里的作用是什么?将非结构化用户问题转化为结构化查询条件,通过函数调用获取数据,最后生成自然语言回答,降低用户使用门槛。
定时任务失败怎么处理?Apscheduler 设置了失败重试,同时任务执行结果写入日志表,方便定位失败原因。

这套项目做下来,最大的收获不是代码量,而是你完整走了一遍“数据采集 → 数据治理 → 算法建模 → API 服务 → 可视化展示”的闭环。我自己的体会是,旅游数据分析类的毕设最出彩的地方不在于模型调参调得多好,而在于数据故事有没有讲清楚:从用户想知道什么出发,到系统用什么方式回答,再到大屏如何呈现答案,这条逻辑线越完整,答辩就越有底气。

最后再分享一个小技巧:如果你的时间只够打磨一个模块,优先打磨大屏视觉效果。评委第一眼看的一定是界面,第二眼才是技术细节。把地图迁徙、热力过渡、自动滚动跑起来,你对这个系统的掌控感会完全不一样。后续如果想扩展,还可以加入实时交通数据预测节假日客流、接入天气数据做出行舒适度评分、甚至把 Agent 升级成支持多轮对话的智能旅游顾问,这些方向都会让项目的深度再上一个台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询