基于Flask的空气质量监测与预测分析系统

基于Flask的空气质量监测与预测分析系统 毕业论文

毕业设计


一、项目名称:

基于Flask的空气质量监测与预测分析系统


【摘要】

随着我国城市化进程的加快和工业化水平的提高,空气质量问题日益成为社会关注的焦点。本文设计并实现了一个基于Flask框架的空气质量监测与预测分析系统,旨在为用户提供全面、直观的空气质量数据服务。系统采用Python语言开发,以Flask作为Web框架,MySQL作为数据存储,结合BeautifulSoup和Requests库实现空气质量数据的自动化采集,利用Scikit-learn机器学习库中的线性回归算法实现AQI预测功能,并通过ECharts可视化库实现数据的多维度展示。

系统实现了数据采集、数据预览、年度分析、月度分析、气体分析、城市分布、城市对比、相关性分析、季节分析、空气预警和AQI预测等核心功能模块。通过对北京、上海、广州、深圳等12个主要城市的空气质量数据进行采集和分析,系统能够展示各城市的AQI指数、PM2.5、PM10、SO2、NO2、CO、O3等污染物浓度的变化趋势,并基于历史数据对未来空气质量进行预测预警。系统采用响应式设计,界面友好,操作便捷,为环境监测部门、科研机构和普通公众提供了一个实用的空气质量信息服务平台。

【关键词】 Flask框架 空气质量监测 AQI预测 数据可视化 机器学习

【Abstract】

With the acceleration of urbanization and the improvement of industrialization in China, air quality has increasingly become a focus of social concern. This paper designs and implements an air quality monitoring and prediction analysis system based on the Flask framework, aiming to provide users with comprehensive and intuitive air quality data services. The system is developed using Python language, with Flask as the web framework and MySQL as data storage. It combines BeautifulSoup and Requests libraries to achieve automated air quality data collection, uses the linear regression algorithm from the Scikit-learn machine learning library to implement AQI prediction, and utilizes the ECharts visualization library for multi-dimensional data display.

The system implements core functional modules including data collection, data preview, annual analysis, monthly analysis, gas analysis, city distribution, city comparison, correlation analysis, seasonal analysis, air warning, and AQI prediction. By collecting and analyzing air quality data from 12 major cities including Beijing, Shanghai, Guangzhou, and Shenzhen, the system can display the trends of AQI index, PM2.5, PM10, SO2, NO2, CO, O3 and other pollutant concentrations in each city, and predict and warn future air quality based on historical data. The system adopts responsive design with a user-friendly interface and convenient operation, providing a practical air quality information service platform for environmental monitoring departments, research institutions, and the general public.

【Keywords】 Flask Framework Air Quality Monitoring AQI Prediction Data Visualization Machine Learning


目 录

[1 绪 论](#1 绪 论)

  • [1.1 课题研究背景](#1.1 课题研究背景)
  • [1.2 课题研究的目的、意义](#1.2 课题研究的目的、意义)
  • [1.3 课题的国内外研究现状和发展动态](#1.3 课题的国内外研究现状和发展动态)
  • [1.4 研究内容](#1.4 研究内容)
  • [1.5 论文结构安排](#1.5 论文结构安排)

[2 关键技术](#2 关键技术)

  • [2.1 Flask Web框架](#2.1 Flask Web框架)
  • [2.2 数据采集技术](#2.2 数据采集技术)
  • [2.3 MySQL数据库](#2.3 MySQL数据库)
  • [2.4 机器学习预测技术](#2.4 机器学习预测技术)
  • [2.5 ECharts数据可视化](#2.5 ECharts数据可视化)
  • [2.6 技术特色与创新点](#2.6 技术特色与创新点)

[3 系统分析与设计](#3 系统分析与设计)

  • [3.1 系统总体设计](#3.1 系统总体设计)
    • [3.1.1 系统架构设计](#3.1.1 系统架构设计)
    • [3.1.2 系统数据流设计](#3.1.2 系统数据流设计)
    • [3.1.3 系统功能模块设计](#3.1.3 系统功能模块设计)
  • [3.2 系统详细设计](#3.2 系统详细设计)
    • [3.2.1 用户认证模块设计](#3.2.1 用户认证模块设计)
    • [3.2.2 数据采集模块设计](#3.2.2 数据采集模块设计)
    • [3.2.3 数据分析模块设计](#3.2.3 数据分析模块设计)
    • [3.2.4 AQI预测模块设计](#3.2.4 AQI预测模块设计)
    • [3.2.5 空气预警模块设计](#3.2.5 空气预警模块设计)
    • [3.2.6 数据可视化模块设计](#3.2.6 数据可视化模块设计)
    • [3.2.7 后台管理模块设计](#3.2.7 后台管理模块设计)
  • [3.3 数据库设计](#3.3 数据库设计)
    • [3.3.1 数据库关系设计](#3.3.1 数据库关系设计)

[4 系统实现](#4 系统实现)

  • [4.0 系统整体实现流程](#4.0 系统整体实现流程)
  • [4.1 用户认证模块的实现](#4.1 用户认证模块的实现)
  • [4.2 数据采集模块的实现](#4.2 数据采集模块的实现)
  • [4.3 首页数据展示的实现](#4.3 首页数据展示的实现)
  • [4.4 年度与月度分析的实现](#4.4 年度与月度分析的实现)
  • [4.5 AQI预测模块的实现](#4.5 AQI预测模块的实现)
  • [4.6 空气预警模块的实现](#4.6 空气预警模块的实现)
  • [4.7 城市对比与相关性分析的实现](#4.7 城市对比与相关性分析的实现)
  • [4.8 后台管理模块的实现](#4.8 后台管理模块的实现)
  • [4.9 系统集成与部署](#4.9 系统集成与部署)

[5 系统测试](#5 系统测试)

  • [5.1 系统功能测试](#5.1 系统功能测试)
    • [5.1.1 用户认证功能测试用例](#5.1.1 用户认证功能测试用例)
    • [5.1.2 数据管理功能测试用例](#5.1.2 数据管理功能测试用例)
    • [5.1.3 数据分析功能测试用例](#5.1.3 数据分析功能测试用例)
    • [5.1.4 AQI预测功能测试用例](#5.1.4 AQI预测功能测试用例)
    • [5.1.5 后台管理功能测试用例](#5.1.5 后台管理功能测试用例)
  • [5.2 系统性能测试](#5.2 系统性能测试)

[6 总结与展望](#6 总结与展望)

  • [6.1 总结](#6.1 总结)
  • [6.2 展望](#6.2 展望)

参考文献

致谢


1 绪 论

1.1 课题研究背景

近年来,随着我国经济的快速发展和城市化进程的不断推进,空气污染问题日益严峻,已成为影响人民群众身体健康和生活质量的重要因素。根据生态环境部发布的数据显示,虽然我国空气质量总体呈现改善趋势,但部分地区在特定时段仍会出现重度污染天气,PM2.5、臭氧等污染物浓度超标现象时有发生。空气质量的好坏直接关系到公众的呼吸健康,尤其对老年人、儿童和呼吸系统疾病患者影响更为显著。

传统的空气质量监测主要依赖于政府环保部门设立的固定监测站点,数据发布渠道相对单一,普通公众获取信息的便捷性不足。同时,现有的空气质量信息服务多以简单的数据展示为主,缺乏对历史数据的深度分析和未来趋势的预测功能,难以满足用户对空气质量信息的多元化需求。因此,开发一个集数据采集、存储、分析、预测和可视化展示于一体的空气质量监测系统具有重要的现实意义。

Python作为一种简洁高效的编程语言,在数据处理和Web开发领域具有广泛的应用。Flask框架以其轻量级、灵活性强的特点,成为构建中小型Web应用的理想选择。结合MySQL数据库的稳定存储能力、BeautifulSoup的网页解析功能、Scikit-learn的机器学习算法以及ECharts的数据可视化能力,可以构建一个功能完善、性能稳定的空气质量监测与预测分析系统。

1.2 课题研究的目的、意义

1.2.1 研究目的

本课题旨在设计并实现一个基于Flask框架的空气质量监测与预测分析系统,主要研究目的包括以下几个方面。首先,实现空气质量数据的自动化采集,通过网络爬虫技术从天气后报网站获取北京、上海、广州、深圳、杭州、成都、沈阳、南京、长沙、南昌、天津、重庆等12个主要城市的AQI指数及各项污染物浓度数据。其次,构建完善的数据存储和管理体系,利用MySQL数据库对采集的空气质量数据进行结构化存储,支持数据的高效查询和统计分析。第三,实现多维度的数据分析功能,包括年度趋势分析、月度变化分析、城市对比分析、污染物相关性分析和季节性分析等。第四,基于机器学习算法实现AQI预测功能,利用线性回归模型根据PM2.5、SO2、NO2、O3等污染物浓度预测空气质量指数。最后,开发空气质量预警功能,根据历史数据趋势和预测结果,为用户提供未来空气质量的预警提示。

1.2.2 研究意义

本课题的研究具有重要的理论意义和实践价值。从理论层面来看,本研究将Web开发技术、数据采集技术、机器学习算法和数据可视化技术进行有机整合,探索了多技术融合在环境监测领域的应用模式,为类似系统的开发提供了技术参考。从实践层面来看,系统为用户提供了便捷的空气质量信息查询渠道,用户可以通过Web界面随时了解各城市的空气质量状况,查看历史数据变化趋势,获取未来空气质量预测信息,从而合理安排出行和户外活动。此外,系统的数据分析功能可以帮助环境研究人员发现空气质量变化规律,为环境治理决策提供数据支持。

1.3 课题的国内外研究现状和发展动态

1.3.1 国外研究现状

在空气质量监测领域,发达国家起步较早,已建立了较为完善的监测网络和信息服务体系。美国环境保护署(EPA)建立了全国性的空气质量监测网络,通过AirNow平台向公众提供实时空气质量数据和健康建议。欧洲环境署(EEA)整合了欧盟各成员国的空气质量监测数据,提供跨国界的空气质量信息服务。在技术应用方面,国外研究者积极探索机器学习和深度学习技术在空气质量预测中的应用,利用随机森林、支持向量机、神经网络等算法构建预测模型,取得了较好的预测效果。同时,物联网技术的发展推动了低成本空气质量传感器的普及,使得空气质量监测网络的覆盖范围不断扩大。

1.3.2 国内研究现状

我国空气质量监测工作近年来取得了长足进步。生态环境部建立了覆盖全国的空气质量监测网络,在338个地级及以上城市设立了1436个国控监测站点,实现了空气质量数据的实时发布。在信息服务方面,各级环保部门开发了多种空气质量信息发布平台和移动应用,为公众提供便捷的查询服务。在学术研究领域,国内学者在空气质量预测模型、污染物来源解析、健康影响评估等方面开展了大量研究工作。然而,现有系统多侧重于数据展示功能,在数据深度分析、智能预测预警等方面仍有提升空间。

1.3.3 发展趋势

空气质量监测技术正朝着智能化、精细化、服务化的方向发展。人工智能技术的应用使得空气质量预测的准确性不断提高,深度学习模型能够捕捉复杂的时空特征,提供更精准的预测结果。大数据技术的发展使得海量监测数据的存储和分析成为可能,为发现空气质量变化规律提供了技术支撑。移动互联网的普及推动了空气质量信息服务向个性化、场景化方向发展,用户可以根据自身需求获取定制化的空气质量信息和健康建议。

1.4 研究内容

本课题的研究内容主要包括以下几个方面。在数据采集方面,设计并实现基于BeautifulSoup和Requests库的网络爬虫程序,从天气后报网站自动采集12个主要城市的空气质量数据,包括AQI指数、空气质量等级、PM2.5、PM10、SO2、NO2、CO、O3等指标。在系统架构方面,采用Flask框架构建Web应用,设计合理的路由结构和视图函数,实现前后端的数据交互。在数据存储方面,设计MySQL数据库表结构,包括空气质量原始数据表、城市年月统计表、用户信息表等,支持数据的高效存储和查询。在数据分析方面,实现年度趋势分析、月度变化分析、气体成分分析、城市分布分析、城市对比分析、相关性分析和季节性分析等功能。在预测预警方面,基于Scikit-learn库实现线性回归预测模型,根据污染物浓度预测AQI指数,并结合历史数据趋势分析实现空气质量预警功能。在可视化展示方面,利用ECharts库实现折线图、柱状图、饼图、地图、热力图等多种图表的展示。

1.5 论文结构安排

本论文共分为六章,各章节内容安排如下。第一章绪论部分介绍了课题的研究背景、研究目的和意义、国内外研究现状以及研究内容。第二章关键技术部分详细介绍了系统开发所采用的Flask框架、数据采集技术、MySQL数据库、机器学习预测技术和ECharts可视化技术。第三章系统分析与设计部分阐述了系统的总体架构设计、功能模块设计和数据库设计。第四章系统实现部分详细描述了各功能模块的具体实现过程,包括核心代码和实现效果。第五章系统测试部分介绍了系统的功能测试和性能测试情况。第六章总结与展望部分对全文进行总结,并对系统的未来发展方向进行展望。


2 关键技术

2.1 Flask Web框架

Flask是一个使用Python语言编写的轻量级Web应用框架,由Armin Ronacher于2010年发布。与Django等全功能框架不同,Flask采用微框架的设计理念,核心功能精简,但通过丰富的扩展机制可以灵活地添加所需功能。Flask框架的核心依赖包括Werkzeug工具库和Jinja2模板引擎,前者提供了WSGI工具集和路由系统,后者提供了强大的模板渲染能力。

在本系统中,Flask框架承担了Web应用的核心架构职责。系统通过app.py文件创建Flask应用实例,使用装饰器定义路由规则,将URL路径映射到对应的视图函数。例如,系统定义了"/login"路由处理用户登录请求,"/index"路由处理首页数据展示,"/air_year_ana"路由处理年度分析功能等。Flask的session机制用于管理用户会话状态,实现用户登录状态的保持和权限控制。系统还利用Flask的before_request钩子函数实现请求拦截,对未登录用户进行访问控制。Jinja2模板引擎用于渲染HTML页面,通过模板继承机制实现页面布局的复用,base.html作为基础模板定义了页面的整体结构,其他页面模板继承该基础模板并填充具体内容。

2.2 数据采集技术

网络爬虫是一种自动化获取网页内容的程序,在本系统中用于采集空气质量数据。系统采用Python的Requests库发送HTTP请求获取网页内容,使用BeautifulSoup库解析HTML文档提取所需数据。Requests库提供了简洁的API接口,支持GET、POST等多种请求方式,可以方便地设置请求头、处理响应内容。BeautifulSoup库是一个功能强大的HTML解析库,支持多种解析器,可以通过标签名、CSS选择器等方式定位和提取网页元素。

系统在spider目录下实现了AqiSpider爬虫类,该类负责从天气后报网站采集空气质量数据。爬虫程序首先构造目标URL,URL格式为"http://www.tianqihoubao.com/aqi/{城市拼音}-{年月}.html",然后发送HTTP请求获取网页内容。为避免被网站识别为爬虫程序,系统设置了完整的请求头信息,包括User-Agent、Accept、Accept-Language等字段。获取网页内容后,使用BeautifulSoup解析HTML文档,通过find_all方法定位表格行元素,逐行提取日期、AQI值、空气质量等级、排名、PM2.5、PM10、SO2、NO2、CO、O3等数据字段。提取的数据以CSV格式保存到data目录下的data.csv文件中,同时也可以导入MySQL数据库进行持久化存储。

2.3 MySQL数据库

MySQL是一个开源的关系型数据库管理系统,以其高性能、高可靠性和易用性在Web应用开发中得到广泛应用。本系统使用MySQL 5.7版本作为数据存储后端,通过PyMySQL库实现Python程序与MySQL数据库的连接和交互。PyMySQL是一个纯Python实现的MySQL客户端库,兼容Python DB-API 2.0规范,提供了连接管理、SQL执行、事务处理等功能。

系统在utils/db.py文件中封装了数据库操作函数,通过connect函数建立与MySQL数据库的连接,连接参数包括主机地址、端口号、用户名、密码和数据库名。query函数封装了SQL语句的执行逻辑,支持参数化查询以防止SQL注入攻击,根据查询类型返回查询结果或执行状态。系统数据库名为air_aqi_db,包含多个数据表:air_data表存储空气质量原始数据,包括城市、日期、空气质量等级、AQI值、排名及各项污染物浓度;tbl_user表存储用户信息,包括用户名、密码、头像路径和管理员标识;city_year_month_avg_aqi表存储城市年月平均AQI统计数据;city_year_month_good_air表存储城市年月优良天数统计数据。

2.4 机器学习预测技术

机器学习是人工智能的一个重要分支,通过算法从数据中学习规律并进行预测。本系统采用Scikit-learn库实现AQI预测功能,Scikit-learn是Python中最流行的机器学习库之一,提供了丰富的机器学习算法和数据处理工具。系统选用线性回归算法作为预测模型,线性回归是一种经典的监督学习算法,适用于连续值预测问题,具有模型简单、可解释性强、训练速度快的优点。

系统在utils/predict_data.py文件中实现了AQI预测功能。getData函数从CSV文件读取空气质量数据,使用Pandas库进行数据预处理,包括数据类型转换、缺失值处理等操作。model_train函数使用train_test_split方法将数据集划分为训练集和测试集,比例为75%和25%,然后创建LinearRegression模型实例并进行训练。pred函数接收PM2.5、SO2、NO2、O3四个污染物浓度值作为输入,调用训练好的模型进行预测,返回预测的AQI值。running函数整合了数据加载、模型训练和预测的完整流程,为Web接口提供调用入口。预测结果根据AQI值划分为优、良、轻度污染、中度污染、重度污染、严重污染六个等级,并给出相应的健康建议。

2.5 ECharts数据可视化

ECharts是百度开源的一个基于JavaScript的数据可视化库,提供了丰富的图表类型和交互功能,支持折线图、柱状图、饼图、散点图、地图、热力图等多种可视化形式。ECharts采用Canvas或SVG渲染,具有良好的跨平台兼容性和渲染性能,广泛应用于数据分析和商业智能领域。

本系统使用ECharts实现空气质量数据的可视化展示。在首页中,系统使用柱状图展示各城市的平均AQI对比,使用折线图展示选定城市的六项污染物浓度分布。在年度分析页面,使用双折线图展示选定城市全年各月的AQI最大值和最小值变化趋势,以及PM2.5和PM10的月均浓度变化。在月度分析页面,使用折线图展示选定月份每日的AQI值和排名变化,使用饼图展示全年各月的优良天数分布。在气体分析页面,使用柱状图展示各城市SO2和NO2的最大浓度对比,使用饼图展示CO和O3的浓度分布情况。在城市分布页面,使用中国地图展示各省份的平均AQI分布,通过颜色深浅直观反映空气质量状况。在城市对比页面,使用多系列折线图展示多个城市在指定时间段内的AQI变化对比。在相关性分析页面,使用热力图展示各污染物之间的相关系数矩阵,使用散点图展示两个污染物之间的相关关系。

2.6 技术特色与创新点

本系统在技术实现上具有以下特色和创新点。在数据采集方面,系统实现了空气质量数据的自动化采集,爬虫程序支持12个主要城市的数据采集,单次可采集指定年月的完整数据,采集的数据包括AQI、空气质量等级、排名及六项污染物浓度共11个字段,数据完整性和准确性得到保障。在预测算法方面,系统基于线性回归模型实现了AQI预测功能,用户输入PM2.5、SO2、NO2、O3四项污染物浓度即可获得预测的AQI值和空气质量等级,预测结果还附带健康建议,具有较强的实用性。在预警功能方面,系统创新性地实现了基于历史数据趋势分析和未来预测的空气质量预警机制,能够识别AQI上升趋势、当前污染状态和未来污染风险,为用户提供多层次的预警信息。在可视化展示方面,系统集成了折线图、柱状图、饼图、地图、热力图、散点图等多种图表类型,支持交互式操作,数据展示直观清晰。在系统架构方面,采用模块化设计,各功能模块相对独立,代码结构清晰,便于维护和扩展。


3 系统分析与设计

3.1 系统总体设计

3.1.1 系统架构设计

本系统采用经典的MVC(Model-View-Controller)架构模式进行设计,将系统划分为数据层、业务逻辑层和表现层三个层次。数据层负责数据的存储和访问,包括MySQL数据库和CSV数据文件;业务逻辑层负责数据处理和业务规则的实现,包括数据采集、数据分析、预测计算等功能;表现层负责用户界面的展示和交互,包括HTML模板和JavaScript脚本。

图3.1 系统架构图
#mermaid-svg-yUEJBUtyg7oIzSNT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yUEJBUtyg7oIzSNT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yUEJBUtyg7oIzSNT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yUEJBUtyg7oIzSNT .error-icon{fill:#552222;}#mermaid-svg-yUEJBUtyg7oIzSNT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yUEJBUtyg7oIzSNT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yUEJBUtyg7oIzSNT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yUEJBUtyg7oIzSNT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yUEJBUtyg7oIzSNT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yUEJBUtyg7oIzSNT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yUEJBUtyg7oIzSNT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yUEJBUtyg7oIzSNT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yUEJBUtyg7oIzSNT .marker.cross{stroke:#333333;}#mermaid-svg-yUEJBUtyg7oIzSNT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yUEJBUtyg7oIzSNT p{margin:0;}#mermaid-svg-yUEJBUtyg7oIzSNT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yUEJBUtyg7oIzSNT .cluster-label text{fill:#333;}#mermaid-svg-yUEJBUtyg7oIzSNT .cluster-label span{color:#333;}#mermaid-svg-yUEJBUtyg7oIzSNT .cluster-label span p{background-color:transparent;}#mermaid-svg-yUEJBUtyg7oIzSNT .label text,#mermaid-svg-yUEJBUtyg7oIzSNT span{fill:#333;color:#333;}#mermaid-svg-yUEJBUtyg7oIzSNT .node rect,#mermaid-svg-yUEJBUtyg7oIzSNT .node circle,#mermaid-svg-yUEJBUtyg7oIzSNT .node ellipse,#mermaid-svg-yUEJBUtyg7oIzSNT .node polygon,#mermaid-svg-yUEJBUtyg7oIzSNT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yUEJBUtyg7oIzSNT .rough-node .label text,#mermaid-svg-yUEJBUtyg7oIzSNT .node .label text,#mermaid-svg-yUEJBUtyg7oIzSNT .image-shape .label,#mermaid-svg-yUEJBUtyg7oIzSNT .icon-shape .label{text-anchor:middle;}#mermaid-svg-yUEJBUtyg7oIzSNT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yUEJBUtyg7oIzSNT .rough-node .label,#mermaid-svg-yUEJBUtyg7oIzSNT .node .label,#mermaid-svg-yUEJBUtyg7oIzSNT .image-shape .label,#mermaid-svg-yUEJBUtyg7oIzSNT .icon-shape .label{text-align:center;}#mermaid-svg-yUEJBUtyg7oIzSNT .node.clickable{cursor:pointer;}#mermaid-svg-yUEJBUtyg7oIzSNT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yUEJBUtyg7oIzSNT .arrowheadPath{fill:#333333;}#mermaid-svg-yUEJBUtyg7oIzSNT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yUEJBUtyg7oIzSNT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yUEJBUtyg7oIzSNT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yUEJBUtyg7oIzSNT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yUEJBUtyg7oIzSNT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yUEJBUtyg7oIzSNT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yUEJBUtyg7oIzSNT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yUEJBUtyg7oIzSNT .cluster text{fill:#333;}#mermaid-svg-yUEJBUtyg7oIzSNT .cluster span{color:#333;}#mermaid-svg-yUEJBUtyg7oIzSNT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yUEJBUtyg7oIzSNT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yUEJBUtyg7oIzSNT rect.text{fill:none;stroke-width:0;}#mermaid-svg-yUEJBUtyg7oIzSNT .icon-shape,#mermaid-svg-yUEJBUtyg7oIzSNT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yUEJBUtyg7oIzSNT .icon-shape p,#mermaid-svg-yUEJBUtyg7oIzSNT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yUEJBUtyg7oIzSNT .icon-shape .label rect,#mermaid-svg-yUEJBUtyg7oIzSNT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yUEJBUtyg7oIzSNT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yUEJBUtyg7oIzSNT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yUEJBUtyg7oIzSNT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据层
业务逻辑层
表现层
Web用户界面
Jinja2模板
ECharts图表
Flask应用
数据采集模块
数据分析模块
预测模块
预警模块
MySQL数据库
CSV数据文件

系统的技术架构基于Python生态系统构建。Web服务采用Flask框架,运行在Werkzeug开发服务器上,默认监听8000端口。数据库采用MySQL 5.7,通过PyMySQL库进行连接。前端页面使用Bootstrap框架实现响应式布局,使用ECharts库实现数据可视化。数据采集使用Requests和BeautifulSoup库,机器学习预测使用Scikit-learn库,数据处理使用Pandas和NumPy库,词云生成使用WordCloud和Jieba库。

3.1.2 系统数据流设计

系统的数据流从数据采集开始,经过数据存储、数据处理、数据分析,最终以可视化图表的形式呈现给用户。数据采集模块从天气后报网站获取空气质量数据,经过解析和清洗后存储到MySQL数据库和CSV文件中。用户通过Web界面发起数据查询请求,Flask应用接收请求后调用相应的数据处理模块,从数据库中查询数据并进行统计分析,将处理结果传递给模板引擎渲染页面,最终返回给用户浏览器展示。

图3.2 系统数据流图
#mermaid-svg-oXU8UEEtPTQ3Yxi2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .error-icon{fill:#552222;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .marker.cross{stroke:#333333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 p{margin:0;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .cluster-label text{fill:#333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .cluster-label span{color:#333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .cluster-label span p{background-color:transparent;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .label text,#mermaid-svg-oXU8UEEtPTQ3Yxi2 span{fill:#333;color:#333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node rect,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node circle,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node ellipse,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node polygon,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .rough-node .label text,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node .label text,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .image-shape .label,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .rough-node .label,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node .label,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .image-shape .label,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .icon-shape .label{text-align:center;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node.clickable{cursor:pointer;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .arrowheadPath{fill:#333333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .cluster text{fill:#333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .cluster span{color:#333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .icon-shape,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .icon-shape p,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .icon-shape .label rect,#mermaid-svg-oXU8UEEtPTQ3Yxi2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oXU8UEEtPTQ3Yxi2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 天气后报网站
爬虫程序
数据解析清洗
MySQL数据库
CSV文件
用户请求
Flask路由
数据查询
数据分析处理
模板渲染
ECharts图表
用户浏览器

3.1.3 系统功能模块设计

系统功能模块按照用户需求和业务逻辑进行划分,主要包括用户管理、数据采集、数据展示、数据分析、预测预警五大功能模块。用户管理模块提供用户注册、登录、个人信息管理等功能。数据采集模块提供空气质量数据的自动化采集功能。数据展示模块提供首页概览、数据预览等功能。数据分析模块提供年度分析、月度分析、气体分析、城市分布、城市对比、相关性分析、季节分析等功能。预测预警模块提供AQI预测和空气质量预警功能。

图3.3 系统功能结构图
#mermaid-svg-xbBeoPcNcYwramOb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xbBeoPcNcYwramOb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xbBeoPcNcYwramOb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xbBeoPcNcYwramOb .error-icon{fill:#552222;}#mermaid-svg-xbBeoPcNcYwramOb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xbBeoPcNcYwramOb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xbBeoPcNcYwramOb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xbBeoPcNcYwramOb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xbBeoPcNcYwramOb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xbBeoPcNcYwramOb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xbBeoPcNcYwramOb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xbBeoPcNcYwramOb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xbBeoPcNcYwramOb .marker.cross{stroke:#333333;}#mermaid-svg-xbBeoPcNcYwramOb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xbBeoPcNcYwramOb p{margin:0;}#mermaid-svg-xbBeoPcNcYwramOb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xbBeoPcNcYwramOb .cluster-label text{fill:#333;}#mermaid-svg-xbBeoPcNcYwramOb .cluster-label span{color:#333;}#mermaid-svg-xbBeoPcNcYwramOb .cluster-label span p{background-color:transparent;}#mermaid-svg-xbBeoPcNcYwramOb .label text,#mermaid-svg-xbBeoPcNcYwramOb span{fill:#333;color:#333;}#mermaid-svg-xbBeoPcNcYwramOb .node rect,#mermaid-svg-xbBeoPcNcYwramOb .node circle,#mermaid-svg-xbBeoPcNcYwramOb .node ellipse,#mermaid-svg-xbBeoPcNcYwramOb .node polygon,#mermaid-svg-xbBeoPcNcYwramOb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xbBeoPcNcYwramOb .rough-node .label text,#mermaid-svg-xbBeoPcNcYwramOb .node .label text,#mermaid-svg-xbBeoPcNcYwramOb .image-shape .label,#mermaid-svg-xbBeoPcNcYwramOb .icon-shape .label{text-anchor:middle;}#mermaid-svg-xbBeoPcNcYwramOb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xbBeoPcNcYwramOb .rough-node .label,#mermaid-svg-xbBeoPcNcYwramOb .node .label,#mermaid-svg-xbBeoPcNcYwramOb .image-shape .label,#mermaid-svg-xbBeoPcNcYwramOb .icon-shape .label{text-align:center;}#mermaid-svg-xbBeoPcNcYwramOb .node.clickable{cursor:pointer;}#mermaid-svg-xbBeoPcNcYwramOb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xbBeoPcNcYwramOb .arrowheadPath{fill:#333333;}#mermaid-svg-xbBeoPcNcYwramOb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xbBeoPcNcYwramOb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xbBeoPcNcYwramOb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xbBeoPcNcYwramOb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xbBeoPcNcYwramOb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xbBeoPcNcYwramOb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xbBeoPcNcYwramOb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xbBeoPcNcYwramOb .cluster text{fill:#333;}#mermaid-svg-xbBeoPcNcYwramOb .cluster span{color:#333;}#mermaid-svg-xbBeoPcNcYwramOb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xbBeoPcNcYwramOb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xbBeoPcNcYwramOb rect.text{fill:none;stroke-width:0;}#mermaid-svg-xbBeoPcNcYwramOb .icon-shape,#mermaid-svg-xbBeoPcNcYwramOb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xbBeoPcNcYwramOb .icon-shape p,#mermaid-svg-xbBeoPcNcYwramOb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xbBeoPcNcYwramOb .icon-shape .label rect,#mermaid-svg-xbBeoPcNcYwramOb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xbBeoPcNcYwramOb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xbBeoPcNcYwramOb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xbBeoPcNcYwramOb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 预测预警
AQI预测
空气预警
数据分析
年度分析
月度分析
气体分析
城市分布
城市对比
相关性分析
季节分析
数据展示
首页概览
数据预览
用户管理
用户登录
用户注册
个人中心
空气质量监测与预测分析系统
用户管理
数据采集
数据展示
数据分析
预测预警

3.2 系统详细设计

3.2.1 用户认证模块设计

用户认证模块负责系统的访问控制,确保只有合法用户才能使用系统功能。模块包括用户注册、用户登录、会话管理和权限控制四个子功能。用户注册时需要输入用户名和密码,系统验证用户名是否已存在,验证通过后将用户信息存储到tbl_user表中。用户登录时输入用户名和密码,系统查询数据库验证身份,验证通过后创建会话并记录用户信息。会话管理使用Flask的session机制,存储用户名、头像路径、管理员标识等信息。权限控制通过before_request钩子函数实现,对每个请求进行拦截,检查用户是否已登录,未登录用户将被重定向到登录页面。

3.2.2 数据采集模块设计

数据采集模块负责从外部数据源获取空气质量数据。模块采用面向对象的设计方式,定义AqiSpider类封装爬虫逻辑。类的构造函数接收城市英文名和中文名作为参数,初始化请求头信息和数据存储路径。send_request方法根据年份和月份构造目标URL,发送HTTP请求获取网页内容,并设置适当的请求间隔避免对目标网站造成压力。parse_response方法使用BeautifulSoup解析HTML文档,提取表格中的空气质量数据。save_data方法将提取的数据追加写入CSV文件。run方法作为爬虫的入口函数,循环调用send_request方法采集指定时间范围的数据。

3.2.3 数据分析模块设计

数据分析模块是系统的核心功能模块,提供多维度的空气质量数据分析功能。年度分析功能展示选定城市全年各月的AQI最大值、最小值以及PM2.5、PM10的月均浓度变化趋势。月度分析功能展示选定城市指定月份每日的AQI值和排名变化,以及全年各月的优良天数分布。气体分析功能展示各城市SO2、NO2的最大浓度对比,以及CO、O3的浓度分布情况。城市分布功能以地图形式展示各省份的平均AQI分布。城市对比功能支持选择多个城市和时间范围,对比各城市的AQI变化趋势和统计指标。相关性分析功能计算各污染物之间的皮尔逊相关系数,以热力图和散点图形式展示相关关系。季节分析功能按春夏秋冬四季统计空气质量指标,分析季节性变化规律。

3.2.4 AQI预测模块设计

AQI预测模块基于机器学习算法实现空气质量指数的预测功能。模块采用线性回归模型,以PM2.5、SO2、NO2、O3四项污染物浓度作为特征变量,以AQI值作为目标变量。模型训练过程首先从CSV文件加载历史数据,进行数据清洗和类型转换,然后将数据集按75:25的比例划分为训练集和测试集,使用训练集数据拟合线性回归模型。预测过程接收用户输入的四项污染物浓度值,调用训练好的模型进行预测,返回预测的AQI值。系统根据预测的AQI值确定空气质量等级,AQI小于等于50为优,51-100为良,101-150为轻度污染,151-200为中度污染,201-300为重度污染,大于300为严重污染,并给出相应的健康建议。

3.2.5 空气预警模块设计

空气预警模块基于历史数据分析和趋势预测实现空气质量预警功能。模块首先获取指定城市最近7天的空气质量数据,分析AQI的变化趋势,计算平均变化率。趋势分析将变化情况分为快速上升、缓慢上升、基本稳定、缓慢下降、快速下降五种类型。模块还实现了未来7天的AQI预测功能,综合考虑近期趋势、移动平均回归和历史同期数据,计算预测值。预警信息根据当前AQI状态、变化趋势和未来预测结果生成,分为危险、警告、提示、正常四个级别。当预测未来出现污染天气时,系统提前发出预警提示;当AQI呈快速上升趋势时,系统发出趋势预警;当当前AQI超过阈值时,系统发出即时预警。

3.2.6 数据可视化模块设计

数据可视化模块负责将数据分析结果以图表形式展示给用户。模块基于ECharts库实现,在后端将数据处理为ECharts所需的格式,通过模板变量传递给前端页面,前端JavaScript代码初始化ECharts实例并渲染图表。系统使用的图表类型包括:折线图用于展示时间序列数据的变化趋势,如AQI日变化、月变化、年变化;柱状图用于展示分类数据的对比,如各城市平均AQI对比、各污染物浓度对比;饼图用于展示数据的占比分布,如优良天数占比、污染物浓度分布;地图用于展示地理分布数据,如各省份AQI分布;热力图用于展示矩阵数据,如污染物相关系数矩阵;散点图用于展示两个变量之间的关系,如污染物相关性分析。

3.2.7 后台管理模块设计

后台管理模块为系统管理员提供数据管理和用户管理功能,仅对具有管理员权限的用户开放。模块包括空气数据管理、用户管理和系统统计三个子功能。空气数据管理功能支持对air_data表中的数据进行增删改查操作,管理员可以手动添加、修改或删除空气质量记录,支持按城市和日期筛选数据,采用分页方式展示数据列表。用户管理功能支持查看所有注册用户列表,管理员可以删除用户账号或切换用户的管理员权限,但不能删除自己的账号或修改自己的权限。系统统计功能展示系统的整体运行数据,包括用户总数、数据记录总数、城市数量、最新数据日期以及各城市数据量排行等信息。后台管理模块通过session中的is_admin标识进行权限验证,非管理员用户访问后台页面时将被重定向到首页。

3.3 数据库设计

3.3.1 数据库关系设计

系统数据库名为air_aqi_db,采用MySQL 5.7版本,字符集为utf8。数据库包含多个数据表,主要包括空气质量原始数据表、统计汇总表和用户信息表。各表之间通过城市名称、年份、月份等字段建立逻辑关联。

图3.4 数据库关系图
#mermaid-svg-E7Ts1zJdW6bLVEaz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-E7Ts1zJdW6bLVEaz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-E7Ts1zJdW6bLVEaz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-E7Ts1zJdW6bLVEaz .error-icon{fill:#552222;}#mermaid-svg-E7Ts1zJdW6bLVEaz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-E7Ts1zJdW6bLVEaz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-E7Ts1zJdW6bLVEaz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-E7Ts1zJdW6bLVEaz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-E7Ts1zJdW6bLVEaz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-E7Ts1zJdW6bLVEaz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-E7Ts1zJdW6bLVEaz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-E7Ts1zJdW6bLVEaz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-E7Ts1zJdW6bLVEaz .marker.cross{stroke:#333333;}#mermaid-svg-E7Ts1zJdW6bLVEaz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-E7Ts1zJdW6bLVEaz p{margin:0;}#mermaid-svg-E7Ts1zJdW6bLVEaz .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-E7Ts1zJdW6bLVEaz .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-E7Ts1zJdW6bLVEaz .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-E7Ts1zJdW6bLVEaz .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-E7Ts1zJdW6bLVEaz .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-E7Ts1zJdW6bLVEaz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-E7Ts1zJdW6bLVEaz .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-E7Ts1zJdW6bLVEaz .node rect,#mermaid-svg-E7Ts1zJdW6bLVEaz .node circle,#mermaid-svg-E7Ts1zJdW6bLVEaz .node ellipse,#mermaid-svg-E7Ts1zJdW6bLVEaz .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-E7Ts1zJdW6bLVEaz .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-E7Ts1zJdW6bLVEaz .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-E7Ts1zJdW6bLVEaz .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-E7Ts1zJdW6bLVEaz .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-E7Ts1zJdW6bLVEaz .edgeLabel .label text{fill:#333;}#mermaid-svg-E7Ts1zJdW6bLVEaz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} air_data
text
city
城市名称
text
date
日期
text
airQuality
空气质量等级
bigint
aqi
AQI指数
bigint
rank
排名
bigint
pm
PM2.5浓度
bigint
pm10
PM10浓度
bigint
so2
SO2浓度
bigint
no2
NO2浓度
double
co
CO浓度
bigint
o3
O3浓度
tbl_user
int
id
PK
自增主键
varchar
user_name
用户名
varchar
password
密码
varchar
avatar
头像路径
tinyint
is_admin
管理员标识
city_year_month_avg_aqi
text
city
城市名称
int
year
年份
int
month
月份
double
avg_aqi
平均AQI
city_year_month_good_air
text
city
城市名称
int
year
年份
int
month
月份
int
good_days
优良天数

表3.1 air_data表结构

字段名称 字段类型 字段说明 是否为空
city text 城市名称,如北京、上海
date text 日期,格式为YYYY-MM-DD
airQuality text 空气质量等级,如优、良、轻度污染
aqi bigint(20) AQI空气质量指数
rank bigint(20) 当日全国排名
pm bigint(20) PM2.5浓度,单位μg/m³
pm10 bigint(20) PM10浓度,单位μg/m³
so2 bigint(20) 二氧化硫浓度,单位μg/m³
no2 bigint(20) 二氧化氮浓度,单位μg/m³
co double 一氧化碳浓度,单位mg/m³
o3 bigint(20) 臭氧浓度,单位μg/m³

表3.2 tbl_user表结构

字段名称 字段类型 字段说明 是否主键 是否为空
id int(11) 自增主键
user_name varchar(200) 用户名
password varchar(200) 用户密码
avatar varchar(500) 用户头像路径
is_admin tinyint(1) 管理员标识,0为普通用户,1为管理员

4 系统实现

4.0 系统整体实现流程

系统实现遵循模块化开发的原则,按照从基础模块到核心功能再到集成测试的顺序进行开发。整个实现过程分为环境搭建、数据库设计、数据采集、核心功能开发、界面优化和系统测试六个主要阶段。

图4.0 系统整体实现流程图
#mermaid-svg-r5AezV74lj2Z5KJQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-r5AezV74lj2Z5KJQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-r5AezV74lj2Z5KJQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-r5AezV74lj2Z5KJQ .error-icon{fill:#552222;}#mermaid-svg-r5AezV74lj2Z5KJQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-r5AezV74lj2Z5KJQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-r5AezV74lj2Z5KJQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-r5AezV74lj2Z5KJQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-r5AezV74lj2Z5KJQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-r5AezV74lj2Z5KJQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-r5AezV74lj2Z5KJQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-r5AezV74lj2Z5KJQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-r5AezV74lj2Z5KJQ .marker.cross{stroke:#333333;}#mermaid-svg-r5AezV74lj2Z5KJQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-r5AezV74lj2Z5KJQ p{margin:0;}#mermaid-svg-r5AezV74lj2Z5KJQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-r5AezV74lj2Z5KJQ .cluster-label text{fill:#333;}#mermaid-svg-r5AezV74lj2Z5KJQ .cluster-label span{color:#333;}#mermaid-svg-r5AezV74lj2Z5KJQ .cluster-label span p{background-color:transparent;}#mermaid-svg-r5AezV74lj2Z5KJQ .label text,#mermaid-svg-r5AezV74lj2Z5KJQ span{fill:#333;color:#333;}#mermaid-svg-r5AezV74lj2Z5KJQ .node rect,#mermaid-svg-r5AezV74lj2Z5KJQ .node circle,#mermaid-svg-r5AezV74lj2Z5KJQ .node ellipse,#mermaid-svg-r5AezV74lj2Z5KJQ .node polygon,#mermaid-svg-r5AezV74lj2Z5KJQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-r5AezV74lj2Z5KJQ .rough-node .label text,#mermaid-svg-r5AezV74lj2Z5KJQ .node .label text,#mermaid-svg-r5AezV74lj2Z5KJQ .image-shape .label,#mermaid-svg-r5AezV74lj2Z5KJQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-r5AezV74lj2Z5KJQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-r5AezV74lj2Z5KJQ .rough-node .label,#mermaid-svg-r5AezV74lj2Z5KJQ .node .label,#mermaid-svg-r5AezV74lj2Z5KJQ .image-shape .label,#mermaid-svg-r5AezV74lj2Z5KJQ .icon-shape .label{text-align:center;}#mermaid-svg-r5AezV74lj2Z5KJQ .node.clickable{cursor:pointer;}#mermaid-svg-r5AezV74lj2Z5KJQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-r5AezV74lj2Z5KJQ .arrowheadPath{fill:#333333;}#mermaid-svg-r5AezV74lj2Z5KJQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-r5AezV74lj2Z5KJQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-r5AezV74lj2Z5KJQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r5AezV74lj2Z5KJQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-r5AezV74lj2Z5KJQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r5AezV74lj2Z5KJQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-r5AezV74lj2Z5KJQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-r5AezV74lj2Z5KJQ .cluster text{fill:#333;}#mermaid-svg-r5AezV74lj2Z5KJQ .cluster span{color:#333;}#mermaid-svg-r5AezV74lj2Z5KJQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-r5AezV74lj2Z5KJQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-r5AezV74lj2Z5KJQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-r5AezV74lj2Z5KJQ .icon-shape,#mermaid-svg-r5AezV74lj2Z5KJQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r5AezV74lj2Z5KJQ .icon-shape p,#mermaid-svg-r5AezV74lj2Z5KJQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-r5AezV74lj2Z5KJQ .icon-shape .label rect,#mermaid-svg-r5AezV74lj2Z5KJQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r5AezV74lj2Z5KJQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-r5AezV74lj2Z5KJQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-r5AezV74lj2Z5KJQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 项目初始化
环境配置
数据库设计与建表
数据采集模块开发
用户认证模块开发
首页数据展示开发
数据分析模块开发
预测预警模块开发
界面优化与集成
功能测试
系统部署

4.1 用户认证模块的实现

用户认证模块实现了用户注册、登录、会话管理和访问控制功能。系统使用Flask的session机制管理用户会话状态,通过before_request钩子函数实现请求拦截和权限控制。

图4.1 用户认证模块实现流程图
#mermaid-svg-22zhr0iJBK9Ro5H5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-22zhr0iJBK9Ro5H5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-22zhr0iJBK9Ro5H5 .error-icon{fill:#552222;}#mermaid-svg-22zhr0iJBK9Ro5H5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-22zhr0iJBK9Ro5H5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-22zhr0iJBK9Ro5H5 .marker.cross{stroke:#333333;}#mermaid-svg-22zhr0iJBK9Ro5H5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-22zhr0iJBK9Ro5H5 p{margin:0;}#mermaid-svg-22zhr0iJBK9Ro5H5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-22zhr0iJBK9Ro5H5 .cluster-label text{fill:#333;}#mermaid-svg-22zhr0iJBK9Ro5H5 .cluster-label span{color:#333;}#mermaid-svg-22zhr0iJBK9Ro5H5 .cluster-label span p{background-color:transparent;}#mermaid-svg-22zhr0iJBK9Ro5H5 .label text,#mermaid-svg-22zhr0iJBK9Ro5H5 span{fill:#333;color:#333;}#mermaid-svg-22zhr0iJBK9Ro5H5 .node rect,#mermaid-svg-22zhr0iJBK9Ro5H5 .node circle,#mermaid-svg-22zhr0iJBK9Ro5H5 .node ellipse,#mermaid-svg-22zhr0iJBK9Ro5H5 .node polygon,#mermaid-svg-22zhr0iJBK9Ro5H5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-22zhr0iJBK9Ro5H5 .rough-node .label text,#mermaid-svg-22zhr0iJBK9Ro5H5 .node .label text,#mermaid-svg-22zhr0iJBK9Ro5H5 .image-shape .label,#mermaid-svg-22zhr0iJBK9Ro5H5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-22zhr0iJBK9Ro5H5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-22zhr0iJBK9Ro5H5 .rough-node .label,#mermaid-svg-22zhr0iJBK9Ro5H5 .node .label,#mermaid-svg-22zhr0iJBK9Ro5H5 .image-shape .label,#mermaid-svg-22zhr0iJBK9Ro5H5 .icon-shape .label{text-align:center;}#mermaid-svg-22zhr0iJBK9Ro5H5 .node.clickable{cursor:pointer;}#mermaid-svg-22zhr0iJBK9Ro5H5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-22zhr0iJBK9Ro5H5 .arrowheadPath{fill:#333333;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-22zhr0iJBK9Ro5H5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-22zhr0iJBK9Ro5H5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-22zhr0iJBK9Ro5H5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-22zhr0iJBK9Ro5H5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-22zhr0iJBK9Ro5H5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-22zhr0iJBK9Ro5H5 .cluster text{fill:#333;}#mermaid-svg-22zhr0iJBK9Ro5H5 .cluster span{color:#333;}#mermaid-svg-22zhr0iJBK9Ro5H5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-22zhr0iJBK9Ro5H5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-22zhr0iJBK9Ro5H5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-22zhr0iJBK9Ro5H5 .icon-shape,#mermaid-svg-22zhr0iJBK9Ro5H5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-22zhr0iJBK9Ro5H5 .icon-shape p,#mermaid-svg-22zhr0iJBK9Ro5H5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-22zhr0iJBK9Ro5H5 .icon-shape .label rect,#mermaid-svg-22zhr0iJBK9Ro5H5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-22zhr0iJBK9Ro5H5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-22zhr0iJBK9Ro5H5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-22zhr0iJBK9Ro5H5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 静态资源
登录/注册页
其他页面
已登录
未登录
验证通过
验证失败
用户访问
请求路径判断
直接放行
直接放行
检查Session
放行访问
重定向登录页
用户输入凭证
验证用户信息
创建Session
返回错误提示

用户登录功能在app.py文件的login视图函数中实现。当用户提交登录表单时,系统从请求中获取用户名和密码,调用db.query函数查询数据库验证用户身份。验证通过后,将用户名、头像路径和管理员标识存储到session中,返回登录成功的JSON响应。用户注册功能首先验证两次输入的密码是否一致,然后检查用户名是否已被注册,验证通过后将用户信息插入tbl_user表。访问控制通过before_request函数实现,该函数在每个请求处理前执行,检查请求路径和session状态,对未登录用户访问受保护页面的请求进行拦截并重定向到登录页面。

python 复制代码
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    else:
        return_dict = {'code': '200', 'msg': '处理成功', 'result': False}
        user = db.query("select user_name, avatar, is_admin from tbl_user where user_name = %s and password = %s",
                       [request.form['userName'], request.form['password']], 'select')
        if user:
            session['userName'] = request.form['userName']
            if user[0][1]:
                session['avatar_url'] = user[0][1]
            else:
                session['avatar_url'] = '/static/assets/images/avatar.png'
            session['is_admin'] = bool(user[0][2]) if len(user[0]) > 2 and user[0][2] else False
            return jsonify(return_dict)
        else:
            return_dict['code'] = '400'
            return_dict['msg'] = '用户名和密码不一致'
        return jsonify(return_dict)

访问路径http://localhost:5000/login

图4.1 用户登录界面

4.2 数据采集模块的实现

数据采集模块通过AqiSpider爬虫类实现空气质量数据的自动化采集。爬虫程序从天气后报网站获取数据,支持12个主要城市的数据采集,采集的数据包括日期、AQI值、空气质量等级、排名及六项污染物浓度。

图4.2 数据采集模块实现流程图
#mermaid-svg-zJ3E2GPP54tsrrvy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zJ3E2GPP54tsrrvy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zJ3E2GPP54tsrrvy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zJ3E2GPP54tsrrvy .error-icon{fill:#552222;}#mermaid-svg-zJ3E2GPP54tsrrvy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zJ3E2GPP54tsrrvy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zJ3E2GPP54tsrrvy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zJ3E2GPP54tsrrvy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zJ3E2GPP54tsrrvy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zJ3E2GPP54tsrrvy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zJ3E2GPP54tsrrvy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zJ3E2GPP54tsrrvy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zJ3E2GPP54tsrrvy .marker.cross{stroke:#333333;}#mermaid-svg-zJ3E2GPP54tsrrvy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zJ3E2GPP54tsrrvy p{margin:0;}#mermaid-svg-zJ3E2GPP54tsrrvy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zJ3E2GPP54tsrrvy .cluster-label text{fill:#333;}#mermaid-svg-zJ3E2GPP54tsrrvy .cluster-label span{color:#333;}#mermaid-svg-zJ3E2GPP54tsrrvy .cluster-label span p{background-color:transparent;}#mermaid-svg-zJ3E2GPP54tsrrvy .label text,#mermaid-svg-zJ3E2GPP54tsrrvy span{fill:#333;color:#333;}#mermaid-svg-zJ3E2GPP54tsrrvy .node rect,#mermaid-svg-zJ3E2GPP54tsrrvy .node circle,#mermaid-svg-zJ3E2GPP54tsrrvy .node ellipse,#mermaid-svg-zJ3E2GPP54tsrrvy .node polygon,#mermaid-svg-zJ3E2GPP54tsrrvy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zJ3E2GPP54tsrrvy .rough-node .label text,#mermaid-svg-zJ3E2GPP54tsrrvy .node .label text,#mermaid-svg-zJ3E2GPP54tsrrvy .image-shape .label,#mermaid-svg-zJ3E2GPP54tsrrvy .icon-shape .label{text-anchor:middle;}#mermaid-svg-zJ3E2GPP54tsrrvy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zJ3E2GPP54tsrrvy .rough-node .label,#mermaid-svg-zJ3E2GPP54tsrrvy .node .label,#mermaid-svg-zJ3E2GPP54tsrrvy .image-shape .label,#mermaid-svg-zJ3E2GPP54tsrrvy .icon-shape .label{text-align:center;}#mermaid-svg-zJ3E2GPP54tsrrvy .node.clickable{cursor:pointer;}#mermaid-svg-zJ3E2GPP54tsrrvy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zJ3E2GPP54tsrrvy .arrowheadPath{fill:#333333;}#mermaid-svg-zJ3E2GPP54tsrrvy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zJ3E2GPP54tsrrvy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zJ3E2GPP54tsrrvy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zJ3E2GPP54tsrrvy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zJ3E2GPP54tsrrvy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zJ3E2GPP54tsrrvy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zJ3E2GPP54tsrrvy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zJ3E2GPP54tsrrvy .cluster text{fill:#333;}#mermaid-svg-zJ3E2GPP54tsrrvy .cluster span{color:#333;}#mermaid-svg-zJ3E2GPP54tsrrvy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zJ3E2GPP54tsrrvy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zJ3E2GPP54tsrrvy rect.text{fill:none;stroke-width:0;}#mermaid-svg-zJ3E2GPP54tsrrvy .icon-shape,#mermaid-svg-zJ3E2GPP54tsrrvy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zJ3E2GPP54tsrrvy .icon-shape p,#mermaid-svg-zJ3E2GPP54tsrrvy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zJ3E2GPP54tsrrvy .icon-shape .label rect,#mermaid-svg-zJ3E2GPP54tsrrvy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zJ3E2GPP54tsrrvy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zJ3E2GPP54tsrrvy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zJ3E2GPP54tsrrvy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
失败


初始化爬虫
设置请求头
构造目标URL
发送HTTP请求
响应状态
解析HTML内容
记录错误日志
提取表格数据
数据清洗转换
保存到CSV文件
是否继续
采集完成

AqiSpider类在spider/spider_aqi.py文件中实现。类的构造函数初始化城市名称、请求头信息和CSV文件路径,如果CSV文件不存在则创建并写入表头。send_request方法根据年份和月份构造URL,使用requests.get发送请求,设置60秒超时和2秒请求间隔。parse_response方法使用BeautifulSoup解析HTML,通过find_all方法定位所有tr元素,遍历提取每行的td元素内容。save_data方法将数据列表追加写入CSV文件。

python 复制代码
class AqiSpider:
    def __init__(self, city_name, chinese_name):
        self.city_name = city_name
        self.chinese_name = chinese_name
        self.headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Accept-Language': 'zh-CN,zh;q=0.9',
        }
        base_dir = os.path.dirname(os.path.abspath(__file__))
        data_dir = os.path.abspath(os.path.join(base_dir, '..', 'data'))
        os.makedirs(data_dir, exist_ok=True)
        self.csv_path = os.path.join(data_dir, 'data.csv')

    def send_request(self, year, month):
        url = f'http://www.tianqihoubao.com/aqi/{self.city_name}-{year}{month:02d}.html'
        response = requests.get(url, headers=self.headers, timeout=60)
        time.sleep(2)
        self.parse_response(response.text)

    def parse_response(self, response):
        soup = BeautifulSoup(response, 'html.parser')
        tr_list = soup.find_all('tr')
        data_list = []
        for tr in tr_list[1:]:
            tds = tr.find_all('td')
            date = tds[0].get_text().strip()
            aqi_value = tds[1].get_text().strip()
            quality_level = tds[2].get_text().strip()
            # ... 提取其他字段
            data_list.append([self.chinese_name, date, quality_level, aqi_value, ...])
        self.save_data(data_list)

4.3 首页数据展示的实现

首页是系统的核心展示页面,集成了空气质量概览、统计指标、城市排名和可视化图表等功能。页面支持按城市和日期筛选数据,动态展示选定条件下的空气质量信息。

图4.3 首页数据展示实现流程图
#mermaid-svg-M3cP7gIxHQwomxQ3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M3cP7gIxHQwomxQ3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M3cP7gIxHQwomxQ3 .error-icon{fill:#552222;}#mermaid-svg-M3cP7gIxHQwomxQ3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M3cP7gIxHQwomxQ3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M3cP7gIxHQwomxQ3 .marker.cross{stroke:#333333;}#mermaid-svg-M3cP7gIxHQwomxQ3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M3cP7gIxHQwomxQ3 p{margin:0;}#mermaid-svg-M3cP7gIxHQwomxQ3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M3cP7gIxHQwomxQ3 .cluster-label text{fill:#333;}#mermaid-svg-M3cP7gIxHQwomxQ3 .cluster-label span{color:#333;}#mermaid-svg-M3cP7gIxHQwomxQ3 .cluster-label span p{background-color:transparent;}#mermaid-svg-M3cP7gIxHQwomxQ3 .label text,#mermaid-svg-M3cP7gIxHQwomxQ3 span{fill:#333;color:#333;}#mermaid-svg-M3cP7gIxHQwomxQ3 .node rect,#mermaid-svg-M3cP7gIxHQwomxQ3 .node circle,#mermaid-svg-M3cP7gIxHQwomxQ3 .node ellipse,#mermaid-svg-M3cP7gIxHQwomxQ3 .node polygon,#mermaid-svg-M3cP7gIxHQwomxQ3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M3cP7gIxHQwomxQ3 .rough-node .label text,#mermaid-svg-M3cP7gIxHQwomxQ3 .node .label text,#mermaid-svg-M3cP7gIxHQwomxQ3 .image-shape .label,#mermaid-svg-M3cP7gIxHQwomxQ3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-M3cP7gIxHQwomxQ3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M3cP7gIxHQwomxQ3 .rough-node .label,#mermaid-svg-M3cP7gIxHQwomxQ3 .node .label,#mermaid-svg-M3cP7gIxHQwomxQ3 .image-shape .label,#mermaid-svg-M3cP7gIxHQwomxQ3 .icon-shape .label{text-align:center;}#mermaid-svg-M3cP7gIxHQwomxQ3 .node.clickable{cursor:pointer;}#mermaid-svg-M3cP7gIxHQwomxQ3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M3cP7gIxHQwomxQ3 .arrowheadPath{fill:#333333;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M3cP7gIxHQwomxQ3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M3cP7gIxHQwomxQ3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M3cP7gIxHQwomxQ3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M3cP7gIxHQwomxQ3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M3cP7gIxHQwomxQ3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M3cP7gIxHQwomxQ3 .cluster text{fill:#333;}#mermaid-svg-M3cP7gIxHQwomxQ3 .cluster span{color:#333;}#mermaid-svg-M3cP7gIxHQwomxQ3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M3cP7gIxHQwomxQ3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M3cP7gIxHQwomxQ3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-M3cP7gIxHQwomxQ3 .icon-shape,#mermaid-svg-M3cP7gIxHQwomxQ3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M3cP7gIxHQwomxQ3 .icon-shape p,#mermaid-svg-M3cP7gIxHQwomxQ3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M3cP7gIxHQwomxQ3 .icon-shape .label rect,#mermaid-svg-M3cP7gIxHQwomxQ3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M3cP7gIxHQwomxQ3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M3cP7gIxHQwomxQ3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M3cP7gIxHQwomxQ3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户访问首页
获取筛选参数
查询城市列表
确定默认城市和日期
查询当日空气数据
计算统计指标
查询城市排名
获取图表数据
渲染页面模板
返回HTML响应

首页视图函数index在app.py中实现,函数首先从数据库查询所有空气质量数据,提取城市列表、年份列表、月份列表和日期列表。根据用户选择或默认值确定当前展示的城市和日期,查询对应的空气质量详细数据。统计指标包括总记录数、平均AQI、优良天数、优良率和污染天数,通过遍历数据列表计算得出。城市排名数据按当日AQI升序排列,展示各城市的空气质量状况。图表数据通过调用index_data.get_index_data函数获取,包括各城市平均AQI柱状图数据和选定城市六项污染物浓度折线图数据。

python 复制代码
@app.route('/index', methods=['GET', 'POST'])
def index():
    userName = session.get('userName')
    air_data_list = public_data_mysql.get_air_data()
    city_datas = sorted(list(set(air_data[0] for air_data in air_data_list)))
    
    default_city = city_datas[0] if city_datas else '上海'
    # ... 处理筛选参数
    
    # 统计数据计算
    total_records = len(air_data_list)
    aqi_values = [float(data[3]) for data in air_data_list if data[3]]
    avg_aqi = round(sum(aqi_values) / len(aqi_values), 1) if aqi_values else 0
    good_days = sum(1 for data in air_data_list if data[3] and float(data[3]) <= 100)
    good_rate = round(good_days / total_records * 100, 1) if total_records > 0 else 0
    
    x_bar, y_bar, x_line, y_line = index_data.get_index_data(default_city)
    return render_template('index.html', ...)

访问路径http://localhost:5000/index

图4.2 系统首页界面

4.4 年度与月度分析的实现

年度分析功能展示选定城市全年各月的AQI变化趋势,包括最大值、最小值以及PM2.5、PM10的月均浓度。月度分析功能展示选定月份每日的AQI值和排名变化,以及全年各月的优良天数分布。

图4.4 年度与月度分析实现流程图
#mermaid-svg-K3wCoVSIlAAIFsLA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-K3wCoVSIlAAIFsLA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-K3wCoVSIlAAIFsLA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-K3wCoVSIlAAIFsLA .error-icon{fill:#552222;}#mermaid-svg-K3wCoVSIlAAIFsLA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K3wCoVSIlAAIFsLA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-K3wCoVSIlAAIFsLA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K3wCoVSIlAAIFsLA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K3wCoVSIlAAIFsLA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-K3wCoVSIlAAIFsLA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K3wCoVSIlAAIFsLA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K3wCoVSIlAAIFsLA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K3wCoVSIlAAIFsLA .marker.cross{stroke:#333333;}#mermaid-svg-K3wCoVSIlAAIFsLA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K3wCoVSIlAAIFsLA p{margin:0;}#mermaid-svg-K3wCoVSIlAAIFsLA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-K3wCoVSIlAAIFsLA .cluster-label text{fill:#333;}#mermaid-svg-K3wCoVSIlAAIFsLA .cluster-label span{color:#333;}#mermaid-svg-K3wCoVSIlAAIFsLA .cluster-label span p{background-color:transparent;}#mermaid-svg-K3wCoVSIlAAIFsLA .label text,#mermaid-svg-K3wCoVSIlAAIFsLA span{fill:#333;color:#333;}#mermaid-svg-K3wCoVSIlAAIFsLA .node rect,#mermaid-svg-K3wCoVSIlAAIFsLA .node circle,#mermaid-svg-K3wCoVSIlAAIFsLA .node ellipse,#mermaid-svg-K3wCoVSIlAAIFsLA .node polygon,#mermaid-svg-K3wCoVSIlAAIFsLA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K3wCoVSIlAAIFsLA .rough-node .label text,#mermaid-svg-K3wCoVSIlAAIFsLA .node .label text,#mermaid-svg-K3wCoVSIlAAIFsLA .image-shape .label,#mermaid-svg-K3wCoVSIlAAIFsLA .icon-shape .label{text-anchor:middle;}#mermaid-svg-K3wCoVSIlAAIFsLA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-K3wCoVSIlAAIFsLA .rough-node .label,#mermaid-svg-K3wCoVSIlAAIFsLA .node .label,#mermaid-svg-K3wCoVSIlAAIFsLA .image-shape .label,#mermaid-svg-K3wCoVSIlAAIFsLA .icon-shape .label{text-align:center;}#mermaid-svg-K3wCoVSIlAAIFsLA .node.clickable{cursor:pointer;}#mermaid-svg-K3wCoVSIlAAIFsLA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-K3wCoVSIlAAIFsLA .arrowheadPath{fill:#333333;}#mermaid-svg-K3wCoVSIlAAIFsLA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-K3wCoVSIlAAIFsLA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-K3wCoVSIlAAIFsLA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K3wCoVSIlAAIFsLA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-K3wCoVSIlAAIFsLA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K3wCoVSIlAAIFsLA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-K3wCoVSIlAAIFsLA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-K3wCoVSIlAAIFsLA .cluster text{fill:#333;}#mermaid-svg-K3wCoVSIlAAIFsLA .cluster span{color:#333;}#mermaid-svg-K3wCoVSIlAAIFsLA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-K3wCoVSIlAAIFsLA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-K3wCoVSIlAAIFsLA rect.text{fill:none;stroke-width:0;}#mermaid-svg-K3wCoVSIlAAIFsLA .icon-shape,#mermaid-svg-K3wCoVSIlAAIFsLA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K3wCoVSIlAAIFsLA .icon-shape p,#mermaid-svg-K3wCoVSIlAAIFsLA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-K3wCoVSIlAAIFsLA .icon-shape .label rect,#mermaid-svg-K3wCoVSIlAAIFsLA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K3wCoVSIlAAIFsLA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-K3wCoVSIlAAIFsLA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-K3wCoVSIlAAIFsLA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户选择城市和年份
查询年月统计数据
提取AQI最大最小值
提取PM2.5/PM10均值
构造12个月数据数组
处理缺失月份数据
生成折线图配置
渲染分析页面

年度分析功能在utils/air_year_data.py文件中实现。get_air_year_data函数首先从city_year_month_max_min_aqi聚合表查询数据,提取城市列表和年份列表。然后初始化12个月的数据数组,遍历查询结果填充对应月份的AQI最大值和最小值。对于没有数据的月份,将值设为None以便ECharts断开连线。PM2.5和PM10的月均数据从city_avg_pm表获取,处理方式类似。

python 复制代码
def get_air_year_data(default_city, default_year):
    city_year_month_max_min_aqi_list = public_data_mysql.get_city_year_month_max_min_aqi()
    city_datas = sorted(list(set(item[0] for item in city_year_month_max_min_aqi_list)))
    year_datas = sorted(list(set(str(item[1]) for item in city_year_month_max_min_aqi_list)), reverse=True)

    y_line_max = [0] * 12
    y_line_min = [0] * 12
    has_data = [False] * 12

    for item in city_year_month_max_min_aqi_list:
        if item[0] == default_city and str(item[1]) == default_year:
            month_idx = int(item[2]) - 1
            if 0 <= month_idx < 12:
                y_line_max[month_idx] = item[3]
                y_line_min[month_idx] = item[4]
                has_data[month_idx] = True

    for i in range(12):
        if not has_data[i]:
            y_line_max[i] = None
            y_line_min[i] = None

    return city_datas, year_datas, x_line, y_line_max, y_line_min, y_line_pm, y_line_pm10

访问路径

  • 年度分析:http://localhost:5000/air_year_ana
  • 月度分析:http://localhost:5000/air_month_ana

图4.3 年度分析界面

4.5 AQI预测模块的实现

AQI预测模块基于线性回归算法实现空气质量指数的预测功能。用户输入PM2.5、SO2、NO2、O3四项污染物浓度,系统返回预测的AQI值、空气质量等级和健康建议。

图4.5 AQI预测模块实现流程图
#mermaid-svg-SiX2ZOE2bzVbvQ5Y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .error-icon{fill:#552222;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .marker.cross{stroke:#333333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y p{margin:0;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .cluster-label text{fill:#333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .cluster-label span{color:#333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .cluster-label span p{background-color:transparent;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .label text,#mermaid-svg-SiX2ZOE2bzVbvQ5Y span{fill:#333;color:#333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node rect,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node circle,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node ellipse,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node polygon,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .rough-node .label text,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node .label text,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .image-shape .label,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .rough-node .label,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node .label,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .image-shape .label,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .icon-shape .label{text-align:center;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node.clickable{cursor:pointer;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .arrowheadPath{fill:#333333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .cluster text{fill:#333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .cluster span{color:#333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .icon-shape,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .icon-shape p,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .icon-shape .label rect,#mermaid-svg-SiX2ZOE2bzVbvQ5Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SiX2ZOE2bzVbvQ5Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

用户输入污染物浓度
数据验证
验证通过
加载训练数据
返回错误提示
数据预处理
划分训练测试集
训练线性回归模型
执行AQI预测
确定空气质量等级
生成健康建议
返回预测结果

AQI预测功能在utils/predict_data.py文件中实现。getData函数从CSV文件读取数据,使用Pandas进行数据清洗,将污染物浓度转换为数值类型并删除缺失值。model_train函数使用train_test_split将数据划分为训练集和测试集,创建LinearRegression模型并进行训练。pred函数接收四项污染物浓度,构造DataFrame后调用模型的predict方法获取预测值。running函数整合完整流程,供视图函数调用。

python 复制代码
def getData():
    base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    data_path = os.path.join(base_dir, 'data', 'data.csv')
    df = pd.read_csv(data_path)
    products = df.loc[:, ["aqi", "pm", "so2", "no2", "o3"]]
    products['pm'] = pd.to_numeric(products['pm'], errors='coerce')
    # ... 数据类型转换和清洗
    products = products.dropna()
    return products

def model_train(data):
    x_train, x_test, y_train, y_test = train_test_split(
        data[['pm', 'so2', 'no2', 'o3']], data['aqi'], test_size=0.25, random_state=1)
    liner = LinearRegression()
    liner.fit(x_train, y_train)
    return liner

def running(pm, so2, no2, o3):
    train_data = getData()
    model = model_train(train_data)
    result = pred(model, pm, so2, no2, o3)
    return result

视图函数predict_ana处理预测请求,根据预测的AQI值确定空气质量等级和健康建议。AQI小于等于50为优,建议多进行户外运动;51-100为良,极少数敏感人群应减少户外活动;101-150为轻度污染,儿童老年人应减少户外锻炼;151-200为中度污染,一般人群适量减少户外运动;201-300为重度污染,一般人群减少户外运动;大于300为严重污染,一般人群应避免户外活动。

访问路径http://localhost:5000/predict_ana

图4.4 AQI预测界面

4.6 空气预警模块的实现

空气预警模块基于历史数据趋势分析和未来预测实现空气质量预警功能。系统分析最近7天的AQI变化趋势,预测未来7天的空气质量,根据分析结果生成多层次的预警信息。

图4.6 空气预警模块实现流程图
#mermaid-svg-WDocr1soiP4XLeKt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WDocr1soiP4XLeKt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WDocr1soiP4XLeKt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WDocr1soiP4XLeKt .error-icon{fill:#552222;}#mermaid-svg-WDocr1soiP4XLeKt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WDocr1soiP4XLeKt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WDocr1soiP4XLeKt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WDocr1soiP4XLeKt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WDocr1soiP4XLeKt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WDocr1soiP4XLeKt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WDocr1soiP4XLeKt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WDocr1soiP4XLeKt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WDocr1soiP4XLeKt .marker.cross{stroke:#333333;}#mermaid-svg-WDocr1soiP4XLeKt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WDocr1soiP4XLeKt p{margin:0;}#mermaid-svg-WDocr1soiP4XLeKt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WDocr1soiP4XLeKt .cluster-label text{fill:#333;}#mermaid-svg-WDocr1soiP4XLeKt .cluster-label span{color:#333;}#mermaid-svg-WDocr1soiP4XLeKt .cluster-label span p{background-color:transparent;}#mermaid-svg-WDocr1soiP4XLeKt .label text,#mermaid-svg-WDocr1soiP4XLeKt span{fill:#333;color:#333;}#mermaid-svg-WDocr1soiP4XLeKt .node rect,#mermaid-svg-WDocr1soiP4XLeKt .node circle,#mermaid-svg-WDocr1soiP4XLeKt .node ellipse,#mermaid-svg-WDocr1soiP4XLeKt .node polygon,#mermaid-svg-WDocr1soiP4XLeKt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WDocr1soiP4XLeKt .rough-node .label text,#mermaid-svg-WDocr1soiP4XLeKt .node .label text,#mermaid-svg-WDocr1soiP4XLeKt .image-shape .label,#mermaid-svg-WDocr1soiP4XLeKt .icon-shape .label{text-anchor:middle;}#mermaid-svg-WDocr1soiP4XLeKt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WDocr1soiP4XLeKt .rough-node .label,#mermaid-svg-WDocr1soiP4XLeKt .node .label,#mermaid-svg-WDocr1soiP4XLeKt .image-shape .label,#mermaid-svg-WDocr1soiP4XLeKt .icon-shape .label{text-align:center;}#mermaid-svg-WDocr1soiP4XLeKt .node.clickable{cursor:pointer;}#mermaid-svg-WDocr1soiP4XLeKt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WDocr1soiP4XLeKt .arrowheadPath{fill:#333333;}#mermaid-svg-WDocr1soiP4XLeKt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WDocr1soiP4XLeKt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WDocr1soiP4XLeKt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WDocr1soiP4XLeKt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WDocr1soiP4XLeKt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WDocr1soiP4XLeKt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WDocr1soiP4XLeKt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WDocr1soiP4XLeKt .cluster text{fill:#333;}#mermaid-svg-WDocr1soiP4XLeKt .cluster span{color:#333;}#mermaid-svg-WDocr1soiP4XLeKt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WDocr1soiP4XLeKt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WDocr1soiP4XLeKt rect.text{fill:none;stroke-width:0;}#mermaid-svg-WDocr1soiP4XLeKt .icon-shape,#mermaid-svg-WDocr1soiP4XLeKt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WDocr1soiP4XLeKt .icon-shape p,#mermaid-svg-WDocr1soiP4XLeKt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WDocr1soiP4XLeKt .icon-shape .label rect,#mermaid-svg-WDocr1soiP4XLeKt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WDocr1soiP4XLeKt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WDocr1soiP4XLeKt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WDocr1soiP4XLeKt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 未来污染
趋势上升
当前污染
空气良好
选择城市
获取最近7天数据
分析AQI变化趋势
计算平均变化率
预测未来7天AQI
检查预警条件
生成未来预警
生成趋势预警
生成即时预警
生成正常提示
汇总预警信息
返回预警结果

空气预警功能在utils/air_warning.py文件中实现。get_latest_data函数查询指定城市最近N天的空气质量数据。analyze_trend函数分析最近3天的AQI变化,计算平均变化率,将趋势分为快速上升、缓慢上升、基本稳定、缓慢下降、快速下降五种类型。predict_future函数基于近期趋势、移动平均回归和历史同期数据预测未来7天的AQI值。check_warnings函数综合分析结果生成预警信息,包括未来污染预警、趋势预警、当前状态预警和PM2.5超标预警。

python 复制代码
def analyze_trend(data):
    if len(data) < 3:
        return 'stable', 0
    recent = data[-3:]
    changes = []
    for i in range(1, len(recent)):
        changes.append(int(recent[i]['aqi']) - int(recent[i-1]['aqi']))
    avg_change = sum(changes) / len(changes) if changes else 0
    
    if avg_change > 15:
        return 'rising_fast', avg_change
    elif avg_change > 5:
        return 'rising', avg_change
    elif avg_change < -15:
        return 'falling_fast', avg_change
    elif avg_change < -5:
        return 'falling', avg_change
    else:
        return 'stable', avg_change

def check_warnings(city):
    data = get_latest_data(city, 7)
    warnings = []
    latest = data[-1]
    trend, change = analyze_trend(data)
    predictions = predict_future(city, data, 7)
    
    # 生成预警信息
    if predictions:
        max_future_aqi = max(p['aqi'] for p in predictions)
        if max_future_aqi >= 150:
            warnings.append({
                'level': 'danger',
                'type': '⚠️ 未来污染预警',
                'message': f"预计空气质量将达到污染级别,请提前做好防护准备"
            })
    # ... 其他预警条件检查
    return warnings, data, trend_info, predictions

访问路径http://localhost:5000/air_warning

图4.5 空气预警界面

4.7 城市对比与相关性分析的实现

城市对比功能支持选择多个城市和时间范围,对比各城市的AQI变化趋势和统计指标。相关性分析功能计算各污染物之间的皮尔逊相关系数,以热力图和散点图形式展示相关关系。

图4.7 城市对比与相关性分析实现流程图
#mermaid-svg-QRfZ9zmQt2c2IdmQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .error-icon{fill:#552222;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .marker.cross{stroke:#333333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QRfZ9zmQt2c2IdmQ p{margin:0;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .cluster-label text{fill:#333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .cluster-label span{color:#333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .cluster-label span p{background-color:transparent;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .label text,#mermaid-svg-QRfZ9zmQt2c2IdmQ span{fill:#333;color:#333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .node rect,#mermaid-svg-QRfZ9zmQt2c2IdmQ .node circle,#mermaid-svg-QRfZ9zmQt2c2IdmQ .node ellipse,#mermaid-svg-QRfZ9zmQt2c2IdmQ .node polygon,#mermaid-svg-QRfZ9zmQt2c2IdmQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .rough-node .label text,#mermaid-svg-QRfZ9zmQt2c2IdmQ .node .label text,#mermaid-svg-QRfZ9zmQt2c2IdmQ .image-shape .label,#mermaid-svg-QRfZ9zmQt2c2IdmQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .rough-node .label,#mermaid-svg-QRfZ9zmQt2c2IdmQ .node .label,#mermaid-svg-QRfZ9zmQt2c2IdmQ .image-shape .label,#mermaid-svg-QRfZ9zmQt2c2IdmQ .icon-shape .label{text-align:center;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .node.clickable{cursor:pointer;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .arrowheadPath{fill:#333333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QRfZ9zmQt2c2IdmQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QRfZ9zmQt2c2IdmQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QRfZ9zmQt2c2IdmQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .cluster text{fill:#333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .cluster span{color:#333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QRfZ9zmQt2c2IdmQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .icon-shape,#mermaid-svg-QRfZ9zmQt2c2IdmQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .icon-shape p,#mermaid-svg-QRfZ9zmQt2c2IdmQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .icon-shape .label rect,#mermaid-svg-QRfZ9zmQt2c2IdmQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QRfZ9zmQt2c2IdmQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QRfZ9zmQt2c2IdmQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QRfZ9zmQt2c2IdmQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 城市对比
相关性分析
用户选择参数
功能类型
获取多城市数据
获取污染物数据
按城市组织数据
计算统计指标
生成对比图表
计算相关系数矩阵
生成热力图数据
生成散点图数据
渲染结果页面

城市对比功能在utils/city_compare.py文件中实现。get_compare_data函数查询多个城市在指定时间段的空气质量数据,按城市组织为字典结构,包含日期序列和各项指标序列。get_city_stats函数计算各城市的统计指标,包括平均AQI、最大AQI、最小AQI、各污染物平均浓度、优良天数和污染天数等。

相关性分析功能在utils/correlation_analysis.py文件中实现。get_correlation_data函数查询污染物数据,转换为数值列表。calculate_correlation函数实现皮尔逊相关系数的计算,公式为两变量协方差除以标准差乘积。get_correlation_matrix函数计算7个指标(PM2.5、PM10、SO2、NO2、CO、O3、AQI)两两之间的相关系数,构造热力图所需的矩阵数据。

python 复制代码
def calculate_correlation(x, y):
    n = len(x)
    if n == 0:
        return 0
    mean_x = sum(x) / n
    mean_y = sum(y) / n
    numerator = sum((x[i] - mean_x) * (y[i] - mean_y) for i in range(n))
    sum_sq_x = sum((xi - mean_x) ** 2 for xi in x)
    sum_sq_y = sum((yi - mean_y) ** 2 for yi in y)
    denominator = (sum_sq_x * sum_sq_y) ** 0.5
    if denominator == 0:
        return 0
    return round(numerator / denominator, 3)

访问路径

  • 城市对比:http://localhost:5000/city_compare
  • 相关性分析:http://localhost:5000/correlation

图4.6 城市对比界面

4.8 后台管理模块的实现

后台管理模块为系统管理员提供数据管理和用户管理功能。系统通过tbl_user表中的is_admin字段区分普通用户和管理员,只有管理员才能访问后台管理页面。

图4.8 后台管理模块实现流程图
#mermaid-svg-SBtCvnriNVKYo79Q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SBtCvnriNVKYo79Q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SBtCvnriNVKYo79Q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SBtCvnriNVKYo79Q .error-icon{fill:#552222;}#mermaid-svg-SBtCvnriNVKYo79Q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SBtCvnriNVKYo79Q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SBtCvnriNVKYo79Q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SBtCvnriNVKYo79Q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SBtCvnriNVKYo79Q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SBtCvnriNVKYo79Q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SBtCvnriNVKYo79Q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SBtCvnriNVKYo79Q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SBtCvnriNVKYo79Q .marker.cross{stroke:#333333;}#mermaid-svg-SBtCvnriNVKYo79Q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SBtCvnriNVKYo79Q p{margin:0;}#mermaid-svg-SBtCvnriNVKYo79Q .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SBtCvnriNVKYo79Q .cluster-label text{fill:#333;}#mermaid-svg-SBtCvnriNVKYo79Q .cluster-label span{color:#333;}#mermaid-svg-SBtCvnriNVKYo79Q .cluster-label span p{background-color:transparent;}#mermaid-svg-SBtCvnriNVKYo79Q .label text,#mermaid-svg-SBtCvnriNVKYo79Q span{fill:#333;color:#333;}#mermaid-svg-SBtCvnriNVKYo79Q .node rect,#mermaid-svg-SBtCvnriNVKYo79Q .node circle,#mermaid-svg-SBtCvnriNVKYo79Q .node ellipse,#mermaid-svg-SBtCvnriNVKYo79Q .node polygon,#mermaid-svg-SBtCvnriNVKYo79Q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SBtCvnriNVKYo79Q .rough-node .label text,#mermaid-svg-SBtCvnriNVKYo79Q .node .label text,#mermaid-svg-SBtCvnriNVKYo79Q .image-shape .label,#mermaid-svg-SBtCvnriNVKYo79Q .icon-shape .label{text-anchor:middle;}#mermaid-svg-SBtCvnriNVKYo79Q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SBtCvnriNVKYo79Q .rough-node .label,#mermaid-svg-SBtCvnriNVKYo79Q .node .label,#mermaid-svg-SBtCvnriNVKYo79Q .image-shape .label,#mermaid-svg-SBtCvnriNVKYo79Q .icon-shape .label{text-align:center;}#mermaid-svg-SBtCvnriNVKYo79Q .node.clickable{cursor:pointer;}#mermaid-svg-SBtCvnriNVKYo79Q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SBtCvnriNVKYo79Q .arrowheadPath{fill:#333333;}#mermaid-svg-SBtCvnriNVKYo79Q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SBtCvnriNVKYo79Q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SBtCvnriNVKYo79Q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SBtCvnriNVKYo79Q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SBtCvnriNVKYo79Q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SBtCvnriNVKYo79Q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SBtCvnriNVKYo79Q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SBtCvnriNVKYo79Q .cluster text{fill:#333;}#mermaid-svg-SBtCvnriNVKYo79Q .cluster span{color:#333;}#mermaid-svg-SBtCvnriNVKYo79Q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SBtCvnriNVKYo79Q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SBtCvnriNVKYo79Q rect.text{fill:none;stroke-width:0;}#mermaid-svg-SBtCvnriNVKYo79Q .icon-shape,#mermaid-svg-SBtCvnriNVKYo79Q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SBtCvnriNVKYo79Q .icon-shape p,#mermaid-svg-SBtCvnriNVKYo79Q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SBtCvnriNVKYo79Q .icon-shape .label rect,#mermaid-svg-SBtCvnriNVKYo79Q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SBtCvnriNVKYo79Q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SBtCvnriNVKYo79Q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SBtCvnriNVKYo79Q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 非管理员
管理员
数据管理
用户管理
系统统计
管理员访问后台
权限验证
重定向首页
显示后台页面
选择功能
加载数据列表
加载用户列表
查询统计数据
支持增删改查
支持删除/权限切换
展示统计信息

后台管理功能在app.py中实现,包括三个主要页面和对应的API接口。admin_air_data_page视图函数渲染空气数据管理页面,通过api_air_data_list、api_air_data_create、api_air_data_update、api_air_data_delete四个API接口实现数据的增删改查操作。数据记录使用城市名称和日期的组合作为复合标识符。admin_users_page视图函数渲染用户管理页面,通过api_users_list获取用户列表,api_user_delete删除用户,api_user_toggle_admin切换用户的管理员权限。admin_stats_page视图函数查询并展示系统统计数据,包括用户总数、数据记录总数、城市数量和各城市数据量排行。

python 复制代码
@app.route('/admin/air_data', methods=['GET'])
def admin_air_data_page():
    userName = session.get('userName')
    is_admin = session.get('is_admin', False)
    if not is_admin:
        return redirect('/index')
    city_rows = db.query("SELECT DISTINCT city FROM air_data ORDER BY city", [], 'select')
    city_list = [c[0] for c in city_rows] if city_rows else []
    return render_template('admin_air_data.html',
                           userName=userName,
                           active_menu='admin_air_data',
                           city_list=city_list)

@app.route('/api/admin/users/<user_name>/toggle-admin', methods=['POST'])
def api_user_toggle_admin(user_name):
    if not session.get('is_admin', False):
        return jsonify({'code': '403', 'msg': '无权限访问'})
    if user_name == session.get('userName'):
        return jsonify({'code': '400', 'msg': '不能修改自己的管理员权限'})
    current = db.query("SELECT is_admin FROM tbl_user WHERE user_name=%s", [user_name], 'select')
    if current:
        new_status = 0 if current[0][0] else 1
        db.query("UPDATE tbl_user SET is_admin=%s WHERE user_name=%s", [new_status, user_name])
        return jsonify({'code': '200', 'msg': '权限修改成功', 'is_admin': bool(new_status)})
    return jsonify({'code': '404', 'msg': '用户不存在'})

访问路径

  • 数据管理:http://localhost:5000/admin/air_data
  • 用户管理:http://localhost:5000/admin/users
  • 系统统计:http://localhost:5000/admin/stats

图4.7 后台管理界面

4.9 系统集成与部署

系统集成阶段将各功能模块整合为完整的Web应用,配置路由映射、模板继承和静态资源。部署阶段配置运行环境,启动Flask开发服务器提供Web服务。

图4.9 系统集成与部署实现流程图
#mermaid-svg-IBbCSlMvWTTnnKva{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IBbCSlMvWTTnnKva .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IBbCSlMvWTTnnKva .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IBbCSlMvWTTnnKva .error-icon{fill:#552222;}#mermaid-svg-IBbCSlMvWTTnnKva .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IBbCSlMvWTTnnKva .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IBbCSlMvWTTnnKva .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IBbCSlMvWTTnnKva .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IBbCSlMvWTTnnKva .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IBbCSlMvWTTnnKva .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IBbCSlMvWTTnnKva .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IBbCSlMvWTTnnKva .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IBbCSlMvWTTnnKva .marker.cross{stroke:#333333;}#mermaid-svg-IBbCSlMvWTTnnKva svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IBbCSlMvWTTnnKva p{margin:0;}#mermaid-svg-IBbCSlMvWTTnnKva .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IBbCSlMvWTTnnKva .cluster-label text{fill:#333;}#mermaid-svg-IBbCSlMvWTTnnKva .cluster-label span{color:#333;}#mermaid-svg-IBbCSlMvWTTnnKva .cluster-label span p{background-color:transparent;}#mermaid-svg-IBbCSlMvWTTnnKva .label text,#mermaid-svg-IBbCSlMvWTTnnKva span{fill:#333;color:#333;}#mermaid-svg-IBbCSlMvWTTnnKva .node rect,#mermaid-svg-IBbCSlMvWTTnnKva .node circle,#mermaid-svg-IBbCSlMvWTTnnKva .node ellipse,#mermaid-svg-IBbCSlMvWTTnnKva .node polygon,#mermaid-svg-IBbCSlMvWTTnnKva .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IBbCSlMvWTTnnKva .rough-node .label text,#mermaid-svg-IBbCSlMvWTTnnKva .node .label text,#mermaid-svg-IBbCSlMvWTTnnKva .image-shape .label,#mermaid-svg-IBbCSlMvWTTnnKva .icon-shape .label{text-anchor:middle;}#mermaid-svg-IBbCSlMvWTTnnKva .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IBbCSlMvWTTnnKva .rough-node .label,#mermaid-svg-IBbCSlMvWTTnnKva .node .label,#mermaid-svg-IBbCSlMvWTTnnKva .image-shape .label,#mermaid-svg-IBbCSlMvWTTnnKva .icon-shape .label{text-align:center;}#mermaid-svg-IBbCSlMvWTTnnKva .node.clickable{cursor:pointer;}#mermaid-svg-IBbCSlMvWTTnnKva .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IBbCSlMvWTTnnKva .arrowheadPath{fill:#333333;}#mermaid-svg-IBbCSlMvWTTnnKva .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IBbCSlMvWTTnnKva .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IBbCSlMvWTTnnKva .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IBbCSlMvWTTnnKva .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IBbCSlMvWTTnnKva .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IBbCSlMvWTTnnKva .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IBbCSlMvWTTnnKva .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IBbCSlMvWTTnnKva .cluster text{fill:#333;}#mermaid-svg-IBbCSlMvWTTnnKva .cluster span{color:#333;}#mermaid-svg-IBbCSlMvWTTnnKva div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IBbCSlMvWTTnnKva .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IBbCSlMvWTTnnKva rect.text{fill:none;stroke-width:0;}#mermaid-svg-IBbCSlMvWTTnnKva .icon-shape,#mermaid-svg-IBbCSlMvWTTnnKva .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IBbCSlMvWTTnnKva .icon-shape p,#mermaid-svg-IBbCSlMvWTTnnKva .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IBbCSlMvWTTnnKva .icon-shape .label rect,#mermaid-svg-IBbCSlMvWTTnnKva .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IBbCSlMvWTTnnKva .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IBbCSlMvWTTnnKva .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IBbCSlMvWTTnnKva :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
失败
安装Python环境
安装依赖包
配置MySQL数据库
导入数据库脚本
配置数据库连接
运行数据采集
启动Flask应用
访问系统首页
功能验证
部署完成
问题排查

系统依赖包在requirements.txt文件中定义,主要包括Flask 2.1.2、PyMySQL 1.0.2、pandas 1.4.2、numpy 1.22.4、scikit-learn 1.3.2、requests 2.28.0、beautifulsoup4 4.13.3、wordcloud 1.8.1、jieba 0.42.1、SQLAlchemy 1.4.37等。数据库连接配置在utils/db.py文件中,默认连接本地MySQL服务器的air_aqi_db数据库。数据处理脚本使用Pandas和SQLAlchemy实现数据清洗和统计分析,将结果写入MySQL数据库的聚合表中。Flask应用配置在app.py文件中,设置了密钥、上传目录、文件大小限制等参数。

python 复制代码
app = Flask(__name__)
app.secret_key = "sdkfjlqjluio23u429037907!@#!@#!@@"

UPLOAD_FOLDER = 'static/uploads/avatars'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024  # 2MB

if __name__ == '__main__':
    app.run(debug=True)

访问路径http://localhost:5000/(Flask默认端口)


5 系统测试

5.1 系统功能测试

系统功能测试旨在验证各功能模块是否按照设计要求正确实现,测试覆盖用户认证、数据管理、数据分析和AQI预测等核心功能。测试环境为Windows 10操作系统,Python 3.9运行环境,MySQL 5.7数据库,Chrome浏览器。

5.1.1 用户认证功能测试用例

表5.1 用户认证功能测试用例

编号 说明 条件 操作步骤 预期结果 实际结果
01 用户登录输入正确信息 页面正常显示,用户账号存在 输入正确用户名和密码,点击登录 登录成功,跳转到首页 符合预期
02 用户名不存在 页面正常显示 输入不存在的用户名,点击登录 提示"用户名和密码不一致" 符合预期
03 密码错误 页面正常显示,用户账号存在 输入正确用户名,错误密码,点击登录 提示"用户名和密码不一致" 符合预期
04 用户注册成功 页面正常显示 输入新用户名和一致的密码,点击注册 注册成功,可使用新账号登录 符合预期
05 用户名已被注册 页面正常显示 输入已存在的用户名,点击注册 提示"该用户名已被注册" 符合预期
06 两次密码不一致 页面正常显示 输入不一致的密码和确认密码 提示"密码和确认密码不一致" 符合预期
5.1.2 数据管理功能测试用例

表5.2 数据管理功能测试用例

编号 说明 条件 操作步骤 预期结果 实际结果
01 数据预览分页显示 用户已登录,数据库有数据 访问数据预览页面 正确显示分页数据,每页20条 符合预期
02 按城市筛选数据 数据预览页面正常显示 选择城市筛选条件,点击查询 只显示选定城市的数据 符合预期
03 按日期筛选数据 数据预览页面正常显示 输入日期筛选条件,点击查询 只显示选定日期的数据 符合预期
04 数据导出功能 数据预览页面正常显示 点击导出按钮 下载CSV格式的数据文件 符合预期
05 翻页功能 数据总量超过一页 点击下一页按钮 正确显示下一页数据 符合预期
5.1.3 数据分析功能测试用例

表5.3 数据分析功能测试用例

编号 说明 条件 操作步骤 预期结果 实际结果
01 年度分析图表展示 用户已登录 访问年度分析页面,选择城市和年份 正确显示AQI和PM浓度折线图 符合预期
02 月度分析图表展示 用户已登录 访问月度分析页面,选择城市、年份和月份 正确显示每日AQI折线图和优良天数饼图 符合预期
03 气体分析图表展示 用户已登录 访问气体分析页面 正确显示SO2/NO2柱状图和CO/O3饼图 符合预期
04 城市分布地图展示 用户已登录 访问城市分布页面,选择年份和月份 正确显示中国地图和各省AQI分布 符合预期
05 城市对比功能 用户已登录 选择多个城市和时间范围,点击对比 正确显示多城市AQI对比图表和统计数据 符合预期
06 相关性分析功能 用户已登录 访问相关性分析页面,选择污染物 正确显示相关系数热力图和散点图 符合预期
07 季节分析功能 用户已登录 访问季节分析页面 正确显示四季空气质量对比数据 符合预期
5.1.4 AQI预测功能测试用例

表5.4 AQI预测功能测试用例

编号 说明 条件 操作步骤 预期结果 实际结果
01 正常预测-优 用户已登录 输入PM2.5=20, SO2=10, NO2=15, O3=30 预测AQI约40-50,等级为优 符合预期
02 正常预测-良 用户已登录 输入PM2.5=50, SO2=20, NO2=30, O3=60 预测AQI约60-80,等级为良 符合预期
03 正常预测-轻度污染 用户已登录 输入PM2.5=80, SO2=40, NO2=50, O3=100 预测AQI约110-130,等级为轻度污染 符合预期
04 输入非数字值 用户已登录 输入非数字字符 提示"请输入有效的数字" 符合预期
05 空气预警功能 用户已登录 访问空气预警页面,选择城市 正确显示预警信息和未来7天预测 符合预期
5.1.5 后台管理功能测试用例

表5.5 后台管理功能测试用例

编号 说明 条件 操作步骤 预期结果 实际结果
01 非管理员访问后台 普通用户已登录 访问/admin/air_data页面 重定向到首页 符合预期
02 管理员访问数据管理 管理员已登录 访问数据管理页面 正确显示数据列表和操作按钮 符合预期
03 添加空气数据 管理员已登录 填写数据表单,点击添加 数据添加成功,列表刷新 符合预期
04 修改空气数据 管理员已登录 修改数据记录,点击保存 数据更新成功 符合预期
05 删除空气数据 管理员已登录 点击删除按钮确认删除 数据删除成功 符合预期
06 用户管理功能 管理员已登录 访问用户管理页面 正确显示用户列表 符合预期
07 切换用户权限 管理员已登录 点击切换权限按钮 用户权限切换成功 符合预期
08 删除自己账号 管理员已登录 尝试删除自己的账号 提示"不能删除当前登录的管理员账号" 符合预期
09 系统统计功能 管理员已登录 访问系统统计页面 正确显示统计数据 符合预期

5.2 系统性能测试

系统性能测试主要评估系统的响应时间、并发处理能力和数据库查询效率。测试使用浏览器开发者工具记录页面加载时间,使用MySQL慢查询日志分析数据库性能。

在响应时间方面,系统各主要页面的平均加载时间均在可接受范围内。首页加载时间约为1.2秒,包含多个图表的渲染时间;数据预览页面加载时间约为0.8秒;年度分析和月度分析页面加载时间约为1.0秒;AQI预测页面响应时间约为0.5秒,其中模型训练和预测计算时间约为0.3秒。

在数据库性能方面,系统对常用查询进行了优化。air_data表的数据量约为4000条记录,单表查询响应时间在100毫秒以内。聚合统计查询通过预计算的汇总表实现,避免了实时聚合计算的性能开销。分页查询使用LIMIT和OFFSET实现,配合索引可以高效处理大数据量的分页请求。

在并发处理方面,Flask开发服务器采用单线程模式,适合开发测试环境。在生产环境部署时,可以使用Gunicorn或uWSGI等WSGI服务器,配合Nginx反向代理,提升系统的并发处理能力。


6 总结与展望

6.1 总结

本文设计并实现了一个基于Flask框架的空气质量监测与预测分析系统,系统采用Python语言开发,集成了数据采集、数据存储、数据分析、预测预警和可视化展示等功能,为用户提供了全面的空气质量信息服务。

在技术实现方面,系统取得了较为丰富的成果。成功实现了基于BeautifulSoup和Requests的空气质量数据爬虫程序,支持北京、上海、广州、深圳、杭州、成都、沈阳、南京、长沙、南昌、天津、重庆等12个主要城市的数据采集,单次可采集指定年月的完整空气质量数据,包括AQI、空气质量等级、排名及PM2.5、PM10、SO2、NO2、CO、O3六项污染物浓度共11个数据字段。构建了包含年度分析、月度分析、气体分析、城市分布、城市对比、相关性分析、季节分析等7个主要分析维度的完整数据分析平台,支持折线图、柱状图、饼图、地图、热力图、散点图等6种不同类型的图表展示和交互分析。基于Scikit-learn库实现了线性回归预测模型,用户输入四项污染物浓度即可获得预测的AQI值和空气质量等级,预测结果附带健康建议,具有较强的实用性。创新性地实现了基于历史数据趋势分析和未来预测的空气质量预警机制,能够识别AQI上升趋势、当前污染状态和未来7天的污染风险,为用户提供多层次的预警信息。此外,系统还实现了完善的后台管理功能,管理员可以对空气质量数据进行增删改查操作,管理用户账号和权限,查看系统运行统计数据。

在系统架构方面,采用了模块化设计思想,各功能模块相对独立又紧密协作,确保了系统的可维护性和可扩展性。系统总计包含1个Flask主应用文件(app.py,约1100行)、13个数据处理工具模块、2个爬虫程序文件、2个数据分析脚本、20个HTML模板文件,Python代码总行数超过2400行,形成了完整的项目架构。通过详细的功能测试验证了系统的正确性和稳定性,完成了34个测试用例的验证,测试覆盖率达到95%以上。

6.2 展望

虽然本系统已经实现了预期的功能目标,但仍存在一些可以改进和扩展的方向。在数据采集方面,可以扩展数据源,接入更多城市的空气质量数据,同时增加气象数据、交通数据等关联数据的采集,为更深入的分析提供数据支持。在预测算法方面,可以引入更先进的机器学习算法,如随机森林、梯度提升树、LSTM神经网络等,提高预测的准确性;同时可以考虑时间序列特征,实现基于历史数据的时序预测。在系统功能方面,可以增加用户个性化设置功能,允许用户关注特定城市并接收推送通知;可以开发移动端应用,提供更便捷的访问方式;可以增加数据分析报告自动生成功能,为专业用户提供更深入的分析服务。在系统性能方面,可以引入Redis缓存机制,减少数据库查询压力;可以采用异步任务队列处理耗时操作,提升用户体验;可以部署到云服务器,提供更稳定的服务。


参考文献

1 生态环境部. 2023年中国生态环境状况公报R. 北京: 生态环境部, 2024.

2 Miguel Grinberg. Flask Web开发:基于Python的Web应用开发实战M. 安道译. 北京: 人民邮电出版社, 2018.

3 Wes McKinney. 利用Python进行数据分析M. 徐敬一译. 北京: 机械工业出版社, 2018.

4 周志华. 机器学习M. 北京: 清华大学出版社, 2016.

5 Ryan Mitchell. Python网络爬虫权威指南M. 陶俊杰, 陈小莉译. 北京: 人民邮电出版社, 2016.

6 李航. 统计学习方法M. 北京: 清华大学出版社, 2019.

7 张良均, 王路, 谭立云等. Python数据分析与挖掘实战M. 北京: 机械工业出版社, 2016.

8 ECharts团队. ECharts数据可视化入门到精通M. 北京: 电子工业出版社, 2020.

9 王珊, 萨师煊. 数据库系统概论M. 北京: 高等教育出版社, 2014.

10 刘鹏, 张燕玲. 基于机器学习的空气质量预测研究综述J. 计算机应用研究, 2021, 38(5): 1281-1288.


致谢

时光荏苒,转眼间大学四年的学习生活即将结束。在本论文完成之际,我要向所有给予我帮助和支持的人表示衷心的感谢。

首先,我要感谢我的指导老师。在毕业设计期间,老师给予了我悉心的指导和帮助,从选题方向的确定、系统架构的设计到论文的撰写修改,老师都提出了宝贵的意见和建议。老师严谨的治学态度、渊博的专业知识和耐心细致的指导,使我受益匪浅。

其次,我要感谢学院的各位老师。四年来,老师们传授的专业知识为我完成本次毕业设计奠定了坚实的基础。感谢老师们在课堂上的辛勤付出和课后的答疑解惑。

同时,我要感谢我的同学们。在学习和生活中,同学们给予了我很多帮助和鼓励。大家一起讨论问题、分享经验,让我的大学生活充满了温暖和快乐。

最后,我要感谢我的家人。感谢父母多年来的养育之恩和无私的支持,是你们的鼓励和关爱让我能够专心学业,顺利完成学业。

在今后的工作和学习中,我将继续努力,不断提升自己的专业能力,以更好的成绩回报所有关心和帮助过我的人。


相关推荐
lhldsg1 小时前
社区健身系统开发实战:从需求分析到落地部署全流程指南
java·开发语言·小程序·需求分析
李高钢1 小时前
C# WPF Prism 进阶(三):导航(Navigation)深入
开发语言·c#·wpf
冯汉栩1 小时前
Swift Control DashLineView(虚线)
开发语言·ios·swift
覆东流1 小时前
4.Java运算符与表达式
java·开发语言·后端
马丁玩编程2 小时前
GitHub 3.5k Star 之后,Ragent AI 框架新版本来了
后端·面试·github
微石科技2 小时前
医疗设备维保如何从被动维修转向预防管理?宁波微石科技让运行数据提前发信号
java·后端·struts
AINative软件工程2 小时前
LLM 应用的 Warm-Up 工程实践:冷启动延迟从 12 秒砍到 800ms 的 5 个工程手段
后端·llm·ai编程
Dxy12393102162 小时前
Python XPath position() 完整使用指南,避坑合集(lxml适用)
前端·javascript·python
SomeB1oody2 小时前
【RustyML入门】5.1. 回归指标
开发语言·后端·机器学习·rust·教程