
又到毕业设计选题和开题的高峰期我陆续看了不少基于Python的新能源汽车数据分析可视化系统方向的初稿。多数同学的问题不是不会写代码而是把这门课学的爬虫、Django、可视化、大模型四个关键词当成四门独立的课硬塞进一个系统里爬虫爬完落一个CSVDjango单独写几个页面图表库堆一个面板最后在PPT里写一句接入AI大模型就当作创新点。答辩时评委问一句爬虫数据和图表数据是怎么流转的大模型到底在哪一个环节起作用当场就答不出来了。这篇文章我就按一个真正能跑、能演示、能讲清楚链路、还能给后续扩展留余地的完整方案把数据采集→数据存储→后端接口→可视化看板→AI大模型问答这条线完整讲一遍。内容适合正在做毕业设计的本科生也适合想用爬虫加数据分析练手、或者想给已有Django项目加一个智能化交互模块的开发者。你不用照抄我的代码重点是理解每个模块之间为什么这么衔接以及哪些地方是评委一眼就能看出做过和抄过的分水岭。1. 选题背后为什么汽车数据可视化大模型能拿高分1.1 评委看一道毕业设计时到底在看什么带毕设这些年我复盘过不少答辩现场。评委其实不会指望一个本科生做出工业级系统他们判断一个题目好坏核心就四件事工作量是否可验证代码能不能跑数据库里有没有真实能查的数据而不是贴一堆截图。技术链路是否完整数据从哪里来、存在哪里、后端如何取、前端如何展示这条链必须一环扣一环。组合是否有逻辑用了新技术不是问题问题是大模型、爬虫、可视化这些点之间有没有真实的调用关系。现场演示是否稳一个能稳定跑十分钟的系统比一个功能花哨但一演示就报错的系统分数高得多。拿这个题目来说它的天然优势在于新能源汽车本身是一个自带热度和数据量的领域。售卖的车型参数、每月销量数据、价格区间、续航里程、电池容量这些信息很多都会出现在公开渠道中你不需要去碰用户隐私也不用伪造数据合规压力小数据可解释性强。只要你自己能把爬虫逻辑跑通就同时完成了数据获取和工程实践两个维度的展示。1.2 新能源数据为什么比随便爬个电商更好发挥我之前见过有学生去爬电商平台的商品价格数据是拿到了但分析起来非常单薄无非是最贵的十件商品评价数分布评委看两眼就腻了。新能源汽车数据完全不一样它天然自带多维分析空间销量维度可以按月份、季度看整体大盘走势也能看单一车型的销量起伏。品牌维度可以对比不同品牌的市场份额变化看头部品牌和腰部品牌的差距。产品参数维度电池容量、纯电续航、车身尺寸、价格区间这些字段之间能挖掘出可解释关系比如续航和电池容量的相关性价格区间的密集带在哪。地域维度如果数据源支持按省份统计还能用地图热力分布展示区域偏好。这些维度随便挑三个做成图表都比商品价格排行榜显得有分析深度。更重要的是当数据字段足够结构化之后大模型才有了发挥空间。大模型擅长的事情不是算数而是理解上下文和生成解释性文本。你把一份销量趋势数据和一份车型配置数据丢给它它能写出人类能读懂的摘要2023年Q4新能源市场整体增长其中纯电车型在15-20万价格带竞争最为激烈……这段文字再配合图表展示整个系统的智能感就出来了。2. 系统架构拆解把爬虫、Django、可视化、大模型串成一条流水线2.1 数据流视角的四层结构做毕业设计最忌讳一上来就写代码。先画一条数据流把每个模块的输入输出定义清楚后面写起来会轻松非常多。这个项目我建议按四层来拆层级职责关键工具/组件输入输出采集层抓取原始数据requests、lxml、BeautifulSoup公开网页/公开数据集结构化列表存储层持久化数据SQLite、MySQL、pandas清洗后的DataFrame数据表记录服务层提供接口Django ORM、视图、URL路由前端请求参数JSON数据展示层可视化与交互ECharts、HTML/CSS/JavaScriptJSON接口数据图表、大屏、对话框这条链路里最关键的一点是每一层只依赖上一层的输出不要让前端直接读爬虫文件。很多同学图省事让Django直接在视图里用pandas去读CSV表面上能跑但一旦数据量变大、或者你想做动态筛选这种方式立刻难以为继。正确做法是爬虫爬完的数据先进数据库Django视图只从数据库里取值前端只跟Django接口通信大模型只接收结构化数据并返回文本。链路清晰答辩时也容易讲。2.2 为什么选Django而不是Flask或者FastAPI每次讲到框架选型都有同学纠结。我的建议很直白如果是毕业设计优先选Django。不是说Flask不好而是Django自带的几件套恰好能覆盖评委关注的点。首先是自带ORM。Flask要自己去配SQLAlchemyDjango的ORM建表、迁移、查询一套流程非常成熟。你写一个models.py执行makemigrations和migrate表就建好了对初学者友好也不会在答辩时被问倒。其次是自带Admin后台。注册几张表后你就拥有一个可视化数据管理界面可以直接在后台增删改查。答辩演示时我通常建议学生先打开Django Admin页面把某一条销量数据改掉再去前端看图表响应这个动作瞬间就能证明前后端数据是通着的这个展示效果比任何PPT截图都有说服力。第三是自带模板系统如果你想快速渲染一个服务端页面直接写HTML模板就能用如果想把前后端完全分离Django也能很好地提供纯JSON接口。进可攻退可守这是Flask不具备的省心。3. 爬虫实现数据采集要解决的不只是抓到数据3.1 数据源选择与合规前提爬虫模块是整个项目的数据入口但我必须先说一句底线问题爬虫不是爬一切。毕业设计里我建议只采集公开数据而且优先选择正规渠道。理想的数据源有几类一是行业协会公开发布的销量报告和月度产销数据这类数据通常是官方口径有权威性适合做大盘走势分析二是汽车资讯网站上的车型参数列表页品牌、车系、价格、续航、电池容量这些字段都是公开产品信息不涉及用户隐私三是企业发布的产销快报这类页面对爬虫的访问频率要求通常也相对宽松。实操时要注意几个合规习惯先看目标网站的robots.txt尊重对方声明控制请求频率建议两次请求之间随机延时1到3秒不伪造明显恶意的User-Agent不需要隐瞒访问意图到伪装浏览器的程度用一个正常的PC端UA即可最重要的是绝不爬取任何个人用户信息、手机号、身份证、浏览记录。做数据可视化不差那点数据量没必要给自己埋雷。3.2 requestsXPath的经典组合抓取公开列表页最稳定的组合就是requests加lxml的XPath。现在有些人喜欢用Selenium模拟浏览器但对于公开列表页来说Selenium太重了启动慢、内存占用高而且不需要执行JavaScript的场合里它纯粹是给自己找麻烦。下面是采集车型参数列表页的思路示例import requests import time import random from lxml import html def fetch_page(url, timeout10): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } for attempt in range(3): try: resp requests.get(url, headersheaders, timeouttimeout) resp.raise_for_status() return resp.text except requests.RequestException as e: print(f第 {attempt 1} 次请求失败: {e}) time.sleep(2) return None def parse_car_list(page_text): doc html.fromstring(page_text) rows doc.xpath(//div[contains(class, car-item)]) result [] for row in rows: # 实际字段名以目标页面的 class 为准这里只是示例 name .join(row.xpath(.//a[contains(class, name)]//text())).strip() price .join(row.xpath(.//span[contains(class, price)]/text())).strip() if name and price: result.append({ 车型名称: name, 指导价: price }) return result这段代码里有几个细节值得注意。第一是重试机制网络请求一定会遇到偶发超时三次重试能显著提高爬虫的健壮性第二是用contains(class, xxx)而不是classxxx真实页面里class往往有多个值精确匹配经常落空第三是清洗空格和换行符否则入库数据会带着一堆\n和空格后面做图表时全是坑。请求之间一定要加延时if __name__ __main__: for page in range(1, 11): url fhttps://example.com/cars?page{page} # 示例地址 text fetch_page(url) if text: items parse_car_list(text) print(f第 {page} 页解析到 {len(items)} 条数据) time.sleep(random.uniform(1, 3))random.uniform(1, 3)的意思是每次暂停1到3秒之间的随机时长这比固定sleep(2)更自然能在一定程度上降低被服务端限流的概率。3.3 数据清洗的隐藏工作量爬虫解析出来的数据基本不能直接入库。原因很简单网页文本里所有字段都是字符串而数据库和图表需要数字类型另外不同页面里的同一种写法也可能不一致比如15.98万15.98万元暂无报价混在一起。我习惯把清洗工作交给pandas集中处理而不是在解析循环里到处打补丁import pandas as pd def clean_price(value): if value in (暂无报价, 待公布, ): return None # 去掉万和元统一转成万元数值 return float(value.replace(万元, ).replace(万, ).replace(元, ).strip()) def clean_range(value): if not value or value -: return None return int(float(value)) df pd.DataFrame(raw_items) df[价格(万元)] df[指导价].apply(clean_price) df[续航(km)] df[续航里程].apply(clean_range) df df.drop_duplicates(subset[车型名称]) df df.dropna(subset[价格(万元)])drop_duplicates是按车型名称去重这个动作必须做因为分页爬虫很容易把同一款车的不同配置页面当成多条记录。dropna是过滤掉关键字段缺失的行价格、续航这种核心字段如果是空值做图表时会破坏整体可信度。清洗完的数据就可以写进数据库了。4. Django后端模型设计与接口设计要匹配可视化需求4.1 三张核心表的设计这个系统我建议至少设计三张表车型信息表、销量记录表、充电设施统计表。不需要设计太多表三张刚好覆盖产品、市场、基础设施三个分析维度。# cars/models.py from django.db import models class CarModel(models.Model): brand models.CharField(max_length50, verbose_name品牌) series models.CharField(max_length100, verbose_name车系) model_name models.CharField(max_length150, verbose_name车型名称) price_min models.DecimalField(max_digits8, decimal_places2, verbose_name最低售价(万元)) price_max models.DecimalField(max_digits8, decimal_places2, verbose_name最高售价(万元)) battery_capacity models.FloatField(verbose_name电池容量(kWh)) range_km models.IntegerField(verbose_name纯电续航(km)) body_type models.CharField(max_length20, verbose_name车身类型) class Meta: verbose_name 车型参数 class SalesRecord(models.Model): brand models.CharField(max_length50, db_indexTrue, verbose_name品牌) car models.ForeignKey(CarModel, on_deletemodels.CASCADE, verbose_name关联车型) month models.CharField(max_length7, db_indexTrue, verbose_name统计月份) sales_volume models.IntegerField(verbose_name销量(辆)) class Meta: verbose_name 销量记录 class ChargingStation(models.Model): province models.CharField(max_length30, verbose_name省份) year models.CharField(max_length4, verbose_name年份) station_count models.IntegerField(verbose_name充电站数量) pile_count models.IntegerField(verbose_name充电桩数量) class Meta: verbose_name 充电设施统计三张表大体对应三类图表主题。车型表可以查续航与电池容量的散点分布、价格区间柱状分布销量表可以查月度趋势、品牌份额充电设施表可以和销量数据做联动分析充电桩数量与新能源销量是否有区域相关性。字段设计我特别加了db_indexTrue因为按品牌和月份做条件查询是高频操作加索引能明显加快响应。4.2 给前端返回JSON而不是返回HTML毕设系统里如果直接让模板渲染HTML图表库拿数据要再从HTML里抠非常别扭。我建议后端只提供JSON接口前端用fetch或者axios拿数据后交给ECharts绘制。普通Django视图返回JSON的做法# cars/views.py from django.http import JsonResponse from .models import SalesRecord def sales_trend(request): brand request.GET.get(brand, ) qs SalesRecord.objects.all() if brand: qs qs.filter(brandbrand) rows (qs .values(month) .annotate(totalmodels.Sum(sales_volume)) .order_by(month)) return JsonResponse({ code: 0, data: list(rows) })路由配置# cars/urls.py from django.urls import path from . import views urlpatterns [ path(api/sales/trend, views.sales_trend, namesales_trend), ]用ORM的values()加annotate()可以把同一个月多条车型记录聚合成月总销量前端拿到的数据直接就是[{month: 2024-01, total: 36800}, ...]这种结构不需要再二次计算。JsonResponse默认会把字典转成JSON中文也做了正确编码前端不会出现乱码。如果项目里想做得更规范一点可以引入Django REST Framework用它的ModelSerializer和视图集自动生成接口。不过对多数毕设来说普通JsonResponse已经够用不要为了技术名词增加不必要的复杂度。4.3 数据更新与后台管理爬虫脚本跟Django要打通不要在项目目录里单写一个孤零零的spider.py然后手动运行。我推荐把爬虫逻辑封装成Django自定义管理命令cars/ management/ commands/ run_spider.py这样你可以直接在项目根目录执行python manage.py run_spider脚本内部读写Django的ORM模型。好处有两个一是数据入库复用同一套模型定义不用写两套建表逻辑二是调试、迁移、定时执行都在Django进程内完成逻辑干净。run_spider.py里调用爬虫模块把清洗后的数据批量写入模型from django.core.management.base import BaseCommand from cars.models import CarModel class Command(BaseCommand): help 采集新能源汽车车型参数并入库 def handle(self, *args, **options): raw_items [] # 这里填爬虫解析结果 bulk_list [ CarModel( branditem[品牌], seriesitem[车系], model_nameitem[车型名称], price_minitem[价格(万元)], price_maxitem[价格(万元)], ... ) for item in raw_items ] CarModel.objects.bulk_create(bulk_list, ignore_conflictsTrue) self.stdout.write(self.style.SUCCESS(f入库成功{len(bulk_list)} 条))bulk_create一次批量插入速度比单条save()快一个数量级ignore_conflictsTrue表示如果有重复记录则跳过配合去重逻辑可以做到重复执行不产生脏数据。Admin后台注册模型在admin.py里写三行就行from django.contrib import admin from .models import CarModel, SalesRecord, ChargingStation admin.site.register([CarModel, SalesRecord, ChargingStation])5. 可视化看板ECharts怎么组合才像系统5.1 大屏布局的取舍可视化部分最怕做成图表堆砌。有的同学一上来就放了七八个图表每个都做得很平均评委看不出重点。我建议按业务叙事来设计版面讲一个完整的数据故事。常见布局是左右分布或者上中下分布顶部系统标题、核心指标卡片总车型数、总销量、平均续航、平均电池容量。左侧销量趋势折线图、品牌份额饼图。中间全国区域地图或品牌销量柱状图。右侧续航与电池容量散点图、价格区间分布图。用ECharts的时候要统一配色不要每个图表单独选一套色系。建议从ECharts官方主题里挑一套深色背景主题整体截图效果会比默认白底专业不少。字体大小也要放大大屏通常是在答辩教室投影小字号根本看不清。5.2 图表选型与数据接口对接图表不是随便选的每种图都有它最适合表达的关系选错会显得外行。我这里给一个选型参考分析目标推荐图表数据接口月度销量变化趋势折线图/api/sales/trend品牌市场份额占比饼图/环形图/api/sales/brand_share各品牌销量对比柱状图/api/sales/brand_top?limit10续航与电池容量的关系散点图/api/cars/scatter价格区间分布直方图/柱状图/api/cars/price_dist省份充电设施分布地图/api/charging/province前端用ECharts绘图的核心流程固定async function loadChart(apiUrl, chartDom, optionMapper) { const res await fetch(apiUrl); const json await res.json(); if (json.code ! 0) return; const chart echarts.init(chartDom); chart.setOption(optionMapper(json.data)); } loadChart(/api/sales/trend, document.getElementById(trend), (data) ({ tooltip: { trigger: axis }, xAxis: { type: category, data: data.map(item item.month) }, yAxis: { type: value, name: 销量(辆) }, series: [{ type: line, smooth: true, areaStyle: {}, data: data.map(item item.total) }] }));optionMapper这个函数的设计我推荐保留。它把接口数据转换成ECharts所需的option结构这样接口返回的数据结构一旦变化只需改映射函数不用重写整段绘图逻辑。答辩时评委如果要改图表类型从折线图换成柱状图也只是改一个type字段的事情演示会非常从容。5.3 图表联动与筛选的注意点真正的系统感来自交互而不是静态图表。至少要实现两个交互一是按品牌筛选全看板二是点击饼图区块联动其他图表。按品牌筛选最简单的做法是下拉框加上全量图表重新加载。给所有图表统一封装一个refreshAll()函数let currentBrand ; async function refreshAll() { const suffix currentBrand ? ?brand${encodeURIComponent(currentBrand)} : ; await loadChart(/api/sales/trend${suffix}, ...); await loadChart(/api/sales/brand_share${suffix}, ...); await loadChart(/api/cars/scatter${suffix}, ...); } document.getElementById(brandSelect).addEventListener(change, (event) { currentBrand event.target.value; refreshAll(); });后端接口统一支持brand参数筛选逻辑在Django的视图函数里做前端只负责传参和重新渲染。这里有个容易踩的坑encodeURIComponent一定要加品牌名里如果有中文和空格不编码直接拼URL浏览器会发出去一个不规范的请求Django解析时可能拿不到完整参数。点击饼图联动其他图表的逻辑本质上是监听ECharts的click事件pieChart.on(click, (params) { currentBrand params.name; refreshAll(); });6. AI大模型接进系统不只是加一个对话框6.1 方案一用大模型生成数据摘要很多同学做大模型接入加个聊天框就算交差。但这个方案在答辩时很容易被追问你的大模型和数据分析有什么关系如果你只是写了一个通用聊天框用户问什么它答什么跟这个系统本身没有任何上下文关联这个功能就是摆设。第一种做法是数据摘要生成。后端统计出关键数据比如本月总销量、同比变化、销量最高的三个品牌、价格集中区间把这些数据拼接成一个结构化的提示词丢给大模型API让它返回一段分析性文本。前端把这段文本放在看板顶部作为AI智能分析简报。提示词构造示例def build_analysis_prompt(stats): return f 你是新能源汽车行业分析师。请根据以下统计数据生成一段简洁分析 1. 最近一个月总销量为 {stats[month_total]} 辆环比 {stats[mom_change]}%。 2. 销量前三位品牌是 {stats[top_brands]}。 3. 热销价格区间为 {stats[hot_price_range]}万元。 要求结论先行不超过150字不罗列数据给出一个可操作的趋势判断。 大模型返回的文字配合页面上的折线图和柱状图既有视觉效果又有解读文字评委一眼就明白大模型在这个系统里的角色是分析师不是摆设聊天机器人。6.2 方案二让AI基于结构化数据做问答第二种做法是给大模型提供一部分数据上下文让它针对这些数据回答问题。这里要控制好传给模型的上下文长度不要试图把整张数据表都丢给它而是先由后端查询出与问题相关的数据片段再拼接进提示词。打个比方用户问15到20万价位有哪些高续航车型推荐后端先从数据库里筛选出价格在15到20万之间、续航高于500公里的车型把筛选结果格式化成文本然后交给大模型做分析和推荐。这个过程不涉及复杂的向量检索但效果已经比大模型凭空回答可靠得多因为它看到的数据是真实数据库里的数据不是模型记忆里的幻觉数据。调用大模型API的通用写法大致如下import requests API_KEY 你的API Key API_URL 你的大模型服务地址/chat/completions def ask_llm(prompt: str) - str: resp requests.post( API_URL, headers{ Authorization: fBearer {API_KEY}, Content-Type: application/json, }, json{ model: 你的模型名称, messages: [ {role: system, content: 你是新能源汽车数据分析助手。}, {role: user, content: prompt}, ], temperature: 0.3, }, timeout30, ) resp.raise_for_status() return resp.json()[choices][0][message][content]注意temperature要设低一点比如0.2到0.3。数据分析场景下我们希望结果稳定不要每次生成的摘要都不一样。这个参数是创造性的控制旋钮温度越高回答越随机温度越低回答越保守分析类任务用低温更合适。6.3 答辩现场API不可用时的兜底方案这里必须说一个真实教训。之前有个学生答辩时现场网络波动大模型API请求超时他准备的智能分析区域直接空白评委脸色立刻变了。任何依赖外部API的功能都必须设计降级方案。我建议做两层降级。第一层是把每次成功调用大模型返回的结果缓存到本地数据库表里设置一个有效期第二次请求同样的数据参数直接读缓存不再调用API。第二层是准备一份离线摘要模板用Django模板语言和统计数据直接填充比如本月销量前三的品牌是A、B、C热销价格带是10-15万元这段文字不经过大模型也能正常显示。前端请求失败时自动切换到降级内容肉眼几乎看不出来。这层设计本身在答辩时也可以主动说出来说明你考虑了系统的稳定性和外部依赖风险这是个加分项不是减分项。7. 交项目之前的检查清单这些细节最容易丢分7.1 环境可复现是基本盘毕业设计最终交付通常包含源码和运行文档。很多同学在自己电脑上跑得好好的换一台电脑就起不来大部分原因是没有隔离依赖。项目根目录一定要有requirements.txt和虚拟环境使用说明。导出依赖pip freeze requirements.txt但我更推荐手动整理一份精简版本只保留真正用到的依赖因为pip freeze会把很多无关的传递依赖也列出来导师安装时容易出问题。一个干净的新能源项目至少要包含这些Django4.2 requests2.31 lxml4.9 pandas2.0数据库文件如果用的是SQLite直接把.db文件连同项目一起交付就行如果用的是MySQL或PostgreSQL务必在说明文档里写清建库语句和数据导入命令否则导师连数据库都没有系统自然跑不起来。7.2 数据量太小怎么办新能源汽车这个方向本身数据量不小但如果你只爬了二三十个车型做出来的图表会非常单薄。补救思路有几个增加时间维度爬取车型参数的同时按月份爬取最近两年销量一张趋势图立刻丰满。增加对比维度加入燃油车销量作为对照更凸显新能源渗透率的变化趋势。合规填充模拟数据如果某个字段确实没有公开来源在文档中明确说明该字段为演示数据按历史趋势线性插值生成。在显眼位置标注反而显得诚实直接伪造不说明才是学术问题。数据量充足之后散点图才看得出相关性柱状图才能比出差距饼图才能体现份额结构。数据可视化项目数据量本身就是展示的一部分。7.3 演示流程要按故事线排练我建议的演示顺序不是从爬虫讲起而是从结果讲起打开可视化看板先讲总览指标让评委对系统有了直观印象。演示品牌筛选和图表联动展示交互能力。打开AI智能分析区域展示大模型生成的摘要和问答结果。打开Django Admin后台修改一条数据刷新前端看变化证明数据链路完整。最后再讲爬虫模块展示爬取的原始数据代码和入库结果此时整个系统的技术版图才完整闭合。这个顺序的逻辑是先给结果再讲实现。评委看到酷炫的看板之后后面讲爬虫、讲Django时他们会带着已有的好印象去理解接受度会高很多。写在最后做完成之后别急着收工我每次带学生做完这类系统都会建议他们再回头做一件事把整个系统的数据流画成一张手绘图从爬虫脚本开始画到数据库再到Django接口最后到ECharts和大模型。这张图不是为了答辩交差而是你自己理清思路的过程。你能不看代码把这张图画出来就说明系统真的是你搭的而不是把别人的项目跑了起来。反过来这张图你画不明白那答辩时评委随便问一个细节大概率就要露馅。最后再分享一个个人经验大模型接入这个模块最容易被低估的是提示词要跟业务数据深度绑定。一个只会在通用问题上聊天的机器人跟一个开口就能说出本月热销车型集中在10到15万价位的数据助手观感差距是肉眼可见的。你在写提示词时多花半小时把几个统计指标拼接进去整个项目的创新含金量会直接上一个台阶。