先想一个常被问的问题:拿到“Python旅游景点信息可视化系统”这类题目,很多人的第一反应是上网找一套源码,改改名字、换换图片,然后交差。这个做法短期应付可以,但一旦被问到“表结构怎么设计的”“图表数据从哪来”“大模型接口怎么对接的”,很容易露馅。
更值得做的思路,是把这套系统当成一条完整的数据链路来理解:数据采集、清洗入库、Django 业务层、可视化展示、大模型智能问答。每一层都有明确的技术选型和踩坑点。这篇文章就按这条链路展开,把环境搭建、数据清洗、模型设计、图表渲染、DeepSeek 接入和常见排查一次讲透,帮你真正跑通一个能演示、能说清原理的景点信息可视化系统。
需要说明的是,本文涉及的爬虫采集只适用于公开、合法授权的数据来源,不建议采集任何带有版权限制或用户隐私的内容;涉及生产部署、数据库操作时,务必在个人测试环境验证,并做好备份。
1. 这个系统到底在解决什么问题
旅游景点信息有个典型特点:数据分散且非结构化。携程有评分、马蜂窝有游记、高德有地理坐标、景区官网有开放时间,但这些信息互相割裂。游客想规划一条线路,往往要开五六个页面来回比对;景区运营方想了解游客偏好,也只能靠零星问卷。
这个可视化系统要解决的,就是把分散的景点数据统一到一个平台里,再通过两个出口把数据价值释放出来:
第一是可视化出口。通过柱状图、饼图、地图散点、词云等方式,把“哪些城市景点最多”“哪些景区评分最高”“游客人均消费集中在什么区间”这类问题直观呈现。第二是智能问答出口。用户用自然语言提问,比如“北京哪些景点评分最高”,系统先基于本地景点数据库做检索,再由大模型生成回答,这样模型不会凭空编造,回答有数据支撑。
从技术栈看,Django 在这里的角色不只是网站框架,而是整个系统的“数据中枢”。它负责数据建模、业务查询、JSON 接口输出,同时也作为大模型调用和前端图表渲染之间的桥梁。这也是为什么推荐用 Django 而不是 Flask:Django 自带 ORM、Admin 后台、迁移机制,对数据密集型系统来说,少写很多底层代码。
2. 系统整体架构与核心概念
2.1 分层架构
一个标准的旅游景点信息可视化系统,建议分成四层:
| 层级 | 职责 | 涉及技术 |
|---|---|---|
| 数据层 | 数据采集、清洗、存储 | requests、pandas、SQLite/MySQL |
| 业务层 | 数据处理与接口输出 | Django ORM、Django REST Framework |
| 展示层 | 可视化图表与页面交互 | ECharts、Bootstrap、jQuery |
| 智能层 | 大模型问答与推荐 | DeepSeek API、Agent 编排 |
层与层之间通过明确的数据结构连接。数据层产出标准化的景点数据表;业务层通过 ORM 查询后,序列化成 JSON;展示层拿到 JSON 后渲染图表;智能层在调用大模型之前,先从数据库查询结果拼装上下文。
2.2 Django 的 MVT 设计模式
MVT 是 Django 的核心设计模式,很多初学者容易把它和 MVC 搞混。理解它的关键在于数据流向:
- Model:定义数据结构,对应数据库表。比如景点表、城市表、评论表。
- View:接收请求、处理业务逻辑、返回响应。这里的 View 对应 MVC 中的 Controller。
- Template:负责渲染 HTML 页面。对应 MVC 中的 View。
用户请求一个 URL,Django 先通过 URLconf 找到对应的 View 函数,View 从 Model 中查询数据,再把数据传递给 Template 渲染成页面返回给用户。这个流程理解透了,后面写接口和页面会非常顺手。
2.3 数据挖掘与可视化在本项目中的边界
项目标题出现“数据挖掘”,但实际毕设场景中不需要做复杂的机器学习算法。真正有价值的数据挖掘工作集中在三块:
- 数据清洗:处理缺失值、重复值、异常值。
- 统计聚合:按城市、按评分区间、按消费水平分组统计。
- 关联分析:分析景点类型与评分之间的关系。
可视化则是这些分析结果的最直观表达方式。没有分析的数据是死数据,没有可视化的分析不便于理解。两者配合,才让系统有了“数据分析”的属性。
3. 环境准备与依赖安装
3.1 版本选型原则
项目开发中,版本并不是越新越好。Django 的版本迭代非常频繁,某些第三方库可能还没有适配最新版本。更稳妥的做法是选择一个自己熟悉的稳定版本,并锁定关键依赖版本号。本文演示以 Django 4.x 的通用思路为准,你本地的实际版本以你创建项目时安装的为准。
推荐使用虚拟环境,避免全局安装导致依赖冲突。
3.2 创建虚拟环境与安装依赖
以下以 Windows 系统为例,macOS/Linux 命令基本相同。
# 创建虚拟环境 python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(macOS/Linux) source venv/bin/activate # 安装核心依赖 pip install django pip install pandas pip install requests pip install mysqlclient如果使用 MySQL,需要安装mysqlclient或者使用pymysql(需要在__init__.py中手动pymysql.install_as_MySQLdb())。如果使用 SQLite,则不需要额外安装数据库驱动,直接使用 Django 内置配置即可。
3.3 创建 Django 项目与应用
# 创建项目,项目名不受限 django-admin startproject travel_vis # 进入项目目录 cd travel_vis # 创建应用 python manage.py startapp spots创建完成后可以在settings.py的INSTALLED_APPS中加入spots:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'spots', ]4. 景点数据获取与数据清洗
4.1 数据来源方式
景区数据的合法来源主要有三类:
第一,开源数据集网站。如 Kaggle、DataFountain 等平台上的旅游相关公开数据,通常有明确授权说明。第二,政府开放数据平台。部分省市会发布 A 级景区名录、旅游收入等数据,这类数据权威且可自由使用。第三,自有爬虫采集。只针对公开页面,且必须遵守网站的 robots 协议,控制请求频率,不采集用户隐私数据。
对于演示项目,可以先用一份 CSV 格式的公开景点数据作为初始数据源,字段大致包括:景点名称、城市、省份、评分、门票价格、景点类型、简介、经度、纬度、热门程度。如果暂时拿不到足够真实的数据,也可以自己构造一份标准化的演示数据,重点在于跑通流程而不是追求数据量级。
4.2 pandas 清洗数据示例
拿到原始数据后,第一步不是急着入库,而是先用 pandas 做清洗。这里给出一个相对完整的清洗示例:
# 文件路径:scripts/clean_data.py import pandas as pd # 读取原始数据 raw_df = pd.read_csv('data/raw_spots.csv', encoding='utf-8') # 去除完全重复的行 raw_df = raw_df.drop_duplicates() # 去除关键字段缺失的行 raw_df = raw_df.dropna(subset=['name', 'city']) # 填充数值型字段缺失值:评分使用平均值填充 raw_df['rating'] = raw_df['rating'].fillna(raw_df['rating'].mean()) # 统一评分为 0-5 分制,超过 5 按 5 分处理 raw_df['rating'] = raw_df['rating'].clip(0, 5) # 门票价格转数值类型 raw_df['ticket_price'] = pd.to_numeric(raw_df['ticket_price'], errors='coerce').fillna(0) # 去掉简介中的换行和多余空格 raw_df['description'] = raw_df['description'].str.replace('\n', '').str.strip() # 保留筛选后的字段 columns = ['name', 'city', 'province', 'rating', 'ticket_price', 'type', 'description', 'longitude', 'latitude'] clean_df = raw_df[columns] # 输出清洗后的数据 clean_df.to_csv('data/clean_spots.csv', index=False, encoding='utf-8') print('清洗完成,共保留 {} 条景点数据'.format(len(clean_df)))这段代码里有几个细节值得注意:
drop_duplicates()默认去除所有列完全重复的数据。如果部分数据存在同城同名但信息不同的情况,建议按['name', 'city']作为去重子集,保留更完整的那条记录。clip(0, 5)用于处理因为不同数据源评分口径不一致导致的异常值,比直接用 replace 更保险。errors='coerce'会把无法转换的值变为 NaN,再用fillna(0)处理,逻辑上更清晰。
5. Django 模型设计与数据导入
5.1 模型字段设计
数据清洗完成后,需要设计 Django ORM 模型。一个基础但完整的景点信息表应该覆盖三类信息:基础属性、地理信息和业务统计字段。
# 文件路径:spots/models.py from django.db import models class City(models.Model): name = models.CharField(max_length=50, unique=True, verbose_name='城市名称') province = models.CharField(max_length=50, verbose_name='省份') class Meta: verbose_name = '城市' verbose_name_plural = '城市' def __str__(self): return self.name class Spot(models.Model): name = models.CharField(max_length=100, verbose_name='景点名称') city = models.ForeignKey(City, on_delete=models.CASCADE, related_name='spots', verbose_name='所属城市') rating = models.FloatField(default=0, verbose_name='评分') ticket_price = models.FloatField(default=0, verbose_name='门票价格') spot_type = models.CharField(max_length=50, blank=True, verbose_name='景点类型') description = models.TextField(blank=True, verbose_name='简介') longitude = models.FloatField(null=True, blank=True, verbose_name='经度') latitude = models.FloatField(null=True, blank=True, verbose_name='纬度') # 该字段可以由评论数量统计而得,避免每次实时计算 popularity = models.IntegerField(default=0, verbose_name='热度值') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: verbose_name = '景点' verbose_name_plural = '景点' ordering = ['-rating'] def __str__(self): return self.name几个设计上的考虑说明一下:
第一,城市单独建模,而不是直接在景点表里存一个字符串。好处是查询“某城市所有景点”时走外键关联,性能更好,数据一致性也更高。第二,on_delete=models.CASCADE表示删除城市时级联删除其下的景点,在测试环境很方便,但生产环境如果要保留历史数据,建议改成PROTECT或SET_NULL。第三,热度值单独存储而不是实时计算,属于典型的空间换时间思路。
5.2 批量导入脚本
有了模型和数据文件,写一个 Django 管理命令脚本将清洗后的数据导入数据库。推荐使用自定义 management command 而不是在脚本中手动写 ORM 逻辑,方便复用。
# 文件路径:spots/management/commands/import_spots.py import csv from django.core.management.base import BaseCommand from spots.models import City, Spot class Command(BaseCommand): help = '从清洗后的 CSV 文件导入景点数据' def handle(self, *args, **options): # 清空旧数据,方便重复导入测试 Spot.objects.all().delete() City.objects.all().delete() file_path = 'data/clean_spots.csv' city_cache = {} with open(file_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) count = 0 for row in reader: city_name = row['city'].strip() if city_name not in city_cache: city_obj, _ = City.objects.get_or_create( name=city_name, defaults={'province': row.get('province', '').strip()} ) city_cache[city_name] = city_obj else: city_obj = city_cache[city_name] Spot.objects.create( name=row['name'].strip(), city=city_obj, rating=float(row.get('rating') or 0), ticket_price=float(row.get('ticket_price') or 0), spot_type=row.get('type', '').strip(), description=row.get('description', '').strip(), longitude=float(row.get('longitude') or 0), latitude=float(row.get('latitude') or 0), popularity=int(float(row.get('popularity') or 0)), ) count += 1 self.stdout.write(self.style.SUCCESS('成功导入 {} 条景点数据'.format(count)))这里每次测试导入时先清空旧数据,是为了保证可重复执行。如果是生产环境,一定要去掉这行删除逻辑,改用update_or_create做增量更新。
# 执行迁移和导入 python manage.py makemigrations python manage.py migrate python manage.py import_spots在导入过程中如果报错,优先排查 CSV 字段名是否与DictReader读取的 header 完全一致。比如系统里字段名是type,但 CSV 里写的是景点类型,就会导致读不到值。
6. 数据分析与可视化实现
6.1 分析维度的选取
景点数据可视化不是把图表堆在页面上就行了,每一个图表都要回答一个具体问题。推荐先从这四个维度入手:
- 城市景点数量 Top10:回答“哪里旅游资源最丰富”。
- 各景点评分分布:回答“景区整体口碑如何”。
- 门票价格区间分布:回答“游客的消费门槛集中在哪个区间”。
- 景点类型分布:回答“当地热门景点以自然风光还是人文景观为主”。
四个维度做下来,页面既不会太空,也不会因为图表过多导致开发压力过大。
6.2 使用 ECharts 渲染图表
ECharts 是目前最适合做数据可视化的前端库之一,图表交互流畅,文档完善,引入方式也非常灵活。这里演示如何通过 Django 视图输出 JSON 数据,然后在前端页面渲染一个柱状图。
后端视图代码:
# 文件路径:spots/views.py import json from django.db.models import Count from django.http import JsonResponse from django.shortcuts import render from spots.models import City, Spot def dashboard(request): return render(request, 'spots/dashboard.html') def city_spot_stats(request): """统计城市景点数量 Top10""" cities = City.objects.annotate(spot_count=Count('spots')).order_by('-spot_count')[:10] data = { 'categories': [city.name for city in cities], 'values': [city.spot_count for city in cities], } return JsonResponse(data)路由配置:
# 文件路径:travel_vis/urls.py from django.urls import path from spots import views urlpatterns = [ path('admin/', admin.site.urls), path('', views.dashboard, name='dashboard'), path('api/city-spot-stats/', views.city_spot_stats, name='city_spot_stats'), ]前端模板:
<!-- 文件路径:templates/spots/dashboard.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>旅游景点数据看板</title> <!-- 通过静态文件加载 ECharts --> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <div id="cityChart" style="width: 800px; height: 400px;"></div> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script> <script> $(function () { $.get('/api/city-spot-stats/', function (data) { var chart = echarts.init(document.getElementById('cityChart')); chart.setOption({ title: { text: '城市景点数量 Top10' }, tooltip: {}, xAxis: { type: 'category', data: data.categories }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.values, itemStyle: { color: '#3398DB' } }] }); }); }); </script> </body> </html>这个过程中最容易遇到的问题有两个:
第一个是静态文件加载失败。开发阶段把 ECharts 放到本地 static 目录也是可以的,但需要先在 settings.py 中配置 STATIC_URL 和 STATICFILES_DIRS。使用 CDN 是一个简便的选择,但部署到内网环境时需要提前下载到本地。第二个是 JSON 接口跨域问题。Django 项目模板默认对同源请求没有跨域限制,但如果你前后端分离部署(前端 8080 端口,Django 8000 端口),就需要使用django-cors-headers库配置允许跨域来源。
6.3 编写其它统计查询
数据可视化的核心是查询能力。多写几个统计查询,页面内容就丰富起来了。
# 文件路径:spots/views.py(追加) def rating_distribution(request): """统计评分分布区间""" values = [] ranges = [(0, 2), (2, 3.5), (3.5, 4.2), (4.2, 4.6), (4.6, 5.0)] labels = ['低分区(0~2)', '一般区(2~3.5)', '良好区(3.5~4.2)', '优秀区(4.2~4.6)', '高分区(4.6~5.0)'] data = [] for r in ranges: count = Spot.objects.filter(rating__gte=r[0], rating__lt=r[1]).count() data.append(count) return JsonResponse({'labels': labels, 'values': data}) def price_distribution(request): """统计门票价格区间分布""" from django.db.models import Case, When, Value, IntegerField queryset = Spot.objects.annotate( price_group=Case( When(ticket_price=0, then=Value(0)), When(ticket_price__lt=50, then=Value(1)), When(ticket_price__lt=100, then=Value(2)), When(ticket_price__lt=200, then=Value(3)), default=Value(4), output_field=IntegerField(), ) ).values('price_group').annotate(count=Count('id')).order_by('price_group') labels = ['免费', '0~50元', '50~100元', '100~200元', '200元以上'] data = [0] * 5 for item in queryset: data[item['price_group']] = item['count'] return JsonResponse({'labels': labels, 'values': data})利用Case/When做区间统计,是 Django ORM 的进阶用法。它把 SQL 的 CASE WHEN 翻译成 Python 代码,比在 Python 内存里 groupby 更高效,数据量大了以后优势更明显。
7. 大模型接入:基于 DeepSeek 的景点智能问答
7.1 为什么要在可视化系统里接入大模型
很多人不理解:可视化系统已经能展示数据了,为什么还要接大模型?
从用户视角看,图表是“被动”的:用户需要自己看图、读数据、得出结论。大模型问答则是“主动”的:用户直接提问,系统返回自然语言答案。两者的使用门槛和体验完全不同。尤其在做课程设计或项目演示时,一个“能回答问题的系统”比一组静态图表更能说明项目的完整性和技术深度。
从技术视角看,Django 后端调用大模型 API 并不复杂,关键是设计好“检索增强”的结构,让大模型基于本地数据库内容回答,而不是凭空发挥。
7.2 Agent 在这个场景里的落地方式
Agent 并不是一个神秘的框架,它的核心思想是:把一个大任务拆解成几个步骤,每一步让模型或程序执行,再根据结果决定下一步。在景点问答场景里,一个最简的 Agent 流程可以这样设计:
- 接收用户自然语言问题。
- 在景点数据库中检索相关景点信息,获得候选结果。
- 将候选结果拼装进提示词模板。
- 调用 DeepSeek 生成最终回答。
- 返回给前端展示。
这个流程本质上就是 RAG(检索增强生成)的简化版。它的好处是,答案里的每个景点名称、评分、城市都有本地数据作为依据,大模型只负责组织和润色语言,极大减少了幻觉问题。
7.3 后端 API 调用实现
首先安装openaiPython SDK,因为 DeepSeek 的接口兼容 OpenAI 格式:
pip install openai需要说明的是,DeepSeek 官方 API 使用https://api.deepseek.com作为 base_url,模型名称以deepseek-chat这类实际存在的模型名为准,具体请以官方文档为准。以下是调用示例:
# 文件路径:spots/services.py import os from openai import OpenAI from spots.models import City, Spot client = OpenAI( api_key=os.environ.get("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com" ) def search_related_spots(question): """根据问题关键词在数据库中检索相关景点""" keywords = [kw.strip() for kw in question.split() if kw.strip()] # 优先按城市匹配,再按景点名称匹配 qs = Spot.objects.all() if keywords: query = None for kw in keywords: if City.objects.filter(name__contains=kw).exists(): qs = qs.filter(city__name__contains=kw) else: qs = qs.filter(name__contains=kw) return qs[:5] def build_prompt(question, spots): spot_lines = [] for idx, spot in enumerate(spots, 1): spot_lines.append( f"{idx}. {spot.name}(所属城市:{spot.city.name},评分:{spot.rating}," f"门票:{spot.ticket_price}元,类型:{spot.spot_type},简介:{spot.description[:50]})" ) context = '\n'.join(spot_lines) prompt = f"""你是一个旅游助手。请根据以下数据库检索到的景点信息,回答用户的问题。 数据库结果: {context} 用户问题:{question} 请用简洁、口语化的中文回答。如果检索结果无法回答问题,直接说信息不足。""" return prompt def ask_deepseek(question): spots = search_related_spots(question) prompt = build_prompt(question, spots) response = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是一个严谨、可靠的旅游助手。"}, {"role": "user", "content": prompt} ], temperature=0.3, max_tokens=500 ) return response.choices[0].message.content注意事项:
第一,API Key 绝不能硬编码在项目里。建议使用os.environ.get("DEEPSEEK_API_KEY")读取环境变量,或在本地使用.env文件管理(确保.env不提交到版本库)。一旦密钥泄露到公开仓库,别人就可以用你的额度调用接口。
第二,temperature=0.3是刻意设置的低参数。问答场景我们希望答案稳定、忠实于数据,不希望模型太有“创意”。
第三,提示词里先给检索数据,再给用户问题。这个顺序很重要,模型会优先关注上下文里的数据,减少幻觉概率。
在 Django 视图中调用:
# 文件路径:spots/views.py(追加) from django.views.decorators.http import require_POST from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from spots.services import ask_deepseek @csrf_exempt @require_POST def ai_answer(request): import json body = json.loads(request.body) question = body.get('question', '') if not question: return JsonResponse({'error': '问题不能为空'}, status=400) try: answer = ask_deepseek(question) return JsonResponse({'answer': answer}) except Exception as e: return JsonResponse({'error': str(e)}, status=500)@csrf_exempt在测试阶段可以方便地用 Postman 调试,但生产环境不建议全局关闭 CSRF,更推荐在前端页面通过模板变量获取 CSRF token,并放在请求头中。
7.4 前端问答输入框
<!-- 在 dashboard.html 中追加 --> <div> <input type="text" id="question" placeholder="例如:北京有哪些高分景点?" /> <button id="askBtn">提问</button> </div> <div id="answerBox" style="margin-top: 10px; padding: 10px; border: 1px solid #ddd; min-height: 60px;"></div> <script> $('#askBtn').click(function () { var question = $('#question').val(); if (!question) return; $('#answerBox').text('思考中……'); $.ajax({ url: '/api/ai-answer/', type: 'POST', contentType: 'application/json', data: JSON.stringify({question: question}), success: function (data) { $('#answerBox').text(data.answer); }, error: function (xhr) { $('#answerBox').text('请求失败:' + xhr.responseJSON.error); } }); }); </script>路由添加一行即可:
path('api/ai-answer/', views.ai_answer, name='ai_answer'),8. 运行流程与效果验证
8.1 完整启动步骤
第一次从零开始运行整个项目,建议按下面的顺序操作,每一步都确保能看到预期结果再进行下一步:
# 1. 激活虚拟环境 venv\Scripts\activate # Windows # source venv/bin/activate # macOS/Linux # 2. 执行数据库迁移 python manage.py makemigrations python manage.py migrate # 3. 导入清洗后的数据 python manage.py import_spots # 4. 启动开发服务器 python manage.py runserver启动成功后,浏览器访问http://127.0.0.1:8000/,能看到数据看板页面。
访问http://127.0.0.1:8000/admin/,使用创建超级管理员账号登录:
python manage.py createsuperuser登录后可以在管理后台看到景点表和城市表的数据,也可以直接在后台编辑数据,方便调试。
8.2 如何确定系统运行正确
按这个顺序验证:
- 数据导入成功。运行
import_spots命令后,终端显示导入条数,在 Django Admin 后台能看到对应记录。 - 首页图表加载。进入
dashboard页面,四个图表正常渲染,鼠标悬停有 tooltip 提示。 - JSON 接口正常。直接访问
http://127.0.0.1:8000/api/city-spot-stats/,浏览器返回 JSON 数据。 - 大模型问答正常。输入问题后,先显示“思考中……”,然后在 3-10 秒内返回 AI 生成的回答。
- 异常处理正常。输入空问题提交,返回
{"error": "问题不能为空"}且状态码 400。
如果某个环节失败,优先从数据流的角度排查:接口返回了什么?JSON 结构是否和前端预期一致?API 调用是否报错?报错信息停在哪个层面?
8.3 不同数据量下的表现
如果导入的数据量很小(比如只有 20 条),图表依然会渲染,但各类型的分布会比较单一。建议演示时至少准备 200 条以上的数据,页面效果和说服力会好很多。通过 pandas 造数时,注意分布尽量均匀:比如评分集中在 3.5~4.5 之间,价格集中在免费到 100 元区间,这样图表看起来更接近真实情况。
9. 常见问题与排查方法
9.1 依赖与数据导入阶段
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
pip install mysqlclient报错 | 缺少编译工具或对应依赖 | 查看报错中的 gcc/cl.exe 提示 | 改用 SQLite 开发,或安装对应系统依赖后再试 |
python manage.py migrate报错 | 数据库已存在旧表或有不一致迁移记录 | 查看完整 traceback,定位到具体 app 的迁移文件 | 开发环境可删库重建,生产环境务必走备份和回滚流程 |
| 导入 CSV 后中文乱码 | CSV 编码不是 UTF-8 | 用记事本或 VSCode 查看文件编码 | 统一保存为 UTF-8 编码,读取时指定encoding='utf-8' |
| 导入后记录条数与 CSV 行数不一致 | 清洗时drop_duplicates或dropna删除了记录 | 在清洗脚本中打印每步删除了多少行 | 确认清洗规则是否符合预期,再重新导入 |
9.2 Django 运行阶段
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 页面能打开但图表空白 | JS 报错或接口返回结构不对 | 打开浏览器 F12 控制台查看报错,直接访问接口地址 | 对比接口返回 JSON 与前端setOption所需数据是否一致 |
| 静态文件 404 | STATIC_URL 或静态文件路径配置错误 | 检查浏览器网络请求状态,确认文件是否存在于 static 目录 | 开发环境启用django.contrib.staticfiles,生产环境使用 collectstatic |
| 大模型 API 调用超时 | 网络不稳定或模型响应慢 | 查看后端日志的超时时间;先单独测试 API | 设置合理的超时参数,调用时用异步任务或在前端提示响应时间 |
| 大模型回答仍出现编造信息 | 检索上下文不充分或提示词约束不够 | 打印build_prompt生成的完整上下文 | 增加检索结果条数,增强“只能根据数据库结果回答”的提示 |
| Postman 调试接口提示 CSRF 校验失败 | 请求没有附带 CSRF token | 查看请求头缺少X-CSRFToken字段 | 测试阶段使用@csrf_exempt,正式前后端分离建议用 Token 认证 |
9.3 性能与安全方面
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 景点数据有几万条,图表加载变慢 | 统计分析接口每次实时全表聚合 | 查看 Django Debug Toolbar 的 SQL 执行时间 | 提前在数据导入阶段算好统计结果,存 Redis 或建立索引 |
| 大模型 API 密钥出现在前后端代码中 | 密钥硬编码或提交到版本库 | 用git log和扫描工具检查仓库历史 | 重置密钥;使用环境变量;将.env加入.gitignore |
10. 最佳实践与工程建议
10.1 数据处理与安全
数据是这个系统最核心的资产,处理起来要谨慎。采集阶段只选择明确开放授权的数据源,不使用违反条款的采集方法,不采集任何个人隐私数据。清洗阶段保留一份原始数据备份,清洗逻辑记录在脚本中,这样后续数据源更新时,可以直接重跑脚本而不是手动改数据库。
涉及数据库操作建议遵循最小权限原则。开发环境可以用 root 账号图省事,生产环境务必创建专用账号,只授予业务库的 SELECT、INSERT、UPDATE、DELETE 权限,不要把 DDL 权限开放给应用账号。删除数据前先在备份库验证,涉及批量清理时使用事务包住操作,rollback是最后的兜底手段。
10.2 配置管理
项目中有三类配置一定要区分管理:
- 业务配置:如每页展示条数、图表配色、默认城市。放在 Django settings.py 中即可。
- 环境敏感配置:如数据库密码、API 密钥、SECRET_KEY。必须放在环境变量或本地
.env文件中,绝不提交到版本库。 - 部署配置:如端口号、静态文件路径。放在部署脚本或 docker-compose.yml 中。
10.3 日志与监控
生产环境的 Django 项目,日志配置决定排查问题的效率。建议在 settings.py 中加入简单的日志配置:
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'file': { 'level': 'INFO', 'class': 'logging.FileHandler', 'filename': 'logs/travel.log', }, }, 'root': { 'handlers': ['file'], 'level': 'INFO', }, }使用logging.getLogger(__name__)在视图中记录关键操作,比如导入批次、大模型调用成功与失败次数、异常堆栈。日志文件建议使用 logrotate 按天切割,避免单个文件过大。
10.4 大模型接入的安全边界
大模型接口是一个独立的外部系统,接入时必须意识到它不在你的安全边界内。生产环境调用时注意这几点:API 密钥由后端持有,前端只负责提交问题和展示答案;对用户输入做长度校验,防止构造极长提示词消耗 token;在大模型返回结果展示给用户前,增加简单的敏感内容过滤,避免生成内容存在合规风险。
另外,大模型 API 的调用成本和延迟通常远高于普通业务接口。如果有多人同时访问,建议加一层本地缓存,命中相同问题时直接返回缓存结果,减少重复调用。
10.5 团队协作与版本管理
哪怕是一个人开发,也推荐从项目初始化开始就用 Git 管理。提交信息按功能模块写清楚,比如feat: 添加景点导入命令、fix: 修复评分统计区间边界。依赖使用requirements.txt锁定版本:
pip freeze > requirements.txt.gitignore至少包含以下内容:
venv/ __pycache__/ *.pyc .env logs/ data/raw_spots.csv11. 总结与后续学习方向
这篇内容用一个完整的旅游景点信息可视化项目,把 Django 开发链路走了一遍:从数据清洗入库,到 ORM 模型设计,到 ECharts 可视化展示,再到 DeepSeek 大模型问答接入。里面没有特别高深的技术,但每一步都有真实的取舍,比如为什么城市单独建模、为什么要做 Case/When 区间统计、为什么提示词要先给数据再给问题。这些细节决定系统能否稳定跑起来,也决定你在答辩或面试时能不能把逻辑讲清楚。
如果后续想继续深入,有三个方向值得投入:第一个是推荐系统,在现有数据基础上,根据用户浏览历史和城市偏好给出景点推荐排序;第二个是地理可视化,用高德地图 API 或者 Leaflet 把景点坐标散点叠加到地图上,具备更直观的空间信息表达;第三个是用户行为分析,采集用户的搜索与浏览记录,结合大模型做更个性化的问答推荐。
做任何扩展之前,先把当前这版跑顺,然后挑一个方向,在测试环境做好备份后逐步迭代。数据系统的价值不在于技术栈多新,而在于每一层数据是否可靠、链路是否清晰,以及是否需要时能讲清楚背后逻辑。