简介:一份基于Python的云南旅游景点数据分析与可视化论文文档,面向计算机技术、旅游信息化相关专业的毕业生、课程设计学生及系统开发人员,可作为毕业设计论文参考或旅游数据平台搭建的入门资料。文档以云南旅游景点数据为对象,完整覆盖从需求分析、技术选型、系统设计到可视化展示的各个环节,包含绪论、开发背景、国内外研究现状、系统开发技术、系统设计与实现、总结等章节。技术方案上选用Python作为主要编程语言,配合Django后端框架与MySQL数据库存储景点数据,开发环境基于Idea平台;系统设计采用前后端分离思路,包含系统首页、用户管理、云南景点管理、旅游资讯发布等模块,并对数据清洗、统计分析与图表可视化等方法进行了说明,给出了功能结构、数据库设计等关键内容。资源包内共1个docx文件,大小约2MB,全文目录与章节结构清晰,便于直接查阅摘要、关键词、开发技术、系统实现及总结等完整内容。目前已有188人学习,适合需要快速获取完整论文框架、核心代码思路与写作样例的读者。
1. 云南旅游景点数据分析与可视化:一份能跑起来的 Django 毕设源码
去年帮一个学弟改毕业设计,他拿到的就是这份题目一模一样的 Word 论文:基于 Python 的云南旅游景点数据分析与可视化系统。论文写得挺完整,从绪论到系统测试都有,但真按论文去复现,问题一堆——数据库连不上、Django 版本对不上、图表画不出来。折腾了两天,我才把整个系统从论文里的「示意图」变成能跑的工程。这份资源的价值不在于论文本身,而在于它把 Python、Django、MySQL 三者串起来,做成了一套带登录、景点管理、资讯发布和可视化大屏的完整 Web 系统。适合正在做课程设计、毕业设计,或者想快速搭一个旅游景点数据展示 Demo 的 Python 学习者。这篇笔记我会把技术选型、建表逻辑、核心代码和踩过的坑全部拆开讲清楚,你能照着复现到本地跑通。
2. 技术选型拆解:Django MTV、MySQL 数据表与 B/S 架构的取舍
2.1 为什么是 Django 而不是 Flask 或 FastAPI
论文里技术栈写的是 Django,这很符合毕业设计的风格:一个框架覆盖后台管理、用户认证、ORM 和模板渲染,不需要像 Flask 那样自己拼第三方库。Django 的 MTV 模式(Model-Template-View)和经典的 MVC 有区别,它把「控制器」的职责交给了 URL 配置和 View 函数——浏览器请求先经过 URL 分发器,匹配到对应 View,View 调用 Model 操作数据库,再把结果丢给 Template 渲染成 HTML 返回。这种松耦合的结构让后续改动很方便。
选型理由很直接:这个系统需要管理员登录、景点信息的增删改查、资讯发布,Django 自带的 admin 后台和 Form 校验机制能省掉大量重复代码。对比 Flask,写一个带权限控制的景点管理模块至少要手动实现 session、CSRF 防护、表单验证,而 Django 全内置。
提示:如果你只是做一个纯 API 后端,那 FastAPI 更合适。但云南旅游景点系统要的是「页面 + 后台 + 数据库」一体的传统 Web 应用,Django 是成本最低的方案。
2.2 B/S 架构与 MySQL 的定位
系统采用 B/S 架构,客户端只需要浏览器。服务器端负责所有业务逻辑,数据库用 MySQL 持久化数据。我实际复现时用的是 MySQL 5.7,是因为 Django 连接 MySQL 需要驱动支持认证插件,MySQL 8.0 默认的 caching_sha2_password 在旧版 mysqlclient 下容易报错,而毕业设计文档一般写 5.5 或 5.7,兼容性更稳。
CREATE DATABASE yunnan_travel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这段建库语句解决了数据表中文乱码的根源。utf8mb4 和 utf8_unicode_ci 能让景点介绍、资讯正文这类长文本字段正常存取。如果不指定字符集,MySQL 默认 latin1,插入中文就会变成问号。
数据库设计上,系统核心是「用户表」「景点表」「资讯表」三张表。景点表包含景点名称、所在州市、门票价格、开放时间、评分、简介等字段,资讯表存旅游动态文章。用户表分了管理员和普通用户两种角色,登录后权限不同。
2.3 开发工具:PyCharm 与依赖管理
论文正文里提到用 IDEA 开发,但实际写 Python 项目,我建议用 PyCharm,社区版足够。项目依赖集中在 requirements.txt:
Django==3.2.25 mysqlclient==2.0.3 pandas==1.5.3 pycharts==1.0.0 PyMySQL==1.0.2注意:最新版 Django(4.x/5.x)对 mysqlclient 的编译环境和 MySQL 版本要求都有变化,这里锁 3.2 系列是求稳。pycharts 是 PyECharts 的旧名,如果你用新版,直接装 pyecharts 就行。
2.4 功能模块梳理
从论文目录能看出,系统主要有四块:系统首页、用户模块(注册登录)、云南景点管理、旅游资讯发布。管理员可以维护景点信息,前台用户能浏览景点和资讯,首页展示统计图表——比如各州市景点数量分布、门票价格区间、热门景点 TOP10。这些图表对应了论文里的「可视化」目标,我后面会在第 4 章给出核心实现方案。
3. 把论文化成工程:环境搭建、数据建模与登录注册实现
3.1 环境准备与项目初始化
先确认本机 Python 版本。Django 3.2 支持 Python 3.6 到 3.10,我用的是 Python 3.8,稳定兼容。打开终端,创建项目:
python -m venv venv # Windows: venv\Scripts\activate macOS/Linux: source venv/bin/activate pip install -r requirements.txt django-admin startproject travel_analysis . python manage.py startapp attractions python manage.py startapp users第一条命令创建虚拟环境,避免和系统全局 Python 包冲突。requirements.txt 里的 Django 版本指定 3.2.25,pip 会自动解析依赖。startproject 后面的
.表示在当前目录生成 manage.py 和项目配置包,这个细节很多人忽略,导致目录结构嵌套错乱。
3.2 settings.py 配置 MySQL 连接
在 travel_analysis/settings.py 里修改数据库配置:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'yunnan_travel', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', } } }这段配置让 Django 通过 ORM 操作 MySQL。USER 和 PASSWORD 改成你本机 MySQL 的实际账号;OPTIONS 里强制指定 charset,是防止连接时编码走了默认 latin1。如果你用的是 PyMySQL,还得在
__init__.py里加上pymysql.install_as_MySQLdb()两行,否则 Django 找不到 MySQLdb 模块。
3.3 数据建模:三张核心表
在 attractions/models.py 里定义景点模型:
from django.db import models class ScenicSpot(models.Model): name = models.CharField(max_length=100, verbose_name='景点名称') city = models.CharField(max_length=50, verbose_name='所属州市') ticket_price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='门票价格') opening_hours = models.CharField(max_length=100, verbose_name='开放时间') rating = models.FloatField(verbose_name='评分', default=0.0) description = models.TextField(verbose_name='景点介绍', blank=True) class Meta: db_table = 'scenic_spot' def __str__(self): return self.name模型类名 ScenicSpot 对应数据库表
scenic_spot。DecimalField 比 FloatField 更适合存门票价格,避免浮点精度误差;rating 字段用于后续可视化分析——按评分排序、画分布直方图。Meta 里 db_table 显式指定表名,不写的话 Django 会自动生成attractions_scenicspot,太丑且和论文表结构对不上。
用户模型用 Django 内置的 AbstractUser 扩展:
from django.contrib.auth.models import AbstractUser class UserProfile(AbstractUser): phone = models.CharField(max_length=11, verbose_name='手机号', blank=True) avatar = models.ImageField(upload_to='avatars/', verbose_name='头像', null=True, blank=True) class Meta: db_table = 'user_profile'直接继承 AbstractUser,复用 Django 自带的登录认证和密码哈希,只额外加手机号和头像两个字段。这是最省事的做法——不用自己写密码校验和 session 逻辑,Django admin 也能直接管用户。
定义完模型后执行迁移:
python manage.py makemigrations python manage.py migrate python manage.py createsuperusermakemigrations 生成迁移文件,migrate 把模型映射成 MySQL 表,createsuperuser 创建管理员账号。这时登录 Django admin 后台,你就能看到 scenic_spot 和 user_profile 两张表的管理入口。
3.4 登录注册的最简实现
用户注册用 Django 的 CreateView 和内置 UserCreationForm:
# users/views.py from django.contrib.auth.forms import UserCreationForm from django.views.generic import CreateView from django.urls import reverse_lazy class RegisterView(CreateView): form_class = UserCreationForm template_name = 'users/register.html' success_url = reverse_lazy('login')CreateView 是 Django 基于类的视图,POST 请求时自动通过 UserCreationForm 校验并保存用户,注册成功跳转到登录页。不需要手动写 if request.method == 'POST' 那套逻辑,代码量直接砍一半。URL 配置加一行
path('register/', RegisterView.as_view(), name='register')就能访问。
4. 数据分析与可视化落地:PyECharts 图表、Pandas 清洗与前端对接
4.1 数据从哪来:CSV 导入与 Pandas 清洗
论文里没有给现成数据集,实际复现时我用 Pandas 构造并清洗了一份云南景点模拟数据。先手工建 CSV,包含景点名、州市、门票、评分、游客量五列,然后写脚本导入数据库:
import pandas as pd from attractions.models import ScenicSpot df = pd.read_csv('yunnan_spots.csv', encoding='utf-8') df = df.dropna(subset=['name', 'city']) df['ticket_price'] = df['ticket_price'].astype(float) for _, row in df.iterrows(): ScenicSpot.objects.create( name=row['name'], city=row['city'], ticket_price=row['ticket_price'], rating=row['rating'], description=row.get('description', '') )这段脚本演示了「先清洗再入库」的流程:dropna 去掉景点名或州市为空的记录,astype(float) 强制转换价格列,避免字符串格式污染数据库。用 iterrows 逐行写入是因为景点数据量一般就几百条,不需要走 bulk_create 批量优化。如果是几万条数据,就用
ScenicSpot.objects.bulk_create(spot_list)一次提交。
4.2 View 层返回聚合数据
可视化图表需要聚合数据,比如「各州市景点数量」。在视图里用 ORM 聚合:
from django.db.models import Count from django.http import JsonResponse from attractions.models import ScenicSpot def city_distribution_api(request): data = list( ScenicSpot.objects.values('city') .annotate(count=Count('id')) .order_by('-count') ) return JsonResponse({'data': data})values('city') 先按城市分组,annotate 用 Count 统计每个城市的景点数,order_by 按数量倒序。返回的 JSON 结构是
[{'city': '大理', 'count': 12}, ...],前端拿到直接塞进 ECharts 柱状图。接口化返回数据的好处是图表和页面解耦,以后想换图表库不用改后端。
4.3 前端对接 ECharts 图表
模板里引入 ECharts,AJAX 拉取上面的接口:
<div id="chart" style="width: 100%; height: 400px;"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script> fetch('/api/city-distribution/') .then(response => response.json()) .then(res => { const chart = echarts.init(document.getElementById('chart')); chart.setOption({ xAxis: { type: 'category', data: res.data.map(item => item.city) }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: res.data.map(item => item.count) }] }); }); </script>这段 JS 是 ECharts 最标准的接入套路:init 初始化实例,setOption 喂数据。xAxis 的 data 来自接口返回的城市列表,series 的 data 对应景点数量。注意 fetch 默认不带 cookie,如果你的接口有登录校验,需要加
credentials: 'same-origin',否则请求会 403。
提示:图表要在页面初始化后再渲染,否则容器宽度为 0 会画成细条。我用 window.onload 包了一下 fetch 逻辑,或者在轮询数据更新时重新 init。
4.4 热门景点排行榜的实现思路
除了柱状图,系统里还能做「游客量 TOP10」横排条形图。SQL 层先算出排序结果,后端只透传:
def top10_api(request): data = list( ScenicSpot.objects.values('name', 'rating') .order_by('-rating')[:10] ) return JsonResponse({'data': data})order_by('-rating') 按评分降序,切片 [:10] 取前十。前端用横向柱状图展示,yAxis 变成 name 字段,series 的 data 变成 rating。横向条形图比纵向柱状图更适合榜单场景,因为景点名称长,纵轴能完整显示。这个接口后面还能直接接到大屏页面,不用改任何代码。
5. 避坑实录:MySQL 驱动、中文编码、静态资源与分页丢参
5.1 Django 连 MySQL 一直报错,找不到 MySQLdb 模块
- 现象:执行
python manage.py migrate时报ModuleNotFoundError: No module named 'MySQLdb'。 - 原因:Django 默认用 MySQLdb 驱动,但这个库在 Windows 上编译很麻烦,大部分人会装 PyMySQL 替代,却忘了告诉 Django。
- 解决:在项目
__init__.py里加两行import pymysql; pymysql.install_as_MySQLdb(),PyMySQL 会伪装成 MySQLdb。另一个方案是直接用 mysqlclient——但 Windows 下你大概率装不上,需要下载预编译 whl 包,所以我推荐 PyMySQL 方案。
5.2 插入景点介绍后读出来是乱码
- 现象:后台填了中文描述,前台展示全是问号或者方块。
- 原因:数据库表字符集不是 utf8mb4。历史遗留的建表语句可能是
CHARACTER SET utf8,而 utf8 在 MySQL 里实际是 utf8mb3,存不了四字节的 emoji 和部分生僻字。 - 解决:建库时强制指定
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,连接串和 settings.py 里也要带charset: utf8mb4。如果已经建了表,用ALTER TABLE scenic_spot CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;补救。
5.3 前端图表显示 404,ECharts 加载不出来
- 现象:页面打开,图表区域空白,控制台报
GET /static/js/echarts.min.js 404。 - 原因:Django 默认不帮你管理静态资源路径。我把 ECharts 的 JS 文件直接放在
static/js/下,但 settings.py 里没配STATICFILES_DIRS,Django 不知道去哪找。 - 解决:在 settings.py 里补一个元组:
STATICFILES_DIRS = [ BASE_DIR / 'static', ]这样模板里
{% load static %}才能正确解析路径。还有个坑:如果用 CDN 链接,内网环境下加载很慢甚至失败,建议把 echarts.min.js 下载到本地 static 目录,部署时也能离线访问。
5.4 列表页分页后,翻到第 2 页筛选条件失效
- 现象:景点列表按城市筛选后点「下一页」,结果筛选条件没了,看到全量数据。
- 原因:分页链接只写了
?page=2,没有把当前的城市筛选参数拼进去。 - 解决:在模板里动态拼接请求参数。
# views.py from django.core.paginator import Paginator from django.db.models import Q def spot_list(request): city = request.GET.get('city', '') spots = ScenicSpot.objects.all() if city: spots = spots.filter(city=city) paginator = Paginator(spots, 10) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'spots/list.html', { 'page_obj': page_obj, 'city': city, }){# 模板里分页链接 #} <a href="?city={{ city }}&page={{ page_obj.next_page_number }}">下一页</a>核心逻辑是把 city 参数显式传到模板,分页 URL 再用
city+page拼接。同理,如果你还有排序参数、关键字搜索,都要在视图里手动收集并回传给模板。凡是「翻页丢条件」的问题,90% 都是这个原因。
5.5 管理员后台上传图片不显示
- 现象:用户头像上传成功后,页面上的 img 标签裂开。
- 原因:MEDIA_URL 和 MEDIA_ROOT 没有配置,Django 开发环境也不自动路由媒体文件。
- 解决:settings.py 配置
MEDIA_URL = '/media/'和MEDIA_ROOT = BASE_DIR / 'media',然后项目级 urls.py 加一段:
from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)这段只在 DEBUG=True 时生效。线上部署用 Nginx 代理,media 目录要单独配 alias,不能依赖 Django 自己服务文件——生产环境跑这个会有性能和安全隐患。我当年在这上面翻过车,部署到服务器后图片全挂。
6. 验证与进阶:从功能测试到 uWSGI+Nginx 上线部署
6.1 给系统写一个冒烟测试脚本
论文里系统测试写了登录、增删改查,但没给测试代码。我复现时用 Django LiveServerTestCase 跑了几个最关键的路径:
# attractions/tests.py from django.test import LiveServerTestCase from selenium import webdriver class SmokeTest(LiveServerTestCase): def setUp(self): self.driver = webdriver.Chrome() def test_home_page_loads(self): self.driver.get(self.live_server_url + '/') self.assertIn('云南旅游景点', self.driver.title)这是比较重的端到端验证,需要本机有 ChromeDriver。日常改完代码先跑
python manage.py test验证 ORM 和视图逻辑,再决定要不要起 Selenium。冒烟测试跑通过,说明页面资源、数据库连接、模板渲染三层都没问题——这是我每次交付前必走一遍的习惯。
6.2 部署到云服务器的关键动作
本地跑通只是第一步,上生产环境才是完整链路。我习惯用 uWSGI + Nginx:
pip install uwsgi uwsgi --http :8000 --module travel_analysis.wsgi --chdir /path/to/projectuWSGI 负责把 HTTP 请求转给 Django,Nginx 负责静态资源和负载均衡。真实部署时还要处理 setting 里的
DEBUG=False,这个开关一关,Django 就不再提供静态资源服务——必须由 Nginx 接管。我一般会配 Nginx 的 location 块分别指向 static 和 media 目录。
6.3 一个值得加的功能:游客量预测模块
论文绪论里花了大篇幅聊游客量预测模型,但正文系统功能里没有落地。如果你想让这套系统在答辩时更有亮点,可以加一个简单的 ARIMA 预测接口:
from statsmodels.tsa.arima.model import ARIMA def forecast_api(request, spot_id): history = list(VisitRecord.objects.filter(spot_id=spot_id) .values_list('visitor_count', flat=True)) model = ARIMA(history, order=(1, 1, 1)) fit = model.fit() forecast = fit.forecast(steps=7) return JsonResponse({'forecast': forecast.tolist()})数据模型里加一张 VisitRecord 表,记录每月游客量,ARIMA(1,1,1) 是最常见的参数组合。这个功能把论文里综述部分的「人工智能预测」落到代码上,答辩时能直观展示「不仅做了可视化,还能预测未来趋势」。如果历史数据少于 30 条,ARIMA 会报不足样本的警告,预测漂移也会比较明显——数据量不够时说明这是功能扩展方向即可。
从那以后我每次给学弟学妹调这种 Django 毕设,都会强制走一遍「装依赖 → 建库 → 迁移 → 跑测试 → 起服务」的流程,先确认环境干净,再进入业务代码排查。很多所谓「系统跑不起来」的问题,根源就是数据库连接和静态资源配置这两处没弄对。这份资源的价值在于把 Python 数据分析、Django Web 开发和可视化整合成了一个闭环,按我上面这些步骤操作,你少踩一半坑。希望帮到你。
本文还有配套的精品资源,点击获取