ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django+Python汽车价格分析可视化系统开发实战

Django+Python汽车价格分析可视化系统开发实战 前阵子朋友问我十几万的预算买什么车最划算我随口报了三个车型结果他打开手机一查价格跟我说的差了两万。那一刻我意识到汽车价格这东西光靠印象根本靠不住。与其每次手动查不如自己做一个系统把主流车型的价格、趋势、配置变化全都抓下来分析。于是就有了这个项目基于Django的Python主流汽车价格分析可视化系统。这个系统用Django搭建后端服务用Python完成数据采集和分析最终通过可视化页面把价格走势、品牌排名、车型分布直观地呈现出来。文章里我会把整个项目的关键细节和踩过的坑完整记录下来给正在做Django项目的朋友一个参考。1. 项目拆解与整体架构设计1.1 系统定位这不是一个简单的报表页面很多人一听汽车价格分析系统第一反应是不就是把数据库里的价格画成折线图吗。真做起来会发现钱都在数据上。汽车价格有几个特点波动快不同地区报价不一样指导价和成交价往往差好几万再加上经销商促销、新款上市清库存价格随时在变。如果只做一个静态报表数据过两天就失效了没有任何参考价值。所以这个系统的核心定位是可持续更新的价格分析工具而不只是展示页面。它要解决三个问题数据从哪来数据怎么保证可信数据怎么让人一眼看懂。围绕这三点我把系统分成了四个模块数据采集模块、数据清洗模块、后端业务模块、可视化展示模块。前两个模块负责把散落在各个汽车平台的价格信息变成规整的数据后两个模块负责把数据转化成用户能看懂的图表和页面。1.2 为什么技术栈选了Django Python说实话这个项目用Spring Boot也能做但我最后选了Django原因很实在。第一Django自带ORM和Admin后台写一套模型就能自动生成管理界面数据维护成本很低第二Django内置用户认证、CSRF防护、Session管理做带登录功能的信息系统几乎不用额外写安全逻辑第三整个项目的核心是数据分析而Python在这块生态太强了pandas处理表格、requests抓页面、pyecharts或ECharts画图全都在同一个语言体系里不用跨语言来回切换。我选的是Python 3.10 Django 4.2的组合。Django 4.2是当前维护周期比较长的版本支持Python 3.8以上文档齐全社区踩坑案例多。数据库用的MySQL 8.0没有特殊原因就是学校和企业里用MySQL的多配合Navicat排查数据比较方便。如果只是本地演示直接用Django默认的SQLite也能跑后面部署到服务器再换MySQL就行。1.3 系统整体请求流程与模块分工从用户操作角度整个系统是这样一个流程用户打开首页选择汽车品牌、车型前端页面发送Ajax请求到Django后端接口后端通过ORM从MySQL查出对应的价格记录经过聚合计算后返回JSON数据前端拿到数据后用ECharts渲染出折线图、柱状图或者排行榜。而数据本身不是手工录入的是由爬虫定时从汽车垂直平台上抓取经过清洗后写入临时表再通过增量更新任务同步到正式表。模块之间我用Django的App做了隔离。项目里建了三个Appcrawler负责抓取和清洗数据analysis负责价格查询和统计接口dashboard负责页面渲染和可视化交互。这样做的好处是职责清晰后面想单独扩展爬虫逻辑或者换可视化方案不会牵一发而动全身。1.4 数据库表设计用最少的表描述最核心的实体汽车价格分析涉及的数据实体其实不多核心就是品牌、车型、价格记录、用户。我把表结构设计得尽量精简避免过度设计。表名核心字段说明brandid, name, logo_url, created_at汽车品牌比如大众、丰田、比亚迪car_modelid, brand_id, name, guide_price, year, fuel_type具体车型存指导价和年款信息price_recordid, car_model_id, dealer_price, city, record_date每日经销商报价核心数据表userDjango内置User表扩展保存用户名、密码、收藏关系user_favoriteid, user_id, car_model_id用户收藏的车型价格记录表是整张表里数据量最大的需要建联合索引。我在car_model_id和record_date上加了联合索引因为所有查询几乎都是某个车型在某个时间段的价格。品牌表和车型表是一对多关系用外键关联。这里有一个细节车型名称不能作为唯一约束因为不同年款的同名车价格完全不同所以我在car_model里加了year字段用name year作为唯一标识。2. 数据采集与清洗把散落的价格变成结构化数据2.1 数据来源梳理与爬虫策略设计做价格分析最忌讳的就是数据源单一只盯着一家平台容易以偏概全。我选了三个主流汽车垂直平台作为数据源相互校验价格。不过爬取之前先做了一件很重要的事查看目标平台的robots.txt确认哪些路径允许抓取。很多人拿到爬虫项目就开始跑结果两三天后IP被封其实大部分时候不是技术问题而是没有遵守最基本的爬虫礼仪。爬虫框架我没有用Scrapy原因是这个项目抓取量不大单线程完全够用Scrapy配置组件反而显得笨重。我用的requests BeautifulSoup一个页面一个页面地解析。抓取频率控制在每2到4秒一个请求模拟人的操作速度同时设置随机的User-Agent。这里贴一段核心抓取逻辑import requests import time import random from bs4 import BeautifulSoup SESSION requests.Session() SESSION.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) def fetch_car_price(url): try: resp SESSION.get(url, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) # 假设页面结构中的价格标签具有 price class price_tag soup.find(span, class_price) if not price_tag: return None text price_tag.text.strip() time.sleep(random.uniform(2, 4)) return text except requests.RequestException as e: print(f请求失败: {e}) return None需要注意的是不同网页的价格元素结构完全不同不能指望一个通用解析函数搞定所有平台。我针对每个数据源写了一个独立的解析器统一返回原始字符串再交给清洗模块处理。这样代码虽然多了一点但维护性比写一堆if-else强得多。2.2 价格数据清洗从10.98万起到可计算的数值爬下来的价格长什么样五花八门。有暂无报价有10.98万起有9.49-15.89万还有需询底价。如果直接存进数据库排序和计算全都得崩。所以清洗这一步我单独写了一个模块专门处理这些脏数据。核心思路是用正则表达式把字符串中的数字提取出来然后判断是单一价格还是区间价格。如果是区间价格就取最低值和最高值两个字段如果是万起这类表述就统一当作最低价处理。数据清洗代码大致长这样import re def clean_price_str(raw_price): if not raw_price: return None, None text raw_price.replace(,, ).strip() if 暂无 in text or 询 in text: return None, None numbers re.findall(r\d\.?\d*, text) if not numbers: return None, None prices [float(num) for num in numbers] if len(prices) 1: # 单一口径价格 return prices[0], prices[0] return min(prices), max(prices)清洗完成后我用pandas对全量数据做了一次去重和缺失值检查。某一个车型如果连续七天都缺失价格我会单独生成一份异常清单要么手动补录要么标记为停售车型。这个环节非常有必要否则图表里会出现大段断档用户会以为系统出了Bug。2.3 数据入库与增量更新怎么保证每天都有新鲜数据数据入库用的是Django ORM的update_or_create方法这个方法会在记录不存在时创建存在时更新指定字段。关键点是唯一约束我在PriceRecord模型上设置了unique_together (car_model, record_date)确保一个车型一天只有一条价格记录。这样每天抓取新数据时不会产生重复记录。from myapp.models import CarModel, PriceRecord def save_price_record(car_model, dealer_price, city, record_date): PriceRecord.objects.update_or_create( car_modelcar_model, record_daterecord_date, defaults{ dealer_price: dealer_price, city: city, } )增量更新我用的是APScheduler在Django启动时加载一个定时任务每天凌晨2点执行一次抓取和清洗。选择凌晨是因为这个时间段没有用户访问数据更新对系统性能影响最小。定时任务里我会先清空临时表重新抓取全量数据再逐条update_or_create。刚开始担心全量抓取太重实际跑下来发现一个平台全量抓一遍也就几百个车型完全能接受。3. 后端核心模块实现Django的查询与接口设计3.1 模型实现ORM建模的细节和坑回到代码层面models.py是整个后端的地基。我定义了三张核心模型其中关键点在于价格字段用DecimalField而不是FloatField。原因是价格计算不能出现浮点数精度问题FloatField在累加和比较时容易出现0.10.2不等于0.3的情况DecimalField才能保证金额精度。from django.db import models from django.contrib.auth.models import User class Brand(models.Model): name models.CharField(max_length50, uniqueTrue) logo_url models.URLField(blankTrue) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name class CarModel(models.Model): brand models.ForeignKey(Brand, on_deletemodels.CASCADE, related_namemodels) name models.CharField(max_length100) year models.IntegerField() guide_price models.DecimalField(max_digits10, decimal_places2) fuel_type models.CharField(max_length20, blankTrue) class Meta: unique_together (name, year) def __str__(self): return f{self.name} {self.year}款 class PriceRecord(models.Model): car_model models.ForeignKey(CarModel, on_deletemodels.CASCADE, related_nameprice_records) dealer_price_min models.DecimalField(max_digits10, decimal_places2) dealer_price_max models.DecimalField(max_digits10, decimal_places2) city models.CharField(max_length50) record_date models.DateField() class Meta: unique_together (car_model, city, record_date) indexes [ models.Index(fields[car_model, record_date]), ]这里有个容易踩的坑删除车型时on_deletemodels.CASCADE会级联删除所有价格记录。这在设计上没问题但生产环境如果误删一个车型几十天历史数据就直接没了。所以我后来加了一个is_active字段用软删除代替物理删除。Django官方没有内置软删除机制要用第三方库或者自己重写delete()方法考虑到项目规模我只是在视图层过滤了is_activeFalse的记录并没有真的重写delete()。3.2 核心查询逻辑ORM怎么写出高效的聚合查询价格分析系统最常用的查询是某个品牌下所有车型的均价走势还有某个车型在最近30天的价格变化。直接用Python循环遍历会导致N1查询数据量一大接口就卡。这里要提一下Django查询的优化思路。我以品牌均价趋势为例。需求是在首页展示主流品牌最近30天平均经销商价格的变化趋势。如果用循环伪代码是查出所有品牌再查出每个品牌的所有车型再查出每个车型的所有价格记录三层嵌套查询次数是品牌数乘以车型数。正确的做法是用values()加annotate()在数据库层面完成聚合from django.db.models import Avg from django.db.models.functions import TruncDate from myapp.models import PriceRecord def brand_price_trend(days30): records ( PriceRecord.objects .filter(record_date__gtetimezone.now().date() - timedelta(daysdays)) .values(car_model__brand__name, record_date) .annotate(avg_priceAvg(dealer_price_min)) .order_by(record_date) ) return list(records)这段代码生成的SQL会一次性做join和group by数据量几千条时毫无压力。如果后续数据量上百万再考虑用缓存或改成ClickHouse现阶段完全够用。我实测这个查询响应时间在50毫秒以内。3.3 RESTful风格接口设计与前端数据结构约定虽然Django可以直接在模板里渲染数据但为了让前端图表能够灵活刷新我还是把数据全部做成JSON接口前端通过Ajax获取。接口主要分三类品牌列表、车型列表、价格趋势数据。下面是我定义的接口表格接口路径方法参数返回结果/api/brands/GET无品牌ID、名称、logo/api/css/GETbrand_id某品牌下所有车型列表/api/trend/GETcar_model_id, days指定车型价格趋势/api/rank/GETdays, top_n指定天数内降价幅度最大的车型排名/api/login/POSTusername, password登录状态和Token接口返回格式统一为{code: 0, msg: success, data: {...}}。前端只用判断code是否为0出错时弹出msg。这个约定写在一个公共函数里所有接口请求都走它避免到处重复写错误处理。我用的是Django自带的JsonResponse没有引入Django REST Framework。原因很简单这个项目接口数量少不需要复杂的序列化器和认证体系DRF有点杀鸡用牛刀。如果你想快速开发DRF也好用但需要额外学习成本而且容易在序列化嵌套上绕圈子。3.4 用户登录与收藏功能Django内置Auth的使用用户模块直接用Django自带的User表没有重新建用户模型。登录视图写得比较快用authenticate和login两个函数就能完成。但这里我加了一个需求用户收藏自己喜欢关注的车型方便首页直接看到这些车型的价格变化。收藏功能需要一个多对多的关联表。我在models.py里增加了一个UserFavorite模型class UserFavorite(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_namefavorites) car_model models.ForeignKey(CarModel, on_deletemodels.CASCADE, related_namefavorited_by) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, car_model)前端点击收藏按钮时先判断用户是否登录没登录就跳转到登录页。登录后通过Ajax转发到/api/favorite/后端用get_or_create和delete来切换收藏状态。这个功能的难点不是ORM而是怎么在模板里保持登录状态和CSRF Token一致。Django的CSRF防护默认开启所有POST请求都要带csrftoken。我在前端加了一个全局Ajax配置从Cookie中读取csrftoken并写入请求头才算把这个坑填平。4. 可视化大屏与前端交互4.1 ECharts集成从图表库选型到页面动态渲染可视化部分我选了Apache ECharts没有用Chart.js原因是ECharts对折线图、柱状图、雷达图的支持都非常完整而且中文文档友好社区案例多。图表是纯前端的后端只负责给数据前后端分离逻辑很清爽。集成方式很简单在模板里引入ECharts的CDN文件然后准备一个具有宽高的div在页面加载完成后用Ajax请求后端接口拿到JSON后初始化图表。核心代码大概是async function loadTrend(carModelId) { const resp await fetch(/api/trend/?car_model_id${carModelId}days30, { headers: {X-Requested-With: XMLHttpRequest} }); const data await resp.json(); if (data.code ! 0) return; const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ xAxis: { type: category, data: data.data.dates }, yAxis: { type: value, name: 价格万元 }, series: [{ type: line, data: data.data.prices, smooth: true, areaStyle: {} }] }); }踩过一个小坑页面初始化时图表容器宽度可能是0导致ECharts渲染出来一片空白。解决方案是在图表初始化之前保证容器已经显示或者监听window.resize事件重新调用chart.resize()。我在实际项目中就是在Tab切换后手动调用了一次chart.resize()才解决了问题。4.2 核心图表设计价格趋势、品牌均价榜、车型分布整个可视化大屏包含三个核心图表对应三种分析视角。第一个是价格走势折线图展示单个车型在近30天或近90天的价格变化。这个图表解决的核心问题是这车最近有没有降价降了多少。我用了平滑曲线加面积填充让用户一眼就能看出趋势方向。数据点上方标注具体价格鼠标悬浮时显示城市和日期信息。第二个是品牌均价排行榜用横向柱状图展示前10个品牌的平均经销商价格。这个图表的业务价值是回答哪个品牌整体价格区间高哪个品牌更有性价比。数据接口按照品牌分组求均值再降序排列前端把数字超过12万的柱子标成橙色一眼能看出高低档次。第三个是车型价格分布散点图横轴是指导价纵轴是经销商最低价每个点代表一款车型。散点图能直观反映指导价和真实成交价之间的差异程度如果某个点远低于对角线说明这款车优惠力度很大。这个图对买车的用户非常有用是我自己比较喜欢的一个分析维度。4.3 查询筛选与联动交互让用户自己探索数据整个页面不是一个静态大屏用户需要能自主筛选。我的实现方式是页面左侧放筛选面板包含品牌下拉框、车型下拉框、时间范围选择器。用户选了品牌后车型下拉框会通过Ajax刷新。选择车型后页面上的折线图立即请求新数据柱状图和散点图也会根据当前品牌自动联动刷新。联动逻辑的核心是触发函数$(#brandSelect).change(function() { const brandId $(this).val(); loadCarModels(brandId); updateAllCharts(); }); $(#carModelSelect).change(function() { updateAllCharts(); });这里有一个性能细节用户连续切换品牌时可能会一瞬间发出十几个请求导致接口压力大。我在前端加了简单的防抖函数只有用户停止操作300毫秒后才真正发起请求。后端也配合做了接口缓存后面会详细讲。5. 部署上线与性能优化5.1 从开发环境到生产环境Nginx Gunicorn部署Django本地开发用python manage.py runserver非常舒服但它只能用于开发环境部署到服务器必须换成正式的WSGI服务。我最终采用Nginx Gunicorn Supervisor的组合部署在一台2核4G的轻量云服务器上。Gunicorn负责启动Django应用配置非常简单。首先安装依赖然后创建一个gunicorn.conf.py配置文件bind 127.0.0.1:8000 workers 3 timeout 60三个worker进程意味着系统可以同时处理三路请求。对于个人项目已经足够不要盲目调大worker数量否则内存会爆。Nginx则负责反向代理和静态文件服务。生产环境下Django的DEBUG必须设为False静态文件需要执行python manage.py collectstatic收集到指定目录。Nginx配置片段server { listen 80; server_name your_domain.com; location /static/ { alias /var/www/myproject/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署时最容易漏的是ALLOWED_HOSTS没有配置Django会直接拒绝请求提示Bad Request (400)。这个坑我很早踩过现在写下来提醒各位。5.2 数据缓存与接口加速系统上线后首页的品牌均价趋势是访问最频繁的接口但数据一天只更新一次完全没必要每次都从数据库查。这里用Django的cache_page装饰器做了接口级缓存from django.views.decorators.cache import cache_page cache_page(60 * 60) def brand_rank_view(request): # 返回品牌均价排行 pass缓存时间设置为一小时用户频繁刷新也不会压到数据库。如果后续要做到秒级实时可以接入Redis但我实测SQLite/MySQL直接扛住这种访问量也没问题。缓存策略的核心是能静态化就静态化能缓存就缓存数据更新频率决定了查询压力上限。5.3 安全加固CSRF、XSS和接口限流Django自带的安全机制比大多数框架都周全。CSRF中间件默认开启只要模板里用了{% csrf_token %}基本不会被跨站请求伪造攻击。XSS方面Django模板默认转义HTML标签只要不用|safe过滤器用户输入很难注入脚本。我额外做的一层保护是接口限流。登录接口和收藏接口容易被刷我用了一个简单的访问频率记录器把用户IP和接口名拼成Key存到缓存中如果60秒内超过30次请求就拒绝。虽然不能做到完美防刷但至少挡住了正常手法的攻击。真正高防需要更复杂的策略个人项目不需要上。6. 踩坑记录与问题排查速查表6.1 印象最深的一个Bug所有价格数据变成了字符串项目开发到一半时我突然发现折线图里的价格全部显示成了字符串比如10.5万元而不是数值10.5。排查了很久才发现问题不在后端接口而在清洗模块。我清洗完价格后又做了一步字符串拼接把单位万元加了回去存入数据库时DecimalField字段虽然可以接收字符串但前端拿到JSON后typeof变成了stringECharts直接用字符串画图坐标轴顺序全乱了。解决方法是前端判断数据类型统一用Number()转换或者后端在JSON序列化前确保返回浮点数。这个Bug看起来低级但很多人会踩。我建议所有接口数据在返回前先自己模拟一遍前端解析确认类型都正确。6.2 中文乱码与编码问题爬虫刚写完时解析出来的品牌名全是乱码。排查后发现是页面编码问题某些平台虽然不是UTF-8但页面没有显式声明编码requests默认用ISO-8859-1解析了。我最初把响应对象的.text直接当成字符串结果中文字符全变成乱码。解决办法是在拿到响应后检查响应头中的编码字段如果没有就通过页面meta标签判断必要时用resp.content.decode(gbk, errorsignore)手动解码。这个坑在爬虫领域非常常见建议大家在封装请求函数时统一处理。6.3 前端图表加载不出来接口返回200但一片空白有一段时间首页折线图怎么刷新都是空白浏览器F12看不到任何报错。我以为是请求没发出去后来发现其实是div容器高度没设置。ECharts初始化时如果容器高度是0图表会静默失败。这个问题很隐蔽因为不会报JS错误。解决办法就是确保图表容器有确定的高度不要依赖内容撑开。6.4 常见问题排查速查表现象可能原因解决办法访问网站返回400 Bad RequestALLOWED_HOSTS没配置在settings.py配置服务器域名或IP图表显示空白容器高度为0或者图表初始化时元素未显示设置固定高度或在容器显示后调chart.resize()接口返回NaN数据库价格字段为空字符串清洗时统一转成NULLJSON序列化时过滤空值数据库有重复记录唯一约束没加或者用了get_or_create但条件不对设置unique_together改用update_or_create定时任务不跑APScheduler没在启动脚本中注册在APP的ready()方法中启动调度器7. 项目拓展方向与个人实操体会7.1 这个系统还能怎么扩展当前系统已经能完成价格趋势分析和基础可视化但离智能分析还有距离。我后续想加两个功能一是价格预测用Python的statsmodels库对历史价格做时间序列预测预测未来一周的涨跌趋势。这个功能的难点不在算法而在数据质量必须保证连续稳定的历史数据。二是增加区域对比把价格按照城市分组展示同一车型在不同城市的经销商报价这就是一张热力地图可视化上可以用ECharts全国地图但需要准备GeoJSON数据。如果数据量真的涨到百万级可以考虑把MySQL换成时序数据库比如InfluxDB或者用ClickHouse做分析引擎。Django的ORM在这种情况下会成为瓶颈需要写原生SQL或者通过API接口调用分析引擎。不过这已经是另外一套架构了。7.2 我的几点实操体会项目做完回头看最大的体会是可视化系统的成败不取决于图表库多炫酷而取决于数据质量和对业务的理解。比如指导价和经销商报价是两个完全不同的概念如果把它们混在一起分析所有结论都是错的。还有城市差异同样是奥迪A6L北京和成都的经销商报价可能差三五个点如果不区分城市趋势图会失真。另外我发现爬虫和数据清洗花费的时间占了整个项目的六成以上。很多人一上来就喜欢写算法、画大屏但真正到上线阶段才发现数据不完整、字段对不上。我的建议是先用最简单的模型把数据链路跑通再逐步优化。先做出一个能用但粗糙的版本比花两个月打磨完美架构更实际。最后分享一个小技巧Django自带Admin后台非常强大我把价格记录表的Admin配置成每行都可以快速编辑平时更新数据、修正异常值直接在后台操作就行完全不写SQL。这个系统从开发到部署我一个人两周时间就完成了工具选对、方向清晰效率能提高一倍。
返回列表