1. 项目概述与毕业设计选题思路
1.1 这是一个什么项目
先直接说结论:这是一个基于Django框架搭建的旅游景点信息采集、分析与可视化系统,底层用selenium做动态页面数据采集,把热门旅游城市、景点的评分、热度、门票价格、地理位置、用户评价等数据抓下来,存进数据库,再通过Django提供数据接口,前端用可视化图表把“冷冰冰的数据”变成一眼就能看懂的走势图、排行图和分布图。
标题里还带着“大模型”“agent”这两个词,这块我后面会单独展开讲。很多同学看到这俩词第一反应是“是不是要接一堆复杂的模型框架”,其实不是,毕业设计阶段把大模型的能力用在一个聚焦的小场景里,比如“自动生成景点推荐语”“对评论做情感分析摘要”,就已经是非常好的加分项。
这系统解决的是什么问题?其实就是两件事:一是省掉人工收集信息的时间——你想对比十个景点的热度变化、价格波动,手动一个个网页复制粘贴能累死个人,采集模块每跑一轮自动更新;二是让数据会说话——单纯抓下来一堆数字没有意义,做成趋势图、排行表,配合筛选条件,用户能直观看到“哪个景点最近被讨论得最多”“哪个目的地性价比更高”。
适合谁参考?两类人。一类是计算机相关专业正在找毕业设计题目的同学,这个题目切中的热点词非常多——爬虫、数据分析、可视化、Django、大模型,评审老师一看就知道工作量饱满、技术栈主流;另一类是刚开始学爬虫和Web开发、想做个完整项目的开发者,这套系统的技术链条覆盖了从数据采集到最终展示的全流程,跟着文章复现一遍,对Django和爬虫的认知会上一个台阶。
1.2 为什么说这是个“万金油”选题
我见过太多毕业设计选题,要么太虚,要么太窄。太虚的比如“基于某某框架的智能系统”,答辩的时候拿不出实际功能;太窄的比如“宿舍楼电量查询系统”,做完也就那么回事,技术点撑不起一篇论文。
旅游景点采集分析可视化系统就不一样,它的技术覆盖面足够宽,每一层都有能写进论文的实际内容:
- 数据采集层:selenium处理JavaScript动态渲染页面、请求头伪装、反爬绕过策略、增量爬取与去重逻辑。
- 数据存储层:MySQL或SQLite的表结构设计、数据清洗、字段规范化。
- 后端业务层:Django的MTV架构、ORM模型、RESTful接口设计、分页与筛选逻辑。
- 数据分析层:热门景点排行、评分分布、价格区间统计、时间趋势分析。
- 可视化层:ECharts图表接入、地图分布展示、筛选联动交互。
- 智能化层:语言模型接口调用、Prompt设计、评论情感判断。
任何一个层面单独拿出来,都可以成为论文里的一章。而且这个题目扩展性极强,今天想做带地理位置的景点分布图,明天想加入机票价格预测模型,后天想把静态图表改成动态大屏,都行——随着你的进度和兴趣,系统的复杂度可以随时伸缩,这一点对时间不确定的毕业生来说非常友好。
1.3 项目核心功能模块一览
| 模块名称 | 核心功能 | 关键输出 |
|---|---|---|
| 采集模块 | 基于selenium遍历景点列表页、详情页,提取名称、评分、评论数、价格、坐标等字段 | 结构化景点数据集 |
| 存储模块 | Django ORM模型 + MySQL数据库,实现数据持久化与去重更新 | 干净、规整的数据表 |
| 接口模块 | 按城市、评分、热度等多维度条件查询数据,返回JSON格式 | RESTful数据接口 |
| 分析模块 | 计算热门排行、价格分布、好评率、趋势变化等指标 | 可直接用于展示的聚合数据 |
| 可视化模块 | ECharts实现柱状图、折线图、饼图、地图下钻 | 用户可交互的图表面板 |
| 大模型模块 | 调用语言模型服务生成景点简介、推荐语,对短评论做情感倾向判断 | 智能化文本输出 |
我建议你在开题报告里就按这六个模块去写任务书,每个模块对应一条技术路线,工作量明明白白,后面做起来也不容易跑偏。
2. 技术选型与环境搭建:Django + selenium组合怎么定下来的
2.1 为什么是Django,不是Flask也不是FastAPI
先说我的观点:毕业设计选Web框架,优先考虑Django,不要为了“轻量”去选Flask。
Flask确实上手快,但一个需要用户登录、后台管理、数据建模、接口分层的系统,你用Flask就得自己装一堆第三方扩展:Flask-SQLAlchemy管数据库、Flask-Login管登录、Flask-Admin管后台、Flask-CORS管跨域……装完一圈回来,还不如直接用Django自带的。
Django的优势在这几个点体现得非常明显:
- 自带Admin后台。我采集的数据想快速看一眼有没有脏数据,直接登录
/admin就行,不需要自己写管理页面。 - ORM成熟。建表不需要手写SQL,定义好models类,一句
python manage.py makemigrations && migrate就同步到数据库,改字段也方便。 - MTV架构分层清晰。写论文的时候架构图画出来很标准,导师看到就知道你理解Web开发的核心思想。
- 社区资料多。出了问题搜一下全是答案,对毕业生来说这太重要了。
FastAPI性能强、自动生成接口文档,但对于这个系统的性能需求来说属于“杀鸡用牛刀”,而且国内答辩组老师对FastAPI的熟悉程度远不如Django,选Django至少不会被质疑“为什么不用主流方案”。一句话,毕设选型最重要的是“稳妥”和“够用”。
2.2 为什么采集用selenium,而不是requests + BeautifulSoup
这是个经典问题,也是很多同学答辩时会被追问的点。
如果目标网站是服务端渲染,也就是右键“查看网页源代码”能看到完整内容,那用requests直接请求页面再套BeautifulSoup解析,效率高、速度快、代码简单。
但旅游类网站几乎都有一个问题:页面内容是JavaScript动态加载的,比如景点列表页,你要的数据可能在页面滚动到某个位置后才通过Ajax请求接口返回,requests拿到的只剩一个空壳HTML。
selenium的思路完全不同:它模拟真实浏览器的行为,打开网页、等待渲染、滚动屏幕、点击翻页按钮,然后再去页面里提取元素。相当于一个“机器人替你浏览网页然后把看到的内容记录下来”,这就能绕开绝大多数动态渲染的限制。
具体对比我放一张表:
| 对比维度 | requests + BeautifulSoup | selenium |
|---|---|---|
| 请求方式 | 直接HTTP请求 | 控制真实浏览器内核 |
| JS渲染内容 | 拿不到 | 可以拿到 |
| 反爬对抗 | 容易被识别封IP | 更接近真人操作,配合随机延时更好用 |
| 速度 | 快 | 慢,因为要开浏览器 |
| 适合场景 | 静态网页批量采集 | 动态渲染、需交互操作的页面 |
| 学习成本 | 低 | 中,需要理解元素定位和等待机制 |
毕设场景下,稳定性比速度更重要,所以我对这个项目的建议是:主采集用selenium,配合Selenium Wire或mitmproxy这类的流量拦截手段做辅助,因为有些网站的数据其实是接口返回的JSON,直接拿接口更快更稳。
2.3 环境版本与依赖清单
这是我的实测环境,兼容性很好,没有踩到版本坑:
Python 3.10.11 Django 4.2.0 selenium 4.15.0 MySQL 8.0 (也可以用SQLite零配置替代) pandas 2.0.0 numpy 1.24.0 djangorestframework 3.14.0 django-cors-headers 4.0.0 pyecharts 2.0.5 (或前端直接引ECharts) openai 1.3.0 (如果走云端语言模型API)安装命令直接给出来:
pip install django==4.2.0 selenium==4.15.0 pandas==2.0.0 numpy==1.24.0 pip install djangorestframework==3.14.0 django-cors-headers==4.0.0 pip install openai==1.3.0数据库建议直接用MySQL,一是毕设评分看“用了独立数据库”印象分更高,二是MySQL的SQL查询能力后面做数据分析会用到,比SQLite踏实。装MySQL、建库建用户这些基础操作我就不赘述了,网上一搜一大把,注意字符集设成utf8mb4,不然存emoji表情评论会报错。
浏览器驱动也要对应上,我用的Chrome,版本和chromedriver版本必须一致,这个坑每天都能看到人踩。
3. 数据采集模块设计与实现:selenium爬虫的完整落地
3.1 定义明确的数据字段
采集之前先想清楚要存什么字段,不然爬完发现少了数据,又得回头补采集,非常消磨耐心。
我设计的景点信息表attraction_info核心字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| spot_id | 自增主键 | 景点唯一标识 |
| spot_name | varchar(100) | 景点名称 |
| city_name | varchar(50) | 所在城市 |
| spot_rank | int | 热度排行 |
| score | float | 评分(如4.8) |
| comment_num | int | 评论数量 |
| price | float | 成人门票价格 |
| open_time | varchar(100) | 开放时间 |
| spot_address | varchar(200) | 具体地址 |
| lon / lat | float | 经纬度坐标 |
| tags | varchar(200) | 景点标签(如“亲子”“历史古迹”) |
| intro | text | 景点简介 |
| crawl_time | datetime | 采集时间 |
为什么要存坐标?因为后面可视化要做地图分布图,没经纬度就画不了。
3.2 selenium采集主流程
框架写出来大概是这么个结构:
import time import random from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def init_driver(): options = webdriver.ChromeOptions() # 隐藏自动化控制标识 options.add_experimental_option('excludeSwitches', ['enable-automation']) options.add_argument('--disable-blink-features=AutomationControlled') options.add_argument('--disable-gpu') options.add_argument('--no-sandbox') options.add_argument('--lang=zh-CN') driver = webdriver.Chrome(options=options) driver.set_page_load_timeout(20) return driver def parse_list_page(driver): items = driver.find_elements(By.CSS_SELECTOR, 'div.list-item') for item in items: name = item.find_element(By.CSS_SELECTOR, '.name').text score = item.find_element(By.CSS_SELECTOR, '.score').text comment = item.find_element(By.CSS_SELECTOR, '.comment').text # ... 提取更多字段 def fetch_attractions(city): driver = init_driver() url = f'https://example.com/{city}/hot' driver.get(url) # 等待页面核心元素出现 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, '.list-item')) ) # 滚动到底部触发懒加载 driver.execute_script('window.scrollTo(0, document.body.scrollHeight);') time.sleep(2) parse_list_page(driver) # 翻页 while True: try: next_btn = driver.find_element(By.CSS_SELECTOR, '.next') if 'disabled' in next_btn.get_attribute('class'): break next_btn.click() time.sleep(random.uniform(2, 5)) # 随机延时,降低被识别风险 parse_list_page(driver) except Exception: break driver.quit()有几个经验点要单独说说。
第一,元素定位用CSS Selector而不是XPath。CSS Selector在大多数情况下更简洁,比如.list-item .name一眼能看懂,而//div[@class='list-item']//span[@class='name']写起来啰嗦,后台报错还不好排查。当然,元素锁得太死会降低定位灵活性,所以我建议使用CSS选择器作为首选项。
第二,用显式等待,不要用time.sleep()硬等。有同学图省事,每个页面固定睡5秒,结果页面加载快了浪费时间,加载慢了元素还没出来就报错。WebDriverWait的语义是“等到某个条件满足才往下走”,既稳又快。我见过很多案例都是这里用错导致采集大量失败。
第三,移动端页面通常没有养爬虫防护,但链路长一点。若站点原生支持,可以换UA为移动端再采集一次,往往两个源互补更完整。这块你写论文可以引申为“多源融合采集策略”。
3.3 数据去重与增量更新
采集爬虫最容易遇到的问题就是跑了几轮之后数据大量重复。解决方案是给数据库表的spot_name和city_name加上联合唯一索引,Django模型里这样写:
class AttractionInfo(models.Model): spot_name = models.CharField(max_length=100) city_name = models.CharField(max_length=50) score = models.FloatField(null=True, blank=True) comment_num = models.IntegerField(default=0) price = models.FloatField(null=True, blank=True) lon = models.FloatField(null=True, blank=True) lat = models.FloatField(null=True, blank=True) crawl_time = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'attraction_info' unique_together = (('spot_name', 'city_name'),)配合get_or_create的方式写入数据,这样同一景点不论采集多少轮都只会有一行记录,最新的字段直接覆盖。
增量采集我用的策略比较简单但也够用:记录每个景点的评论数变化,比如上一次评论数是1200,这次变成了1350,说明热度在上升,数据有变化才更新comment_num字段。这是后面趋势分析的数据来源。
3.4 采集模块的异常处理
selenium采集最考验人的不是功能实现,而是异常处理。页面结构变了、弹窗挡住了、验证码出来了、元素加载超时了,这些都可能导致爬虫中途崩溃。
我的经验是三步走:
- 在解析每个景点时用try/except包裹,单条数据出错不影响整体循环。
- 全局设置重试机制,一个城市采集失败就重新发起来跑,最多重试3次。
- 记录日志。打印醒目的报错信息和点位,
loguru库比print好用得多,存文件、带颜色、定位行号。
在采集层写好异常兜底,程序挂机跑一夜才安心,毕设演示的时候也不会因为网络波动当场翻车。
4. 数据分析与可视化:从“一堆数据”到“一张图”
4.1 选哪些分析维度才算“有料”
数据抓回来了,下一步是让它产生价值。我建议不要做那种“展示每个景点的评分是多少”的简单表格,太无聊。往下面这六个维度做,分析就有深度了:
- 热门城市景点数量与热度排行Top10:一眼看出哪些城市是旅游第一梯队。
- 景点评分分布直方图:分析整体口碑情况。
- 价格区间分布图:比如0-50、50-100、100-200、200+四档,看看景点定价结构。
- 评论数Top20景点横向对比:评论数代表真实热度,比排行更有数据感。
- 不同城市平均票价对比:直观反映目的地消费水平差异。
- 大模型评论情感摘要与推荐语生成:这是整个系统最有亮点的部分,后面单独讲。
我拿模拟项目X跑了真实数据,比如某些热门城市平均票价上探到200元以上,而西部部分城市主流票价集中在百元以内,这种差异呈现出来要比单个景点的罗列有意思得多。
4.2 可视化方案选择:前后端分离还是pyecharts?
可视化我试过两条路,说说各自的优缺点。
第一条,pyecharts后端生成HTML页面。代码写起来快,Django视图函数里生成图表HTML字符串,直接渲染到模板,不需要额外开前端工程。缺点是交互插件和自定义样式受限,前后端代码耦合度高。
第二条,前端原生HTML + ECharts库 + Django REST接口。后端提供JSON数据接口,前端用ECharts读接口数据渲染图表,筛选条件变化时重新请求接口。这更贴近真实项目开发模式,答辩时也可以讲“前后端分离架构”。
我最终选了第二条,原因是毕设展示的时候,实时交互效果比静态图表惊艳得多。做一个城市筛选下拉框,选中“某城市”后图表全部联动更新,这个展示效果在现场演示环节非常加分。
前端核心代码的结构化思路给一个示例:
$.ajax({ url: '/api/analysis/city_top10/', type: 'GET', data: { city: selectedCity }, success: function(res) { var chart = echarts.init(document.getElementById('top10')); chart.setOption({ title: { text: '城市热门景点Top10' }, tooltip: {}, yAxis: { type: 'category', data: res.data.names }, xAxis: { type: 'value' }, series: [{ type: 'bar', data: res.data.commentNums, itemStyle: { color: '#49a9ee' } }] }); } });后端Django的接口用DRF写,几行代码就搞定序列化。
4.3 数据分析接口的设计套路
接口不要堆在某个视图函数里,按模块拆清楚。我的接口结构是:
urlpatterns = [ path('api/analysis/city_top10/', views.city_top10), path('api/analysis/price_distribution/', views.price_distribution), path('api/analysis/score_histogram/', views.score_histogram), path('api/analysis/avg_price_by_city/', views.avg_price_by_city), path('api/attractions/list/', views.attraction_list), ]比如avg_price_by_city的核心逻辑:
from django.db.models import Avg def avg_price_by_city(request): city_name = request.GET.get('city', '') queryset = AttractionInfo.objects if city_name: queryset = queryset.filter(city_name=city_name) result = queryset.values('city_name').annotate( avg_price=Avg('price'), count=Count('id') ).order_by('-avg_price') return JsonResponse({'data': list(result)}, safe=False)Django的ORM聚合查询是现成的知识点,论文里单开一节写这个也算技术点。分析维度和可视化相结合,整个系统的“分析”属性就很扎实了。
5. 大模型与agent场景落地:给系统加一个“会说话的大脑”
5.1 大模型在这个系统里到底做什么
我看到很多毕设把大模型硬塞进去,比如写个“基于大模型的问答机器人”,但实际就是个ChatGPT套壳,没意思。大模型的正确用法,是去解决传统程序做不好或做不了的事情。
在这个旅游系统里,有两个场景特别合适。
场景一:评论情感分析与摘要。
景点评论每条几十上百字,人工看不过来。调大模型接口对评论做判断:正面、负面还是中性,再聚合出“游客普遍反映排队时间过长”“适合亲子出行”这类摘要。传统情感分析要用机器学习训练分类器,数据量少效果差;大模型零样本就能完成这个任务,准确率还非常高。把评论摘要展示在景点详情页旁边,用户在对比景点时一目了然。
场景二:首页智能推荐语。
根据景点的标签、评分、评论数、价格,生成一段自然语言推荐语。比如数据里有“古迹”“评分4.9”“评论数2万+”,大模型生成:“历史与口碑兼具的热门文化景点,适合对人文感兴趣的游客,建议错峰出行以提升体验。”这段文字你很难用模板穷举出来,这就是大模型的价值。
5.2 走本地推理还是云端API?
毕设阶段我不建议你本地部署大模型,因为7B量级的模型也要至少8G显存,普通学生电脑根本带不动。直接用云端API是更现实的选择,有免费额度的服务商也不少。
调用逻辑很简单,封装成一个函数:
def generate_attraction_summary(attraction_info: dict) -> str: prompt = ( "你是一名资深的旅游编辑,请根据以下景点信息,写一段80字以内的" "推荐语,语气自然,不要套话。\n" f"景点名称:{attraction_info['name']}\n" f"评分:{attraction_info['score']}\n" f"评论数:{attraction_info['comment_num']}\n" f"标签:{attraction_info['tags']}\n" f"门票价格:{attraction_info['price']}元\n" ) response = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "你是一个可靠的旅游内容助手。"}, {"role": "user", "content": prompt} ], temperature=0.7, max_tokens=200 ) return response.choices[0].message.content注意,不要用真实公司名称,你就叫它“语言模型服务”。答辩时你讲清楚“使用大模型的零样本能力对UGC评论做结构化理解”,老师就明白你的设计了。接口免费额度对毕设演示来说基本够用,不要天天跑全量批处理就行。
5.3 “agent”标签的含义扩展
“agent”严格说是在大模型能力之上增加“工具调用”“任务规划”这类智能化能力,毕业生做全流程agent容易失控,但可以从两个实用角度切入:
- 让采集模块可配置化:写一段“指令”定义要采集的城市、页面深度、字段范围,大模型理解后生成配置JSON,采集器按配置执行。这其实就是一个“意图识别 + 工具调用”的小型agent骨架。
- 让分析结论自然语言化:调用语言模型把聚合结果转成带观点、有对比的顾问式结论,比如“根据2023—2024年数据,某某城市的热门景区门票均价上升了18%,主要原因是新晋网红景区拉高了均值”。
这块也是论文里“创新点”的很好素材,既有深度又有落地场景,不会被认为是华而不实的噱头。
6. 常见问题与排查技巧实录
这个部分直接把我在开发过程中被折磨过的坑全部列出来,你照着避。
6.1 环境与驱动问题
问题:selenium启动时报session not created或chrome not reachable。处理:90%的情况是Chrome浏览器自动更新了,但chromedriver还是旧版本。去chromedriver官网下载对应浏览器版本的驱动,替换掉旧的就行。注意不要用“最新版本驱动”盲目覆盖,要严格版本匹配。
问题:Django连接MySQL报Unknown character set: 'utf8mb4'。处理:检查MySQL版本,5.5及以下不支持utf8mb4,至少要用5.7以上或8.0。之前我帮A同学排查,发现他本地MySQL是5.5老旧版本,换成8.0后问题消失。如果用云数据库也要注意服务端字符集配置。
6.2 日志与清理问题
问题:日志文件越来越大,磁盘满了。处理:使用RotatingFileHandler按大小切割日志文件,超过10MB就自动归档保留最近5份。毕设项目跑一个月,光采集日志就积累了几个GB,不处理会拖垮系统。建议凡是写日志的地方都考虑加个自动切割。
6.3 爬虫与采集问题
问题:selenium一直检测到“不是真人”。处理:是因为直接用默认的Chrome自动测试模式了。加上--disable-blink-features=AutomationControlled参数,把navigator.webdriver属性改掉。再配合每次点击前的随机鼠标轨迹延迟,实测稳定度提高非常多。
问题:翻页到第三页就无数据。处理:很多列表页的URL翻页有规律,但前几页走JS点击,后几页走URL参数拼接,需要你把翻页策略改成“优先直接拼URL,不行再点击”。我模拟项目X时发现,对某些目标站,拼URL直取比点击慢,但成功率高,这个取舍你要学会。
问题:数据里有乱码或空白字符。处理:采集后的文本统一做清洗,用strip()去掉首尾空格,用正则去掉控制字符。Django存MySQL前要注意字符串长度,比如简介字段超长会导致ORM保存报错。
6.4 数据库与接口问题
问题:MySQL连接数过多导致Too many connections异常。处理:把数据库连接池上限调高,max_connections=500;同时检查Django CONN_MAX_AGE,设为60秒,让连接能复用。这个坑在开发机上的MySQL默认配置下特别容易踩。
6.5 可视化与大模型问题
问题:ECharts图表不显示,控制台报Cannot read property 'init' of undefined。处理:一般是ECharts的CDN地址被拦了或者本地没引对。最稳妥的是把echarts.min.js下载到项目的static/js目录下,引入本地文件。演示环境断网也能正常展示,细节到位的演示不容易紧张。
问题:大模型API调用有延迟,页面等待太久。处理:不要为每条数据都现场调用一次,给推荐语加缓存字段,数据变化后才重新生成。首次生成后存入数据库,后面直接读缓存结果,响应速度能提升10倍以上。
6.6 项目整体运行的几个提示
放几个我自己总结的、有点“反常识”的建议:
第一,数据库别用默认的SQLite,除非你明确知道自己在做什么,原因我也说了,SQLite对并发写入和复杂分析的支撑确实弱。换MySQL,你论文多写一节“数据库选型与优化”,一举两得。
第二,定时任务别用系统自带的Cron,Windows环境下尤其别扭。用Django的Celery或者APScheduler,一个python manage.py runscript也能跑,网上资料都多的是。
第三,别在答辩前一周改架构。决定Django就全程Django,别中途想着“换成Flask更快”或者“用Vue重写前端更高级”。毕业设计追求的是完整稳定,不是语言大战,稳定的绿点远比花哨但崩溃的界面重要。
第四,演示准备两条路:一是本地环境连数据库和爬虫都跑通,画面展示实时动态;二是准备好静态录屏和截图,防止现场网络、数据库、演示机任何一环出问题。
写在最后的经验
这套系统是我带过的多个学生项目里比较典型的组合,技术链条完整、选题方向好写、增量空间大,所以我认真推荐给正在选题的同学。实际做的时候,给两个优先级建议。
第一个建议是先跑通采集,再做分析和可视化。数据是根,没有采集成功的数据,后面所有的图表和模型都是空的。先把一个城市的景点数据完整采集下来,看到数据库里真的有几条带分数、带评论数、带经纬度的记录,心里的底就踏实了一半。
第二个建议是每天都让系统跑一遍爬虫,哪怕是只跑一个城市十分钟。不给数据过夜,脚本能每天稳定产出增量数据,系统的稳定性就证明了你代码质量,这才是毕设里最有说服力的东西。
行吧,就聊到这儿。这篇内容把采集、存储、分析、可视化、大模型集成的关键细节都过了一遍,照着做下来,你的毕设稳了。如果遇到具体的报错或者想聊某个细节,欢迎在评论区把你的日志贴上来,我看到了会回。