首页 / 资讯中心 / 文章详情

Django PM2.5可视化项目:从源码拆解到部署避坑全指南

Django PM2.5可视化项目:从源码拆解到部署避坑全指南 ★ FEATURED ARTICLE
简介围绕Django框架开发的城市PM2.5空气质量数据可视化分析源码面向高校数据可视化课程与期末大作业设计以北京、上海、广州、成都、沈阳五城六年PM2.5数据为分析对象完整覆盖数据导入、数据库设计、后端逻辑与前端展示是一份个人评分98分以上的高分大作业项目适合需要完整可运行参考方案的在校生与初学者。压缩包共64个文件类型涵盖Python代码、CSV数据、HTML模板、XML配置与SQL脚本包体约12.38MB。其中py文件对应Django路由、视图、模型及数据导入脚本CSV按城市和气象因子组织包括PM2.5与温度、湿度、气压、风向、露点等关系数据并配有登录、注册、首页等前端页面和数据库文件。资源内含不同季度逐年、时间序列、月度变化等多维分析内容目录结构清晰便于按模块阅读和二次开发。已有328人学习或下载适合作为课程设计、期末大作业或毕业设计原型直接参考。1. 城市PM2.5可视化项目为什么这类Django毕设源码值得你动手拆一遍每年毕业季都能看到大量“基于Django的空气质量数据可视化分析”这类题目一个包含完整源码的zip包往往被当成交差工具解压、跑通、截图、完事。但如果你只想靠这个应付查重你会错过一个真正能写进简历的技术样本。这个项目看起来是“爬数据画图”实际上把Django MTV架构、ORM建表、异步采集、ECharts前端渲染、缓存策略全串起来了。我见过太多人卡在“爬虫能跑但页面空白”“图表加载不出来”“数据库表对不上”这类问题上根源都是只抄了代码没理解Django的请求生命周期。这篇就按我复现这类项目的路径从造数到部署一步步拆开讲包括那些耗了我两个晚上的玄学报错。2. 先搭Django项目骨架模型设计和数据落库是可视化的地基2.1 项目结构规划不要让所有代码都堆在根目录按Django官方的MTV分层习惯拿到任何源码包第一件事不是点运行而是先看目录结构。一个合格的可视化分析项目至少要分出apps业务模块、static静态资源、templates模板、db.sqlite3四个部分。我见过很多初学者把视图函数全部写在views.py里一个文件三千行数据清洗、爬虫逻辑、业务查询全混在一起这种代码跑到后面基本没法调试。这个项目的合理拆分方式是这样的project_root/ ├── manage.py ├── pm25/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── data_collect/ │ │ ├── models.py # 城市、监测点、小时浓度表 │ │ ├── views.py # 返回JSON给前端 │ │ └── urls.py │ ├── analysis/ │ │ └── utils.py # pandas聚合计算 ├── static/ │ ├── echarts/ # 本地化的echarts.min.js │ └── css/ ├── templates/ │ └── index.html └── db.sqlite3上面的pm25是Django默认项目目录apps是我习惯用来区分业务模块的约定。注意apps目录下每个业务模块都要有__init__.py否则Django无法识别这是一个应用。一个常被忽略的细节是静态文件目录里的ECharts库我强烈建议下载到本地因为可视化页面加载不出来的原因一半是CDN被墙或加载慢一半才是代码bug。2.2 设计PM2.5浓度数据模型字段类型和索引决定了查询速度接下来要做的是创建数据表模型。PM2.5监测数据最常见的字段包括城市名称、监测站点、AQI指数、PM2.5浓度微克/立方米、PM10浓度、空气质量等级、采集时间。用Django的ORM把这些字段映射成模型类是第一步但字段类型的选择直接决定了后面查询的效率。比如pm25_value这个字段有人喜欢用FloatField存带一位小数的浓度但实测发现国内公开数据源返回的PM2.5浓度基本都是整数用IntegerField就够了还省空间。再比如collect_time字段如果要做按小时分组的趋势图建议加上db_indexTrue否则数据量到几万条以后filter(collect_time__range...)这种查询会明显变慢。# apps/data_collect/models.py from django.db import models class City(models.Model): 城市基础信息表 name models.CharField(max_length50, uniqueTrue, verbose_name城市名) province models.CharField(max_length50, blankTrue, verbose_name所属省份) lng models.FloatField(default0, verbose_name经度) lat models.FloatField(default0, verbose_name纬度) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table city ordering [-created_at] verbose_name 城市 class AirQuality(models.Model): 小时级空气质量数据 city models.ForeignKey(City, on_deletemodels.CASCADE, related_nameaqi_data) station models.CharField(max_length100, blankTrue, verbose_name监测站点) pm25 models.IntegerField(default0, verbose_namePM2.5浓度(μg/m³)) pm10 models.IntegerField(default0, verbose_namePM10浓度(μg/m³)) aqi models.IntegerField(default0, verbose_nameAQI指数) level models.CharField(max_length20, blankTrue, verbose_name空气质量等级) collect_time models.DateTimeField(db_indexTrue, verbose_name采集时间) class Meta: db_table air_quality ordering [collect_time] verbose_name 空气质量数据这里有个关键点on_deletemodels.CASCADE意味着删除城市会连带删除该城市的所有监测数据。做课程设计一般没问题但如果你后续接的是真实监测设备数据流建议改用SET_NULL并允许空值。另外注意related_nameaqi_data这个参数它允许你在City实例上用city.aqi_data.all()直接取出所有数据反查起来比默认的airquality_set直观得多。模型的Meta类里db_table是显式指定表名不指定的话Django会按“应用名_模型名”自动生成表名例如此处会是data_collect_air_quality。显式指定表名的好处是你后面用原生SQL写统计分析时不会记错表名。3. 数据采集与清洗把公开接口的数据变成数据库里的干净记录3.1 自己写采集脚本还是用现成API先聊聊数据源选型做可视化分析必须得有数据这个项目的原料是城市的PM2.5历史浓度。常见的数据获取方式有三种第一种是爬取空气质量发布平台如中国空气质量在线监测分析平台的公开网页第二种是调用第三方天气API和风天气、心知天气等注册后拿一个token直接请求JSON第三种是自己用模拟数据生成器造假数据。我一般会建议课程设计优先用第二种——公开API返回的JSON结构非常规整没有登录验证码和反爬而网页爬取要处理HTML解析、请求头伪装、频率限制。唯一的代价是免费API的调用次数有限但做毕业设计演示完全够了。如果你选第一种爬虫方案请注意限速手动在time.sleep(5)以上。下面这段代码是用requests直接请求和风天气API拿PM2.5数据并存入Django的开发脚本写成一个独立的Django管理命令是最规范的这样python manage.py collect_data就能执行# apps/data_collect/management/commands/collect_data.py import requests from datetime import datetime from django.core.management.base import BaseCommand from apps.data_collect.models import City, AirQuality class Command(BaseCommand): help 从和风天气API采集指定城市的PM2.5数据 def handle(self, *args, **options): api_key 你的key cities City.objects.all()[:5] # 先采集前5个城市 for city in cities: url https://devapi.qweather.com/v7/air/now params { location: f{city.lng},{city.lat}, # 用经纬度定位 key: api_key, type: aqi } try: resp requests.get(url, paramsparams, timeout10) data resp.json() if data.get(code) ! 200: self.stderr.write(f{city.name} 接口返回异常{data.get(code)}) continue now_data data[now] AirQuality.objects.create( citycity, stationnow_data.get(station, ), pm25int(now_data.get(pm2p5, 0)), pm10int(now_data.get(pm10, 0)), aqiint(now_data.get(aqi, 0)), levelnow_data.get(category, ), collect_timedatetime.now().replace(second0, microsecond0) ) self.stdout.write(f{city.name} 采集成功PM2.5{now_data.get(pm2p5)}) except requests.exceptions.Timeout: self.stderr.write(f{city.name} 请求超时跳过) except ValueError: self.stderr.write(f{city.name} JSON解析失败可能是接口限流)Django管理命令脚本有几点必须说明BaseCommand的handle方法就是执行入口self.stdout.write是规范化的日志输出红色报错用self.stderr.write在终端里是红字。关键逻辑在AirQuality.objects.create这一行——每执行一次脚本就插入一条记录如果你要采集历史数据注意别把collect_time写成datetime.now()因为按小时补历史数据时应该用历史时刻否则后面画时间轴会乱。参数方面timeout10是给requests库设置的超时秒数防止某个城市的接口卡死整个脚本。replace(second0, microsecond0)是去掉秒和微秒保证同一个小时内重复采集不会出现时间戳精度不同导致的重复数据。被try包住的resp.json()在接口返回非JSON内容时会抛出ValueError这个在实际运行中非常常见免费接口经常返回一行报错文本。3.2 数据去重和清洗为什么insert进去的是重复脏数据采集脚本跑起来很容易但数据质量才是可视化项目的隐形评分点。我第一次做这个项目就踩了重复数据的坑定时任务每10分钟跑一次采集一个月积累了十几万条记录画趋势图的时候曲线毛刺多到没法看因为同一时刻存在多条记录pandas聚合时直接翻车。去重逻辑应该在模型层就设计好而不是等查数据的时候才处理。最可靠的办法是做联合唯一约束class Meta: db_table air_quality ordering [collect_time] constraints [ models.UniqueConstraint( fields[city, collect_time], nameunique_city_time ), ]加了约束之后如果再采集到同一城市同一分钟的数据会直接报错你在脚本里对IntegrityError做异常捕获就行。此外清洗维度还要看空气质量等级字段同一个AQI值在不同标准下对应不同等级部分API返回的等级文字和中国的国标不一致。稳妥做法是自己在代码里根据AQI区间重算等级不要信接口的category字段。4. 从数据库到前端图表接口响应和ECharts配置的全链路4.1 视图函数返回JSON还是渲染模板按页面刷新频率决定可视化项目的核心页面是展示趋势图和排名柱状图。Django有两种做法第一种是模板渲染后端把数据塞进context页面用scriptvar data {{ data|safe }}/script传给ECharts第二种是后端只提供JSON接口前端用fetch请求后动态渲染。两种我都实测过结论很明确第二种更接近真实企业级数据可视化项目。原因在于当你需要在页面上做日期范围筛选、城市切换时第一种方案会导致整个页面刷新体验很差而JSON接口配合前端事件绑定能做到局部更新。而且面试官问起来“前后端分离”这个点本身就是加分项。下面是一个典型的可视化接口视图# apps/data_collect/views.py from django.http import JsonResponse from django.views import View from django.db.models.functions import TruncHour from django.db.models import Avg, Max from apps.data_collect.models import AirQuality class PM25TrendAPI(View): 返回指定城市24小时PM2.5趋势数据 def get(self, request): city_id request.GET.get(city_id, 1) hours request.GET.get(hours, 24) # 按小时取平均浓度 rows ( AirQuality.objects .filter(city_idcity_id) .values(collect_time) .annotate( hourTruncHour(collect_time), avg_pm25Avg(pm25) ) .order_by(-hour)[:int(hours)] ) # 倒序回来方便前端直接按时间正序渲染 rows list(reversed(rows)) time_axis [row[hour].strftime(%m-%d %H:00) for row in rows] pm25_values [round(row[avg_pm25], 1) for row in rows] return JsonResponse({ code: 200, data: { times: time_axis, pm25: pm25_values, city_id: int(city_id) } })这段代码的核心在TruncHour这个数据库函数它把DATETIME类型的collect_time按小时截断配合annotate做分组聚合SQL层面就完成了“同一小时取平均”的动作完全不需要在Python里循环手动处理。注意values(collect_time)的作用是告诉ORM“我要按这个字段分组”实际上Django生成的SQL是GROUP BY原始时间字段——这有个坑如果原始数据存在秒级差异分出来的组会不一样所以模型设计时强制秒数归零就很重要。[:int(hours)]这段切片是在数据库层面执行LIMIT而不是把全部数据查回来再截取这是Django查询集懒加载的优势。如果你要做一个24小时趋势实际请求的数据库记录只有24条压力很小。4.2 ECharts配置折线图、柱状图的参数总结后端接口写好后前端就是ECharts的配置问题。可视化大屏最常见的两类图是时间轴折线图和城市排名柱状图。下面这段是templates/index.html里折线图的完整配置!-- templates/index.html -- div idtrendChart stylewidth: 100%; height: 400px;/div script src{% static echarts/echarts.min.js %}/script script fetch(/api/trend/?city_id1hours24) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ tooltip: { trigger: axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: data.data.times, boundaryGap: false, axisLabel: { rotate: 30 } }, yAxis: { type: value, name: PM2.5 (μg/m³), scale: true }, series: [{ name: PM2.5浓度, type: line, smooth: true, symbol: circle, symbolSize: 6, data: data.data.pm25, areaStyle: { opacity: 0.2 }, lineStyle: { width: 2 } }] }); }); /scriptECharts的参数有几个最容易出界面的细节boundaryGap: false让折线从第一个点开始贴边绘制不移半个坐标点这对时间轴尤其重要axisLabel的rotate: 30解决24个时间标签横排重叠的问题smooth: true会让曲线变得圆滑但如果数据波动剧烈过度平滑会被质疑数据造假。yAxis里加scale: true的意思是Y轴不从0开始而是根据数据范围自适应这样PM2.5浓度在30到60之间波动时曲线振幅大视觉上更明显。还有一点容易忽视echarts.init的容器必须要有明确宽度和高度否则初始化出来是空白。很多人在Django模板里把图表容器放在display:none的tab页中结果点击切换时图表不显示或尺寸错乱——解决思路是切换后调用chart.resize()。5. 避坑指南Django可视化项目最常见的五个翻车现场5.1 静态文件404CSS、JS加载不出来现象页面HTML渲染正常但浏览器控制台一堆404图表区域空白。 原因Django开发环境默认由django.contrib.staticfiles应用处理静态文件但必须在settings.py里配置STATIC_URL和STATICFILES_DIRS两个参数。很多源码zip里只配了STATIC_URL/static/忘了把项目本地static目录加入搜索路径。 解决在settings.py中显式添加STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / static, ]另外模板里引用静态文件务必用{% load static %}加{% static path %}写法不要直接写死/static/echarts/echarts.min.js。5.2 数据库迁移报错表已存在或字段缺失现象python manage.py migrate报django.db.utils.ProgrammingError: table air_quality already exists或者提示Unknown column level。 原因这类源码包通常附带db.sqlite3文件里面可能已经有历史表结构。Django迁移系统通过django_migrations表记录执行过的迁移当你改了模型但没生成新迁移直接跑migrate会跳过makemigrations新产生的变更。 解决如果不需要保留数据库里的历史数据直接把db.sqlite3删掉然后重新执行makemigrations和migrate。如果需要保留用makemigrations --name add_field_level生成一个独立迁移再migrate。5.3 AQI等级显示None前端图表有数据但文字空现象数据能展示下拉列表或悬浮提示里的空气质量等级是空字符串。 原因第三方接口返回的字段名不一定叫level有的叫category有的叫quality。代码里取错了键名模型字段又允许blankTrue所以静默存入空值。 解决写数据清洗函数时先打印一次完整响应体把实际字段名核对一遍。我的经验是兼容所有可能性level now_data.get(category) or now_data.get(quality) or 5.4 时区问题导致日期差8小时现象前端展示趋势图最后一个点不是当前时刻而是当前时刻前8小时。 原因Django的USE_TZTrue时数据库存的是UTC时间前端JS的new Date()按本地时区解析如果接口返回的时间字符串不带时区后缀浏览器会按本机时区解释造成偏移。 解决视图函数里查询时间字段时用timezone.localtime()转成东八区再格式化from django.utils import timezone hour_label timezone.localtime(row[hour]).strftime(%m-%d %H:00)5.5 历史数据量大时图表加载卡死现象选“近30天”或“近一年”的维度页面请求接口要等十几秒才出图。 原因把几万条原始数据全部序列化返回给前端ECharts一次性渲染上万个点。 解决接口层面做降采样按天聚合、按小时聚合而不是返回全部明细。查询用TruncDay替代TruncHour数据量缩小24倍视觉上趋势几乎不变。6. 部署与进阶把Django项目跑在公网服务器上的三个关键步骤6.1 用uWSGINginx把Django跑起来本地跑通之后你要把这套可视化系统部署到Linux服务器上给评委演示。Django自带的开发服务器绝不能用于生产常见部署是Nginx反向代理加uWSGI。核心命令如下# 在服务器项目根目录下启动uWSGI监听9000端口 uwsgi --http :9000 --module pm25.wsgi --processes 4 --threads 2 --stats :9191--module pm25.wsgi指向项目里pm25/wsgi.py这个入口。--processes 4表示启动4个子进程处理并发请求如果你服务器只有1核CPU这个值设2就够了——之前我在2核4G的阿里云主机上设了8个进程内存直接飙到2.2G页面加载反而变慢。Nginx里做一个简单的反向代理把80端口流量转发到uWSGI的9000端口server { listen 80; server_name your_domain_or_ip; location /static/ { alias /project_root/static/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:9000; } }6.2 采集任务的自动化用cron定时调度还是直接写死数据可视化项目最难的不是画图而是让数据持续更新。如果只有一个静态的db.sqlite3文件评委一问你今天的趋势怎么没有场面会很尴尬。常见做法是写一条crontab任务每小时跑一次采集脚本0 * * * * cd /project_root python manage.py collect_data logs/data.log 21 logs/data.log 21是把正常日志和报错都追加到同一个文件里方便第二天排查接口限流问题。注意crontab的环境变量和登录shell不一致脚本里要用python的绝对路径或者先source虚拟环境激活文件。6.3 给可视化加分的一个技巧页面加载时的缓存策略最后补一个让项目性能提升一个档次的技巧。可视化接口如果每次请求都实时查数据库在并发访问时会快速暴露性能问题。用Django的cache_page装饰器可以轻松把接口响应缓存一段时间from django.views.decorators.cache import cache_page cache_page(60 * 30) # 缓存30分钟 class PM25TrendAPI(View): def get(self, request): # 原有查询逻辑不变这行装饰器会让同一城市30分钟内的所有请求直接走Django缓存框架数据库零压力。默认的缓存后端是本地内存如果你是多进程部署建议把缓存后端换成Redis否则每个进程各缓存一份效果减半。我在这个项目上最大的教训是可视化展示的“好看”永远排在“数据可信”后面。早期为了图面华丽把平滑曲线和数据堆叠效果开到最大结果曲线和实际监测值对不上。后来调整成展示原始平均浓度接口里保留数据更新时间戳页面越朴素反而是答辩时质疑最少的那一版。建议你把数据采集时间、数据来源、数据口径这三个字段做成页面底部的小字标注专业感比任何炫技配色都管用。可视化的本质是让数据说话不是让图表替数据演戏。希望这些经验能让你少走几个弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站