Python+Django城市PM2.5空气质量数据可视化分析完整指南
2026/9/23 1:49:44 网站建设 项目流程

简介:面向高校毕业设计场景的Python+Django城市PM2.5空气质量数据可视化分析项目源码,适合计算机、数据分析、环境科学等专业学生用于课题设计、课设参考与功能扩展。压缩包共64个文件,约12.38MB,包含17个Python源码文件、24个CSV数据文件、10个pyc编译文件、4个XML配置、3个HTML页面,以及SQL数据库文件、TXT说明和README文档,覆盖Django项目配置、数据导入、可视化页面与数据库结构。该项目已有406人浏览学习,能够帮助快速理解Django框架下的数据可视化开发流程。项目中内置北京、上海、广州、成都、沈阳五城市六年PM2.5数据,并整理出时间序列、不同季度、一天中不同时段等分析维度,以及PM2.5与露点、风向、大气压、温度、相对湿度之间关系的统计表;同时提供数据导入脚本和MySQL数据库文件,整体代码结构清晰,便于直接运行、二次开发或作为毕业设计答辩演示素材。

1. Python+Django城市PM2.5空气质量数据可视化分析:先弄清楚这套源码能干什么

标题里这串名字,说白了就是一段用 Python 后端框架 Django 写好的完整网站——上面有登录、有后台、有数据库,前端页面用 ECharts 把城市 PM2.5 浓度、AQI 指数、主要污染物画成折线图、柱状图和地图分布。你下载到的那个 zip 里,通常装着 Django 工程源码和一个现成的数据库文件(SQLite 最常见),解压后理论上敲一条命令就能跑起来,然后从浏览器里看到图表。它解决的核心问题就一句话:把散落各处的空气质量数据,做成一套能查询、能看趋势、能打印报告的 Web 系统。这个方向适合两类人,一是拿它当毕业设计交付的学生,二是刚学完 Django 想找一个“完整链路”练手的初级开发。但说实话,我帮人调过太多这种压缩包了,真正能一次跑通的不到一半——问题大多不在代码本身,而在环境、数据库路径和静态文件这三处,这篇笔记就按我的落地顺序从架构讲到部署。

2. 架构与数据流:从数据采集到图表渲染,Django 在这套系统里管了哪几件事

2.1 Django MTV 模式与工程模块划分

Django 的 MTV(Model-Template-View)和传统 MVC 最直观的区别,是它把“路由+控制器”揉进了 View 层,而 Template 只负责渲染展示。你在压缩包里会看到典型的工程骨架:

django_pm25/ ├── manage.py ├── django_pm25/ # 工程配置目录 │ ├── settings.py │ ├── urls.py │ ├── wsgi.py ├── app_air/ # 空气质量业务 App │ ├── models.py │ ├── views.py │ ├── urls.py │ ├── templates/app_air/ │ └── static/app_air/ └── db.sqlite3

一个正常的做法是把“城市信息”和“空气质量记录”放在同一个业务 App 里,比如app_air,这样 models 之间的外键关系写在同一个文件里,后续做 ORM 查询时不用跨 App。settings.py 里要确认INSTALLED_APPS已经注册这个 App,否则迁移时会出现no such table的报错。

这套划分对应到 MTV 上是这样的:models.py 负责定义表结构(对应 Model),templates 目录下放 HTML 模板(对应 Template),views.py 里处理请求、查数据库、给模板传数据(对应 View);而 urls.py 负责把浏览器地址映射到对应的 view 类或函数。很多新手在这里犯的第一个错,是直接在工程级django_pm25/urls.py里写业务路由,结果把业务 App 写成了一坨不可复用的代码。我一般会把属于业务的路由全部下沉到app_air/urls.py,工程级只是include一下,这样后期加新页面只需要在业务 App 里改。

2.2 数据流设计:采集层、存储层、接口层、展示层

这一整套系统我最看重的是数据流,因为它直接决定了你后面要写哪些模块。以最常见的毕业设计功能清单来拆,这四层跑下来模型是这样:

公开数据源(爬虫/CSV) → SQLite/MySQL 数据库 → Django ORM 查询 → JSON API 接口 → ECharts 渲染页面
  • 采集层:两种做法。一种是写爬虫用 requests 从公开空气质量站点抓当天数据,另一种是拿现成的 CSV 数据文件做导入。毕设阶段建议两个都保留:CSV 导入保证演示现场断网也能跑,爬虫脚本作为“数据更新”功能的加分项。
  • 存储层:Django 默认的 SQLite 足够支撑几千条记录,答辩演示完全够用。如果你的数据库文件名在 settings.py 中被写死成了D:/xxx/db.sqlite3这种绝对路径,那换电脑基本必炸,后面避坑章节我会专门讲。
  • 接口层:Django 视图函数从数据库取数,序列化成 JSON 返回。这里不涉及 DRF(Django REST Framework)也可以,毕业设计手工用JsonResponse反而更容易讲清楚。
  • 展示层:前端模板用 ECharts 的ajax拉取接口数据,渲染折线图、柱状图和地图。要注意的是,ECharts 官方示例经常直接写成fetch请求,而 Django 模板里有csrf_token的语法,和 ECharts 本身没有冲突,但如果你在模板里插原生<script>标签,注意保持 JavaScript 的变量名和 Python 传过来的 JSON 字段名严格一致。

2.3 环境准备:Python 版本、虚拟环境与依赖安装

先说版本:Django 3.2 LTS 及以上都可以跑这套工程,Python 用 3.8 到 3.11 之间最稳。别一上来就装 Python 3.13 配最新 Django 5.x,某些第三方库(比如 MySQL 驱动)在最新版本上可能出现预编译问题,没必要在这个环节消耗精力。

推荐在项目根目录建虚拟环境,Windows 下的命令如下:

python -m venv venv venv\Scripts\activate pip install -r requirements.txt

requirements.txt我一般手动维护,最少包含这几项:

Django>=3.2,<5.0 requests beautifulsoup4 pandas

其中 pandas 只在导入 CSV 时用到,如果你用纯 Python 标准库csv模块处理,可以直接删掉这一行,少一个重量级依赖,虚拟环境体积会小很多。为什么要用虚拟环境?因为 DEBUG 模式下很多包冲突不显山露水,等项目部署到服务器,pip 装了一堆全局包,版本错乱后你根本不知道是哪个库在打架,虚拟环境至少把黑匣子缩小到一个 requirements.txt 里。

装完依赖后先跑python manage.py migrate把 Django 内置表建出来,再跑python manage.py runserver验证工程能起来。注意,如果此时直接访问首页报 404,不要慌,这代表数据库连接没问题,只是还没配置路由。

3. 数据库建模与数据灌入:models 设计、CSV 导入与爬虫两条路径

3.1 用 models 建两张核心表:城市表与空气质量日报表

城市 PM2.5 分析的核心是两张表:城市基础信息表和每日空气质量记录表。前者撑起地图分布和城市下拉框,后者作为事实表存数值。我在 models.py 里的设计是这样:

from django.db import models class City(models.Model): name = models.CharField(max_length=32, unique=True, verbose_name='城市名称') province = models.CharField(max_length=32, blank=True, verbose_name='省份') lon = models.FloatField(verbose_name='经度') lat = models.FloatField(verbose_name='纬度') class Meta: ordering = ['name'] def __str__(self): return self.name class AirQuality(models.Model): city = models.ForeignKey(City, on_delete=models.CASCADE, verbose_name='所属城市') date = models.DateField(verbose_name='监测日期') pm25 = models.FloatField(verbose_name='PM2.5 浓度(μg/m³)') pm10 = models.FloatField(verbose_name='PM10 浓度(μg/m³)') so2 = models.FloatField(verbose_name='SO2 浓度(μg/m³)') no2 = models.FloatField(verbose_name='NO2 浓度(μg/m³)') co = models.FloatField(verbose_name='CO 浓度(mg/m³)') o3 = models.FloatField(verbose_name='O3 浓度(μg/m³)') aqi = models.IntegerField(verbose_name='AQI 指数') level = models.CharField(max_length=16, verbose_name='空气质量等级') primary_pollutant = models.CharField(max_length=32, blank=True, verbose_name='首要污染物') class Meta: unique_together = ('city', 'date') ordering = ['date'] def __str__(self): return f'{self.city.name} {self.date}'

设计说明:

  • unique_together是这套建模里最关键的约束。同一城市同一天只能有一条记录,这条约束既阻止了爬虫重复写入,也让后面的update_or_create有了判断依据,如果去掉它,数据导入只会越导越脏。
  • 外键用on_delete=models.CASCADE,城市一旦删除,它下面的空气质量记录全部清掉。这对毕设没什么问题,但如果你后面想保留历史数据做审计,就得改成SET_NULL搭配一个默认的“未知城市”记录。
  • 字段类型上pm25pm10这些浓度用FloatField,AQI 必须是IntegerField,因为 AQI 的计算结果只取整数部分,用浮点反而会在前端显示一堆小数点。
  • levelprimary_pollutant在爬虫里很好拿,但如果你的数据源只有六项污染物没有等级和首要污染物,就要在导入时自己推算,这个逻辑我放在 3.3 节讲。

生成迁移的命令是:

python manage.py makemigrations app_air python manage.py migrate

说到风控提示:数据源上不要注册登录、抓取接口不弹性限频,学会 sleep。

3.2 从 CSV 批量灌数据:一个独立的数据导入脚本

毕设项目的数据通常是一张 CSV,列名一般是城市、日期、PM2.5、PM10、SO2、NO2、CO、O3、AQI、等级。我们要写一个 Django 原生 shell 里的脚本,把它灌进数据库。Django 提供了一条最简单的路径:在项目根目录写import_csv.py,然后在manage.py shell里执行。

import csv import django import os os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_pm25.settings') django.setup() from app_air.models import City, AirQuality def run(csv_path): with open(csv_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) count = 0 for row in reader: city_name = row['城市'].strip() city, _ = City.objects.get_or_create( name=city_name, defaults={'province': row.get('省份', ''), 'lon': 0, 'lat': 0} ) AirQuality.objects.update_or_create( city=city, date=row['日期'], defaults={ 'pm25': float(row['PM2.5']), 'pm10': float(row['PM10']), 'so2': float(row['SO2']), 'no2': float(row['NO2']), 'co': float(row['CO']), 'o3': float(row['O3']), 'aqi': int(float(row['AQI'])), 'level': row['等级'], 'primary_pollutant': row.get('首要污染物', ''), } ) count += 1 print(f'成功写入 {count} 条记录') if __name__ == '__main__': run('data/city_air_2024.csv')

逻辑说明:

  • 脚本开头四行os.environ.setdefaultdjango.setup()是让 Django ORM 能在独立脚本里运行的固定套路,少了它们你会得到django.core.exceptions.ImproperlyConfigured报错。
  • get_or_create负责城市表去重,update_or_create负责按(city, date)去重,这就是上面unique_together约束的代码侧呼应。第二次导入同一份 CSV 时,数据会更新而不是插入重复行,这也是血泪经验——很多同学的数据库跑了几次爬虫后多出几百条重复记录,图表的趋势直接变成锯齿。
  • 城市经纬度在 CSV 里可能没有,这里先用 0 占位,后续在地图页面显示完后再补真实经纬度。这不是偷懒,而是避免 CSV 数据质量太差导致导入中断。

执行方式:

python manage.py shell < import_csv.py

如果你希望把脚本做成一个可以反复调用的模块,可以在app_air下新建management/commands/目录,写一个import_csv.py的自定义命令,这样就能用python manage.py import_csv data.csv执行,答辩演示时也更像样。

3.3 爬虫增量更新:requests + BeautifulSoup 抓公开页面

如果说 CSV 导入是“打底数据”,爬虫脚本就是“数据保鲜”。常见的做法是抓取公开数据平台页面,解析表格后存入模型。为了不影响演示,一定要让爬虫脚本断网也能走,也就是只作为增量更新工具。

import time import requests from bs4 import BeautifulSoup def fetch_realtime(city_list): headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'} for city in city_list: url = f'https://example-air-source.com/city?id={city}' try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() soup = BeautifulSoup(resp.text, 'html.parser') rows = soup.select('table tbody tr') for row in rows[:10]: tds = [td.text.strip() for td in row.find_all('td')] # 假设列顺序: 日期 PM2.5 PM10 SO2 NO2 CO O3 AQI 等级 save_air_quality(city, tds) except Exception as e: print(f'{city} 抓取失败: {e}') time.sleep(2) def save_air_quality(city_name, tds): # 省略 models 写入逻辑,与 3.2 的 update_or_create 一致 pass

参数与容错说明:

  • User-Agent必须伪装成浏览器,很多站点对默认的python-requests是直接拒绝的,这是最基础的“反爬”应对。别问我怎么知道,第一次写爬虫时没有 UA,死活拿不到数据,加完 UA 立刻通了。
  • time.sleep(2)是给目标网站的礼貌性限速。两个城市之间必须等待,否则几十个城市的请求会在几分钟内把你的 IP 打进封禁名单,报错 403 或 503 时先检查这里。
  • 抓到的数据要二次校验——tds的长度是否符合预期,日期格式是否是YYYY-MM-DD,PM2.5 数值是否大于等于 0。脏数据入库的后果比没有数据严重得多,图表上一个 -999 的异常值会把整条折线的量程拉飞。
  • 更稳妥的增强方案是加一个 3 次重试机制,超过 3 次直接放弃这个城市,继续下一个。

3.4 完整链路:跑了导入脚本后,用 Django ORM 验证数据是否入库

不管数据是从 CSV 还是爬虫进来,灌完数据后都要验一验。这里给你一个最直观的查询验证,也是面试官最爱问的 ORM 操作:

from app_air.models import AirQuality from django.db.models import Avg result = AirQuality.objects.values('date').annotate(avg_pm25=Avg('pm25')) print(list(result[:10]))

这个查询的含义是:按日期分组,计算所有城市当天的 PM2.5 平均值,取前 10 天。values('date')生成 GROUP BY 子句,annotate(avg_pm25=Avg('pm25'))生成 AVG 聚合字段,得到的result是一个包含字典的 QuerySet。这是做全城趋势图最底层的查询模型,后面的可视化接口就是在这个逻辑上加城市过滤和日期区间。

如果查询结果为空,优先检查AirQuality.objects.count(),如果为 0 说明数据导入环节出了问题,而不是查询逻辑有问题。把验证写在导入脚本的关键节点上,能让你的数据链路上线时少踩一半的坑。

4. 可视化与 ECharts:后端接口返回 JSON 的姿势与前端渲染的三个坑

4.1 用 JsonResponse 写查询接口带日期区间和城市参数

可视化页面的数据来源是一个 JSON 接口,我们用 Django 原生视图实现,核心是查询参数校验和 JsonResponse 使用。下面代码在你下载的源码里应该能找到类似版本,如果没有,照着写就行。

import json from datetime import datetime from django.http import JsonResponse from django.views.decorators.http import require_GET from app_air.models import City, AirQuality @require_GET def pollution_api(request): city_name = request.GET.get('city', '北京') start_date = request.GET.get('start') end_date = request.GET.get('end') try: city = City.objects.get(name=city_name) except City.DoesNotExist: return JsonResponse({'status': 1, 'msg': '城市不存在'}, status=404) qs = AirQuality.objects.filter(city=city) if start_date: qs = qs.filter(date__gte=start_date) if end_date: qs = qs.filter(date__lte=end_date) qs = qs.order_by('date') data = { 'date': [], 'pm25': [], 'aqi': [], } for item in qs: data['date'].append(item.date.strftime('%Y-%m-%d')) data['pm25'].append(item.pm25) data['aqi'].append(item.aqi) return JsonResponse({'status': 0, 'data': data})

逻辑说明:

  • @require_GET装饰器限定这个视图只接受 GET 请求,前端用 ECharts 的数据请求是标准 GET,不需要 POST。
  • date__gtedate__lte是 Django 的日期字段查询语法,对应 SQL 中的>=<=,支持直接用字符串如2024-01-01,Django 会自动解析成日期对象。
  • strftime('%Y-%m-%d')把日期格式化成字符串,这样前端 JavaScript 可以直接显示,不用再处理 Python 的 datetime 对象序列化问题。
  • 这里没有加缓存。如果是展示用的热点页面,建议用django.core.cachecache.set/cache.get把接口返回结果存 5 分钟,减轻数据库压力。这个可以在加分项里写,但别在最基础的版本里用,否则答辩时讲不清楚就没必要。

4.2 模板里的 ECharts 渲染:折线图加城市下拉框

前端页面用fetch或 jQuery 的ajax请求上面的接口,把返回数据塞进 ECharts 的 option 里。ECharts 是可视化的事实标准,你不需要自己画 canvas,核心工作是维护好 option 对象。

{% load static %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>PM2.5 趋势分析</title> <script src="{% static 'app_air/echarts.min.js' %}"></script> </head> <body> <select id="citySelect"> <option value="北京">北京</option> <option value="上海">上海</option> <option value="广州">广州</option> </select> <div id="chart" style="width: 900px; height: 500px;"></div> <script> let chart = echarts.init(document.getElementById('chart')); function loadData(city) { fetch(`/api/pollution/?city=${encodeURIComponent(city)}&start=2024-01-01&end=2024-12-31`) .then(res => res.json()) .then(json => { if (json.status !== 0) return; chart.setOption({ title: { text: `${city} PM2.5 日均浓度` }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: json.data.date }, yAxis: { type: 'value', name: 'μg/m³' }, series: [{ name: 'PM2.5', type: 'line', data: json.data.pm25 }] }); }); } document.getElementById('citySelect').addEventListener('change', function() { loadData(this.value); }); loadData('北京'); </script> </body> </html>

这里的核心坑有三个:

第一,echarts.min.js必须放在 static 目录里并用{% static %}标签引用,而不是直接写/static/app_air/echarts.min.js。Django 的静态文件查找机制在 DEBUG 模式下会按STATICFILES_DIRS和每个 App 的static/目录展开,写死相对路径在路径变化时会直接 404。

第二,fetch中拼 URL 时要用encodeURIComponent(city),否则城市名里的中文会变成不可读的编码,特别是当你的城市名含“重庆”这样的多音字,极容易在 URL 解析时出隐性 bug。

第三,chart.setOption在切换城市时要先chart.clear(),否则旧数据残留在画布上。ECharts 的 setOption 默认是合并模式,不 clear 的话新旧 series 数据会重叠,折线会变成一坨毛线。这是可视化最常见的翻车现场。

4.3 视图与模板函数:城市列表注入页面的方法

上面的<select>下拉框是写死的三个城市。更好的做法是从数据库读出所有城市,动态填充。Django 模板里可以用for循环,视图代码:

from django.shortcuts import render def chart_view(request): cities = City.objects.values('id', 'name') active_city = request.GET.get('city', '北京') return render(request, 'app_air/chart.html', { 'cities': cities, 'active_city': active_city, })

模板中:

<select id="citySelect"> {% for c in cities %} <option value="{{ c.name }}" {% if c.name == active_city %}selected{% endif %}>{{ c.name }}</option> {% endfor %} </select>

这里的values('id', 'name')City.objects.all()更轻,只查询需要的字段,模板里也不需要访问c.id之外的属性。选中态用{% if %}条件渲染,切页时能保留选择状态,体验提升一个档次,而且代码量不增加。

urls.py 里的配置:

from django.urls import path from app_air import views urlpatterns = [ path('chart/', views.chart_view, name='chart_view'), path('api/pollution/', views.pollution_api, name='pollution_api'), ]

4.4 页面里加一张城市地图分布图:适可而止的加分项

如果只做折线图和柱状图,这个项目的完成度已经能支撑毕业设计答辩。加分项是加一张中国地图显示各省 PM2.5 浓度,颜色深浅代表污染程度。ECharts 5 已经内置了地图组件的registerMap,但地图 JSON 数据默认放在 CDN,答辩现场断网就白屏。

常用的做法是下载一份中国地图 GeoJSON 文件,存到static/app_air/china.json,然后在代码里:

fetch('/static/app_air/china.json') .then(res => res.json()) .then(geoJson => { echarts.registerMap('china', geoJson); chart.setOption({ series: [{ type: 'map', map: 'china' }] }); });

地图 JSON 文件可以是 100% 离线的,但要注意它的坐标系版本和省份名称,必须是“北京”“上海”这种简写,而不是“北京市”。如果注册地图报错 “GeoLite2 database not found” 或 “Invalid geoJson”,先检查 JSON 里省份名称是否与你的城市表外键对齐。

我的建议是,地图作为“最后一页”,其他数据图表全部正常后再加,不要一上来就啃坐标系。

5. 避坑:毕业设计常踩的 8 个经典疑难杂症

5.1 现象:解压源码后 runserver 起不来,提示 “no such table: app_air_airquality”

原因:这是最常见的新手问题。源码里附带的是数据库文件,但没有执行迁移,Django 工程不知道表结构。数据库文件为空或不存在,Migrate可以建表,但数据不会自动生成。

解决:先跑python manage.py migrate创建表结构,再把压缩包里的数据导入脚本执行一遍。如果数据库文件已经在压缩包里,确认 settings.py 的DATABASES配置指向的是BASE_DIR / 'db.sqlite3',而不是某个写死的绝对路径:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } }

若下载的源码里 database 目录下有.sqlite3文件,直接改这个路径即可。这里要特别提一句:如果压缩包里的数据库文件名和 settings 里不一致(比如pm25_data.sqlite3db.sqlite3),你会看到一个奇怪的现象——程序不报错,但页面上一直没有任何数据。先ls database/看文件名对不对。

5.2 现象:ECharts 图表一直空白,控制台报 “geojson is null”

原因:地图注册失败,最常见的原因是 GeoJSON 文件没有下载到本地。很多教程给出的是https://cdn.jsdelivr.net/...的在线地址,学校网络环境和演示环境常常无法访问。

解决:把 GeoJSON 下载到static/app_air/china.json,页面用相对路径引用;同时确认STATIC_URL配置正确。一个降级方案是:如果地图实在搞不定,就只用折线图展示数据——毕业答辩并不强制要求地图模块,把趋势分析做扎实比堆图表更重要。

5.3 现象:切换城市后图表两条折线重叠,数值错乱

原因:EChartssetOption的合并机制导致旧数据没清掉。具体表现是:第一次加载北京的正常,切到上海后北京的数据还在图上。

解决:在chart.setOption前调用chart.clear(),或者给 series 的name做整体覆盖。推荐 clear() 方案,逻辑简单:

chart.clear(); chart.setOption({ /* 完整配置 */ });

5.4 现象:查询接口返回正常,但页面请求显示 403

原因:Django 默认开启了 CSRF 中间件,对 POST 请求会校验 token。虽然这个接口是 GET,但如果你在某个页面里用了 POST 提交筛选条件,就会触发 CSRF 验证。

解决:视图上加@csrf_exempt装饰器(不推荐),或者在前端页面里加上 Django 模板提供的{% csrf_token %},并把 token 带给后端。对毕业设计来说,简单方案是把所有查询都改成 GET,避免 CSRF 的复杂性。

5.5 现象:数据库连接 MySQL 报错 “Unknown collation utf8mb4_0900_ai_ci”

原因:这是 MySQL 8.0 默认的排序规则,而你的服务器或同学的 SQLite 不支持某些特性,或 MySQL 5.7 不兼容。

解决:建库时手动指定:

CREATE DATABASE pm25_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

数据库连接配置里加上:

'OPTIONS': {'charset': 'utf8mb4', 'sql_mode': 'STRICT_TRANS_TABLES'}

这个问题在 Windows + MySQL 5.7 的环境下玄学频发,换成 utf8mb4_general_ci 之后通常就通了。

5.6 现象:SQLite 版本的日期筛选永远查不到数据

原因:SQLite 对日期比较是文本比较,如果你的date字段里存的是2024-01-01这种标准格式,不会有问题;但如果存的是2024/01/01或者1/1/2024date__gte就会变成字典排序,结果完全不符合预期。

解决:在导入数据时统一datetime.strptime(row['日期'], '%Y-%m-%d').date()格式化,入库前把格式锁死,别存字符串。

另:如果你的数据库迁移后又改过 models 字段,建议先删掉数据库文件重新migrate。毕业设计不需要考虑数据积累,删了重建成本最低。

5.7 现象:Windows 上python manage.py runserver启动成功但局域网访问不了

原因:Django 默认监听 127.0.0.1,同事的手机和会议室电脑访问不到你的开发服务器。

解决:

python manage.py runserver 0.0.0.0:8000

同时确认 Windows 防火墙放行了 8000 端口,这是答辩演示前必须提前测试的一步。

5.8 现象:页面样式全部丢失,控制台一堆静态资源 404

原因:settings.py 里的DEBUG = False时,Django 默认不再提供静态文件服务。

解决:为演示页面临时把DEBUG改回True,或者用whitenoise托管静态文件。更简单的命令行方案是在工程级 urls.py 里加一条:

from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

6. 从开发到可部署:用 waitress 跑起来,再给项目加一张“日报卡片”

6.1 用 waitress 在 Windows 上替代 runserver

runserver 是开发服务器,性能差、不支持并发,答辩演示时如果同时刷新页面较多,会出现明显的响应变慢。Windows 下最省事的部署方案是 waitress,纯 Python、无需编译:

pip install waitress waitress-serve --listen=0.0.0.0:8000 django_pm25.wsgi:application

如果服务器是 Linux,习惯上用 gunicorn;Windows 专属就选 waitress。启动后用http://127.0.0.1:8000验证,然后该写关机脚本就写关机脚本,演示前把这行命令放到启动 bat 文件里双击执行,能减少很多现场尴尬。

6.2 配一张 nginx 反向代理并托管 static 文件

线上环境最常见的组合是 waitress 处理 Python 应用,nginx 处理静态文件和反向代理。Django 这边先把静态文件收集到一个目录:

python manage.py collectstatic

nginx 配置核心片段:

server { listen 80; server_name your_server_ip; location /static/ { alias /var/www/django_pm25/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

配置完记得nginx -t校验语法再nginx -s reload。需要注意的细节:location /static/alias的路径结尾都要有斜杠,少一个斜杠就 404,这是 nginx 配置里最经典的翻车点。

6.3 增量价值:给系统加一张每日空气质量等级统计卡片

当你把链路跑通之后,我想再分享一个能显著提升完成度的功能,做法是写一个定时计算任务,把最近一天各城市的空气质量等级统计成一个文本卡片,落实到系统的首页或单独接口上。

from django.db.models import Count from app_air.models import AirQuality def daily_summary(date): rows = AirQuality.objects.filter(date=date).values('level').annotate( total=Count('level') ).order_by('-total') if not rows: return '今日暂无数据' summary = [f"{row['level']}: {row['total']}城" for row in rows] return '、'.join(summary)

这段代码按空气质量等级分组,统计某天每个等级的城市数量,返回一串比如“良: 12城、优: 8城、轻度污染: 3城”的文本。配合 Django 的crontab或者 Windows 计划任务每天跑一次,就能把整个系统从“能看趋势”升级到“会自动汇报”,这在答辩现场提出来是很加分的点。

我的习惯是,拿到任何一套新环境,先手工跑一次python manage.py shell < import_csv.py确认数据链路通,再用测试用例保护update_or_create的幂等性,最后才写定时任务。定时任务刚写完一两天不出问题不代表没问题,跨月的日期边界最容错,记得加try-except捕获并打印异常,否则半夜定时任务崩了,第二天早上你对着一个空数据库懵掉。

说起来,我最早自己写这套系统时也栽过不少跟头,尤其 SQLite 文件被 IDE 锁死导致数据库操作直接报错那回,折腾到凌晨两点才发现是 Pycharm 的数据库插件占用了文件。后来用回命令行和显式的数据库配置,这些坑基本就绕开了。希望这篇笔记里的设计思路和排错经验能帮到你,至少让你解压那个 zip 后不会再对着 runserver 的报错发呆。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询