news 2026/9/4 3:36:12

Python+Flask+ECharts构建天气数据采集与可视化分析系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python+Flask+ECharts构建天气数据采集与可视化分析系统实战

简介:这是一套面向气象数据分析初学者与Python Web开发学习者的完整实践项目,聚焦山东省天气数据的实时采集、结构化存储与多维度可视化分析。系统基于Flask构建轻量级Web服务,集成requests、pandas、matplotlib等主流库,实现温度、湿度、风速、降水等要素的自动爬取、CSV持久化及热力图、折线图、柱状图等交互式图表展示,适用于课程设计、科研辅助或地方气候观察场景。压缩包共140个文件(3.26MB),含35个气象CSV数据样本、31张可视化结果JPG图、19个前端交互JS脚本、12个PNG图标资源、10个CSS样式文件及4个核心Python后端模块,前端采用Bootstrap+Font Awesome+jQuery生态,结构清晰、开箱即用。已有120人下载学习,提供可直接运行的完整工程目录、配套静态资源与基础数据集,省去环境配置与接口调试环节,便于快速理解Web爬虫、数据清洗与前后端协同可视化全流程。

1. 项目缘起:为什么我们需要一个自己的天气数据系统?

作为一名长期和数据打交道的开发者,我经常遇到一个看似简单却颇为棘手的需求:需要某个地区,比如山东省,连续、稳定、多维度的天气数据来做分析。无论是为了研究气候变化趋势,还是为某个智能农业、物流调度项目提供决策支持,亦或是单纯想做一个酷炫的个人数据看板,你都会发现,直接可用的、干净的、结构化的历史与实时天气数据源,要么收费昂贵,要么接口不稳定,要么数据维度单一。

市面上有很多天气API,但它们往往有调用频率限制,历史数据查询要么不提供,要么是付费服务。更重要的是,这些数据是“别人的”,你无法按照自己的业务逻辑进行深度定制化的存储、清洗和分析。比如,你想分析过去五年济南市夏季高温天与降水量的相关性,并在地图上用热力图展示各城市的高温分布,用折线图追踪某个城市全年的湿度变化——这种复合型需求,通用API很难一站式满足。

于是,自己动手搭建一个“山东省天气数据爬取及可视化分析系统”的想法就变得非常实际。这不仅仅是一个技术Demo,它是一个能解决真实问题的生产级工具雏形。通过Python的灵活性和Flask框架的轻量级Web能力,我们可以构建一个从数据采集、存储、处理到可视化展示的完整闭环。这个系统能让你真正“拥有”数据,并按照你的想法去“驾驭”数据。接下来,我将详细拆解如何从零开始构建这样一个系统,其中会包含大量我在实际开发中踩过的坑和总结的经验。

2. 系统架构设计与技术选型背后的思考

在动手写代码之前,清晰的架构设计能避免后期大量的返工。我们这个系统的核心目标是:稳定获取数据、可靠存储数据、灵活分析数据、直观展示数据。围绕这个目标,我设计了如下架构,并解释了每一个技术选型背后的理由。

整个系统可以划分为四个核心层:数据采集层、数据存储层、数据处理层和可视化应用层。它们通过Flask这个轻量级的Web框架有机地整合在一起。

2.1 数据采集层:爬虫的稳健之道

数据源是系统的血液。对于天气数据,我们需要两类:实时数据历史数据

  • 实时数据源:目标是获取山东省各个地级市当前最新的温度、湿度、风速、风向、降水量、气压等要素。我选择从中国天气网等提供公开数据的网站进行抓取。为什么不直接用API?因为免费的公共API往往不稳定或有限制,而爬取公开网页虽然需要解析HTML,但数据源相对稳定可控。

    • 工具选型requests+BeautifulSoup4。这是Python生态中最经典、最稳定的HTTP请求和HTML解析库组合。requests用于模拟浏览器发送请求,BeautifulSoup4用于从复杂的HTML标签中精准提取我们需要的数据。
    • 关键考量:必须处理反爬机制。这类气象网站通常会有简单的反爬,比如检查请求头(User-Agent)。我们的爬虫需要伪装成正常的浏览器访问,并合理设置请求间隔,避免对目标服务器造成压力,这也是职业道德和可持续爬取的前提。

    注意:爬虫的伦理和法律边界必须遵守。我们只爬取公开的、非敏感的气象数据,用于个人学习和分析,绝不进行商业牟利或恶意攻击。在代码中务必设置合理的延时(如time.sleep(2)),并尊重网站的robots.txt协议。

  • 历史数据获取:历史数据是分析趋势的基础。一个可靠的来源是国家气象科学数据中心等官方机构,它们通常会提供历史气象数据集的下载。另一种方案是利用一些聚合了历史数据的第三方API(可能有免费额度)。在我们的系统中,我假设我们已经通过合法渠道获得了一份结构化的山东省历史天气CSV或Excel文件,作为系统的初始化数据。

2.2 数据存储层:SQLite与MySQL的抉择

爬取到的数据需要持久化存储。这里面临一个经典选择:轻量级的SQLite还是功能更全面的MySQL/PostgreSQL?

  • SQLite:它是一个服务器进程、零配置的数据库,整个数据库就是一个文件。对于个人项目、原型系统或中小型数据量(比如单个省几年的天气数据,总量在几十万到百万条记录级别)来说,SQLite是绝佳选择。它部署简单,无需安装和配置独立的数据库服务,用Python标准库sqlite3即可直接操作。
  • MySQL/PostgreSQL:更适合企业级应用,支持高并发、复杂的用户权限管理、存储过程等。如果预期未来数据量极大(数亿条),或者需要多用户同时进行复杂的读写操作,应该选择它们。

我的选择与理由:对于这个“山东省天气数据”系统,我优先推荐使用SQLite。理由如下:

  1. 简化部署:项目可以一键运行,无需用户额外安装配置数据库环境,降低了使用门槛。
  2. 性能足够:山东省17个地级市,假设每天采集4次数据,一年约17 * 4 * 365 ≈ 2.5万条记录。存储10年也才25万条,对SQLite来说游刃有余。
  3. 开发便捷:在Flask中集成SQLite极其简单,配合SQLAlchemyORM(对象关系映射)工具,可以用Python类来定义数据表,让数据库操作像操作普通Python对象一样直观。

当然,我们在设计数据表时,会保持ORM模型的独立性。这意味着如果未来数据量激增,需要迁移到MySQL,只需修改Flask的数据库连接配置即可,业务代码几乎不用改动。这是使用ORM带来的巨大优势。

2.3 数据处理与服务层:Flask与Pandas的黄金组合

这是系统的“大脑”,负责协调所有模块,并提供数据接口。

  • Flask框架:它是一个“微”框架,核心简单但扩展性强。我们用它来做什么?
    • 构建Web应用:承载我们的数据可视化前端页面。
    • 提供RESTful API:为前端图表提供数据接口。例如,前端需要济南市2023年的温度数据,就向Flask服务器发送一个请求到/api/temperature/jinan/2023,Flask处理后返回JSON格式的数据。
    • 调度后台任务:我们可以利用APScheduler等库,在Flask应用中定时执行爬虫任务,实现数据的自动更新。
  • Pandas库:它是数据分析的“瑞士军刀”。当我们需要从数据库中查询出一段时间的数据进行分析时(比如计算月平均温度、找出风速最大的十天),用Pandas进行分组、聚合、筛选、计算,比直接写复杂的SQL语句要直观和高效得多。Flask处理完API请求后,可以很方便地调用Pandas进行数据加工,再将结果返回给前端。

2.4 可视化展示层:ECharts的强大与易用

前端图表库的选择至关重要,它直接决定最终展示效果的美观度和交互性。我强烈推荐Apache ECharts

  • 为什么是ECharts?
    1. 功能全面:完美支持我们需要的地图(热力图)折线图柱状图,以及散点图、饼图等几乎所有常见图表类型。
    2. 地图支持好:对中国的省市县地图支持非常完善,可以轻松绘制山东省地图,并在地图上以热力图或散点图形式展示各城市的气象数据分布。
    3. 配置灵活:通过JSON配置项就能实现极其丰富的视觉效果和交互功能,学习曲线相对平缓。
    4. 社区活跃:中文文档丰富,遇到问题容易找到解决方案。

前端页面我们使用简单的HTML、CSS和JavaScript,通过Ajax技术调用Flask提供的API,获取数据后动态渲染ECharts图表。这样前后端分离,结构清晰。

3. 从零开始:环境搭建与核心代码实现

理论讲完,我们进入实战环节。我会手把手带你搭建环境,并讲解核心模块的代码。请确保你的电脑上已经安装了Python(3.7及以上版本)。

3.1 项目初始化与依赖安装

首先,创建一个项目文件夹,例如sd-weather-system。在文件夹内,我们建议使用虚拟环境来管理依赖,避免污染全局Python环境。

# 在项目根目录下 python -m venv venv # 创建虚拟环境 # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 激活后,命令行提示符前会出现 (venv) 标识

接下来,创建一个requirements.txt文件,列出所有需要的库:

Flask==2.3.3 requests==2.31.0 beautifulsoup4==4.12.2 pandas==2.0.3 sqlalchemy==2.0.19 apscheduler==3.10.4

然后使用pip安装:

(venv) pip install -r requirements.txt

3.2 数据库模型设计

在项目根目录下创建app.py作为主程序入口,同时我们创建一个models.py来定义数据模型。

models.py

from flask_sqlalchemy import SQLAlchemy from datetime import datetime # 先创建db对象,在app.py中初始化 db = SQLAlchemy() class City(db.Model): """城市表""" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(20), unique=True, nullable=False) # 城市名,如‘济南’ code = db.Column(db.String(10), unique=True) # 城市代码,用于爬虫定位 # 定义关系,一个城市有多条天气记录 weather_records = db.relationship('WeatherRecord', backref='city', lazy=True) class WeatherRecord(db.Model): """天气记录表(存储实时和历史数据)""" id = db.Column(db.Integer, primary_key=True) city_id = db.Column(db.Integer, db.ForeignKey('city.id'), nullable=False) date = db.Column(db.Date, nullable=False) # 日期 time = db.Column(db.Time) # 时间(对于实时数据或逐小时历史数据) temperature = db.Column(db.Float) # 温度,单位:摄氏度 humidity = db.Column(db.Float) # 湿度,单位:百分比 wind_speed = db.Column(db.Float) # 风速,单位:m/s wind_direction = db.Column(db.String(10)) # 风向 precipitation = db.Column(db.Float) # 降水量,单位:mm pressure = db.Column(db.Float) # 气压,单位:hPa # 添加唯一约束,防止同一城市同一时刻的数据重复插入 __table_args__ = (db.UniqueConstraint('city_id', 'date', 'time', name='_city_date_time_uc'),) def to_dict(self): """将记录转换为字典,方便API返回JSON""" return { 'id': self.id, 'city': self.city.name, 'date': self.date.isoformat() if self.date else None, 'time': self.time.isoformat()[:5] if self.time else None, # 只返回时分 'temperature': self.temperature, 'humidity': self.humidity, 'wind_speed': self.wind_speed, 'wind_direction': self.wind_direction, 'precipitation': self.precipitation, 'pressure': self.pressure }

设计思路

  1. 城市天气记录分开成两张表,这是典型的数据库范式化设计,避免了数据冗余。如果每个天气记录都重复存储城市名,既浪费空间,更新城市信息时也会很麻烦。
  2. WeatherRecord表包含了我们关心的所有气象要素字段。datetime字段分开存储,便于按天或按小时进行聚合查询。
  3. UniqueConstraint唯一约束是关键。它能防止调度任务意外重复运行时,向数据库插入重复的实时数据,确保数据的唯一性。
  4. to_dict方法是一个实用技巧,它让我们在将SQLAlchemy对象通过Flask的jsonify返回给前端时,能轻松地将其转换为字典格式。

3.3 实时数据爬虫实现

创建一个spider.py文件。这里以模拟抓取为例,实际爬取需要你根据目标网站的HTML结构调整解析逻辑。

spider.py

import requests from bs4 import BeautifulSoup import time from datetime import datetime from models import db, City, WeatherRecord def fetch_real_time_weather(): """ 模拟抓取山东省各城市实时天气。 实际应用中,你需要替换URL和解析逻辑。 """ # 假设我们有一个城市代码映射字典 city_codes = { '济南': '101120101', '青岛': '101120201', '淄博': '101120301', # ... 添加山东省其他城市 } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } for city_name, code in city_codes.items(): try: # 构造请求URL (此处为示例URL,需替换为真实地址) url = f'http://www.weather.com.cn/weather/{code}.shtml' print(f'正在抓取 {city_name} 的天气...') resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() # 检查请求是否成功 resp.encoding = 'utf-8' soup = BeautifulSoup(resp.text, 'html.parser') # --- 这里是解析逻辑示例,需要你根据实际网页结构调整 --- # 假设温度在某个class为'tem'的span标签里 temp_tag = soup.find('span', class_='tem') temperature = float(temp_tag.text.replace('℃', '')) if temp_tag else None # 假设湿度在某个class为‘hum’的div里 hum_tag = soup.find('div', class_='hum') humidity = float(hum_tag.text.replace('%', '')) if hum_tag else None # 风速、风向等类似解析... # 这里我们模拟一些数据 import random temperature = round(random.uniform(15, 35), 1) humidity = round(random.uniform(30, 90), 1) wind_speed = round(random.uniform(1, 10), 1) wind_direction = random.choice(['东风', '南风', '西风', '北风']) precipitation = round(random.uniform(0, 5), 1) pressure = round(random.uniform(1000, 1030), 1) # --- 解析结束 --- # 获取或创建城市记录 city = City.query.filter_by(name=city_name).first() if not city: city = City(name=city_name, code=code) db.session.add(city) db.session.flush() # 获取city的id # 创建天气记录 now = datetime.now() record = WeatherRecord( city_id=city.id, date=now.date(), time=now.time(), temperature=temperature, humidity=humidity, wind_speed=wind_speed, wind_direction=wind_direction, precipitation=precipitation, pressure=pressure ) # 使用db.session.merge处理唯一约束冲突 # 如果存在相同城市、日期、时间的记录,则更新;否则插入 db.session.merge(record) # 礼貌性延时,避免请求过快 time.sleep(1) except requests.RequestException as e: print(f'抓取{city_name}数据时发生网络错误: {e}') except Exception as e: print(f'处理{city_name}数据时发生未知错误: {e}') try: db.session.commit() print('实时天气数据抓取并保存完成!') except Exception as e: db.session.rollback() print(f'数据提交到数据库失败: {e}') if __name__ == '__main__': # 这个if块用于单独测试爬虫 from app import app with app.app_context(): fetch_real_time_weather()

关键点解析

  1. 错误处理:网络爬虫必须健壮。我们用try...except包裹了每个城市的抓取过程,即使某个城市抓取失败,也不会影响其他城市,并且会打印出错误信息便于排查。
  2. 请求头:设置User-Agent是绕过基础反爬的最基本措施。
  3. 延时time.sleep(1)是必须的,体现了对目标网站的尊重,也是保证爬虫能长期稳定运行的基础。
  4. 数据库操作:使用db.session.merge(record)是一个高级技巧。它基于模型定义的主键或唯一约束(我们定义的UniqueConstraint)来判断是插入新记录还是更新已有记录。这完美解决了定时任务可能导致的重复数据问题。
  5. 会话管理:所有的数据库增删改操作,最后需要通过db.session.commit()一次性提交,如果中间出错,则db.session.rollback()回滚,保证数据一致性。

3.4 Flask应用整合与API设计

现在,我们在app.py中整合所有部件,并创建Web服务和数据API。

app.py

from flask import Flask, render_template, jsonify, request from models import db, City, WeatherRecord from spider import fetch_real_time_weather from apscheduler.schedulers.background import BackgroundScheduler from datetime import datetime, timedelta import pandas as pd app = Flask(__name__) # 配置SQLite数据库路径 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///weather.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 初始化db db.init_app(app) # 创建数据库表(如果不存在) with app.app_context(): db.create_all() # --- 配置定时任务 --- scheduler = BackgroundScheduler() # 每隔1小时执行一次爬虫任务 scheduler.add_job(func=fetch_real_time_weather, trigger='interval', hours=1, id='fetch_weather_job') scheduler.start() # 确保在应用退出时关闭调度器 import atexit atexit.register(lambda: scheduler.shutdown()) # --- 定义路由 --- @app.route('/') def index(): """首页,展示可视化图表""" return render_template('index.html') @app.route('/api/current_weather') def get_current_weather(): """API: 获取所有城市的最新一条实时天气""" # 一个子查询,找到每个城市最新的记录时间 subquery = db.session.query( WeatherRecord.city_id, db.func.max(WeatherRecord.date).label('max_date'), db.func.max(WeatherRecord.time).label('max_time') ).group_by(WeatherRecord.city_id).subquery() # 关联查询,获取每个城市最新的那条完整记录 latest_records = db.session.query(WeatherRecord).join( subquery, db.and_( WeatherRecord.city_id == subquery.c.city_id, WeatherRecord.date == subquery.c.max_date, WeatherRecord.time == subquery.c.max_time ) ).all() result = [record.to_dict() for record in latest_records] return jsonify(result) @app.route('/api/historical_weather') def get_historical_weather(): """API: 获取指定城市、日期范围的历史天气数据,用于绘制折线图/柱状图""" city_name = request.args.get('city', '济南') start_date = request.args.get('start', (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')) end_date = request.args.get('end', datetime.now().strftime('%Y-%m-%d')) data_type = request.args.get('type', 'temperature') # 默认查询温度 city = City.query.filter_by(name=city_name).first() if not city: return jsonify({'error': 'City not found'}), 404 records = WeatherRecord.query.filter( WeatherRecord.city_id == city.id, WeatherRecord.date.between(start_date, end_date) ).order_by(WeatherRecord.date, WeatherRecord.time).all() # 使用Pandas进行数据处理:按日期分组计算日平均值 data_list = [{'date': r.date.isoformat(), data_type: getattr(r, data_type)} for r in records] if not data_list: return jsonify([]) df = pd.DataFrame(data_list) # 将字符串日期转换为datetime对象以便分组 df['date'] = pd.to_datetime(df['date']) # 按日期分组,计算平均值(如果一天有多个时间点的数据) daily_avg = df.groupby(df['date'].dt.date)[data_type].mean().reset_index() daily_avg['date'] = daily_avg['date'].astype(str) # 转换回字符串方便JSON序列化 result = daily_avg.to_dict('records') return jsonify(result) @app.route('/api/city_list') def get_city_list(): """API: 获取系统中所有城市列表""" cities = City.query.all() result = [{'name': city.name, 'code': city.code} for city in cities] return jsonify(result) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000)

代码详解

  1. Flask配置:非常简单,主要就是配置数据库连接。SQLALCHEMY_TRACK_MODIFICATIONS设置为False是为了避免不必要的内存开销。
  2. 定时任务:使用APScheduler在后台启动一个调度器,每隔1小时自动执行一次fetch_real_time_weather函数。这样我们的系统就具备了自动更新数据的能力。BackgroundScheduler确保任务在独立的线程中运行,不会阻塞Flask的主Web线程。
  3. API设计
    • /api/current_weather:这个接口的查询有点技巧。为了获取每个城市最新的一条数据,我们使用了SQLAlchemy的子查询(subquery)和分组聚合(max函数)。这是数据库查询中一个经典的“分组取最新”问题。返回的数据可以直接用于首页展示当前天气概况或地图热力图。
    • /api/historical_weather:这是为折线图/柱状图提供数据的核心接口。它接收城市名、起止日期和要查询的数据类型(如温度、湿度)作为参数。这里展示了Pandas的强大之处:我们从数据库拿到原始数据后,用Pandas的groupbymean方法,轻松地计算出了每天的该气象要素的平均值,这对于趋势分析非常有用。
    • /api/city_list:一个简单的辅助接口,用于前端动态生成城市选择下拉框。
  4. 参数处理:使用request.args.get()安全地获取URL查询参数,并提供了默认值,增强了API的健壮性。

3.5 前端可视化页面实现

前端我们创建一个简单的index.html,放在项目根目录下的templates文件夹中。这里使用ECharts来绘制地图和图表。

templates/index.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>山东省天气数据可视化分析系统</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts/map/js/china.js"></script> <style> body { font-family: 'Microsoft YaHei', sans-serif; margin: 20px; background-color: #f5f5f5; } .container { display: flex; flex-wrap: wrap; gap: 20px; } .chart-box { flex: 1; min-width: 500px; height: 500px; background: white; border-radius: 8px; padding: 15px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .controls { margin-bottom: 20px; padding: 15px; background: white; border-radius: 8px; } select, input, button { margin-right: 10px; padding: 8px 12px; } h2, h3 { color: #333; } </style> </head> <body> <h2>🌤️ 山东省天气数据可视化分析系统</h2> <div class="controls"> <h3>数据筛选</h3> <label>选择城市:</label> <select id="citySelect"> <option value="济南">济南</option> <!-- 其他城市将通过JS动态加载 --> </select> <label>选择数据类型:</label> <select id="dataTypeSelect"> <option value="temperature">温度 (°C)</option> <option value="humidity">湿度 (%)</option> <option value="wind_speed">风速 (m/s)</option> <option value="precipitation">降水量 (mm)</option> </select> <label>开始日期:</label> <input type="date" id="startDate"> <label>结束日期:</label> <input type="date" id="endDate"> <button onclick="loadHistoricalChart()">加载历史趋势图</button> <button onclick="loadMapChart()">刷新实时地图</button> </div> <div class="container"> <div id="mapChart" class="chart-box"></div> <div id="lineChart" class="chart-box"></div> <div id="barChart" class="chart-box"></div> </div> <script> // 初始化ECharts实例 const mapChart = echarts.init(document.getElementById('mapChart')); const lineChart = echarts.init(document.getElementById('lineChart')); const barChart = echarts.init(document.getElementById('barChart')); // 1. 加载城市列表 fetch('/api/city_list') .then(res => res.json()) .then(cities => { const select = document.getElementById('citySelect'); select.innerHTML = ''; // 清空默认选项 cities.forEach(city => { const option = document.createElement('option'); option.value = city.name; option.textContent = city.name; select.appendChild(option); }); // 默认加载第一个城市的数据和地图 loadMapChart(); loadHistoricalChart(); }); // 2. 加载实时数据地图(热力图) function loadMapChart() { fetch('/api/current_weather') .then(res => res.json()) .then(data => { // 准备地图数据:将城市名映射到坐标(这里简化处理,实际项目需要城市坐标字典) // 示例坐标,实际需要更精确的经纬度 const cityCoords = { '济南': [117.12, 36.65], '青岛': [120.38, 36.07], '淄博': [118.05, 36.81], // ... 其他城市坐标 }; const mapData = data.map(item => { const coord = cityCoords[item.city] || [0, 0]; return { name: item.city, value: [...coord, item.temperature] // 值放在第三维,用于热力图 }; }); const option = { title: { text: '山东省实时温度分布(热力图)', left: 'center' }, tooltip: { formatter: function(params) { return `${params.name}<br/>温度:${params.value[2]}°C`; } }, visualMap: { min: Math.min(...mapData.map(d => d.value[2])), max: Math.max(...mapData.map(d => d.value[2])), calculable: true, inRange: { color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026'] }, textStyle: { color: '#000' } }, series: [{ name: '温度', type: 'heatmap', coordinateSystem: 'geo', data: mapData, pointSize: 15, blurSize: 10 }], geo: { map: 'china', roam: false, zoom: 1.2, center: [118, 36.5], label: { emphasis: { show: false } }, itemStyle: { areaColor: '#f0f8ff', borderColor: '#aaa' } } }; mapChart.setOption(option); }); } // 3. 加载历史趋势图(折线图) function loadHistoricalChart() { const city = document.getElementById('citySelect').value; const dataType = document.getElementById('dataTypeSelect').value; const start = document.getElementById('startDate').value || getDefaultDate(-30); const end = document.getElementById('endDate').value || getDefaultDate(0); const url = `/api/historical_weather?city=${city}&type=${dataType}&start=${start}&end=${end}`; fetch(url) .then(res => res.json()) .then(data => { if (data.error) { console.error(data.error); return; } const dates = data.map(item => item.date); const values = data.map(item => item[dataType]); const lineOption = { title: { text: `${city} - ${getDataTypeName(dataType)}历史趋势`, left: 'center' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: dates, axisLabel: { rotate: 45 } }, yAxis: { type: 'value', name: getDataTypeUnit(dataType) }, series: [{ data: values, type: 'line', smooth: true, lineStyle: { width: 3 }, itemStyle: { color: '#5470c6' } }] }; lineChart.setOption(lineOption); // 同时用柱状图展示(例如展示降水量用柱状图更直观) const barOption = { title: { text: `${city} - ${getDataTypeName(dataType)}历史数据(柱状图)`, left: 'center' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: dates, axisLabel: { rotate: 45 } }, yAxis: { type: 'value', name: getDataTypeUnit(dataType) }, series: [{ data: values, type: 'bar', itemStyle: { color: '#91cc75' } }] }; barChart.setOption(barOption); }); } // 工具函数 function getDefaultDate(offsetDays) { const d = new Date(); d.setDate(d.getDate() + offsetDays); return d.toISOString().split('T')[0]; } function getDataTypeName(type) { const map = { temperature: '温度', humidity: '湿度', wind_speed: '风速', precipitation: '降水量' }; return map[type] || type; } function getDataTypeUnit(type) { const map = { temperature: '°C', humidity: '%', wind_speed: 'm/s', precipitation: 'mm' }; return map[type] || ''; } // 初始化日期输入框 document.getElementById('startDate').value = getDefaultDate(-30); document.getElementById('endDate').value = getDefaultDate(0); // 窗口大小改变时重绘图表 window.addEventListener('resize', function() { mapChart.resize(); lineChart.resize(); barChart.resize(); }); </script> </body> </html>

前端逻辑解析

  1. 动态加载:页面加载后,首先通过/api/city_list接口获取城市列表,动态填充下拉选择框,使系统更具扩展性。
  2. 地图热力图loadMapChart函数调用/api/current_weather获取最新数据,并将其与预设的城市经纬度结合,通过ECharts的heatmap类型在地图上渲染。颜色映射 (visualMap) 根据温度值自动生成渐变效果,直观展示全省温度分布。
  3. 历史图表联动loadHistoricalChart函数根据用户选择的城市、数据类型和日期范围,调用/api/historical_weather接口。获取到按日聚合的数据后,同时用折线图展示趋势,用柱状图展示具体数值。这种多图表联动,可以从不同维度观察数据。
  4. 用户体验:设置了默认的日期范围(最近30天),并添加了窗口 resize 事件监听,确保图表能自适应浏览器窗口大小变化。

4. 系统部署、优化与踩坑实录

一个能跑起来的系统只是第一步,要让其稳定、高效地运行,还需要考虑部署和优化。这里分享几个关键点和踩过的坑。

4.1 部署到生产环境

在本地开发时,我们使用app.run(debug=True)启动的Flask开发服务器,它性能低、不安全,不能用于生产环境。

推荐部署方案:Gunicorn + Nginx

  1. Gunicorn:一个Python WSGI HTTP服务器,用于替代Flask自带的服务器,处理并发请求。

    (venv) pip install gunicorn # 在项目根目录启动,-w 4 表示启动4个worker进程 (venv) gunicorn -w 4 -b 127.0.0.1:8000 app:app

    现在,你的Flask应用运行在8000端口,并由Gunicorn管理。

  2. Nginx:作为反向代理和静态文件服务器。

    • 反向代理:将外部的80/443端口请求,转发给内网的Gunicorn(8000端口)。
    • 处理静态文件:Nginx处理CSS、JS、图片等静态文件的效率远高于Python应用。
    • 负载均衡与SSL:如果需要,可以配置多台Gunicorn实例进行负载均衡,并方便地配置HTTPS证书。

一个简单的Nginx配置片段 (/etc/nginx/sites-available/weather_system):

server { listen 80; server_name your_domain.com; # 你的域名或IP location / { proxy_pass http://127.0.0.1:8000; # 转发给Gunicorn proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 可选:直接由Nginx提供静态文件,减轻Flask负担 location /static { alias /path/to/your/project/static; expires 30d; } }

配置好后,使用sudo systemctl restart nginx重启Nginx。

4.2 性能优化与常见问题排查

  • 数据库查询优化

    • 索引是王道:对于WeatherRecord表,我们经常按city_iddate查询。应该在这两个字段上建立复合索引,可以极大提升历史数据查询接口的速度。
      # 在模型定义中添加(SQLAlchemy会自动创建) __table_args__ = ( db.UniqueConstraint('city_id', 'date', 'time', name='_city_date_time_uc'), db.Index('idx_city_date', 'city_id', 'date'), # 添加复合索引 )
    • 分页查询:如果历史数据量非常大,API一次性返回所有数据会导致传输慢、前端渲染卡顿。务必实现分页功能,例如在/api/historical_weather接口中添加pageper_page参数,并使用SQLAlchemy的.paginate()方法。
  • 爬虫稳定性

    • 异常重试:网络请求可能失败。可以为requests.get添加重试机制,可以使用tenacity库。
    • 代理IP池:如果目标网站反爬严厉,单IP频繁访问会被封。需要考虑使用代理IP池,但这会显著增加系统复杂度。对于天气数据这种更新频率不高(小时级)的爬取,通过设置足够长的请求间隔(如2-3秒一个城市)和伪装请求头,通常可以稳定运行。
    • 日志记录:不要只用print。使用Python的logging模块将爬虫的运行状态、错误信息记录到文件,方便后期排查问题。
  • 前端图表性能

    • 数据量过大:当折线图需要展示数年的每日数据时,上千个数据点会导致浏览器渲染缓慢。解决方案有两个:一是后端进行数据聚合(比如返回月平均值),二是使用ECharts的数据采样sampling)功能,在保持趋势的前提下减少渲染点数。
  • 定时任务管理

    • 使用数据库存储任务状态APScheduler默认将任务存储在内存中。如果Flask应用重启,所有的定时任务都会丢失。在生产环境中,应该配置一个作业存储器(Job Store),比如使用SQLAlchemyJobStore将任务信息持久化到数据库中,这样即使应用重启,任务也能恢复。
    • 避免任务重叠:如果爬虫任务执行时间超过1小时(比如网络很慢),下一个定时任务又会启动,可能导致数据库锁冲突或资源竞争。可以在爬虫函数开始和结束时加锁,或者使用APSchedulermax_instances参数限制同一任务的最大并发实例数。

4.3 我踩过的那些“坑”

  1. 时区问题:服务器时间和本地时间可能不同。爬虫记录的datetime.now()是服务器所在时区的时间。如果服务器在国外,数据的时间戳就乱了。解决方案:在应用开始时,统一设置时区。app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False这个配置无关,应该用import pytz并在记录时间时使用datetime.now(pytz.timezone('Asia/Shanghai'))
  2. 数据库连接泄露:在定时任务中,如果每次爬虫都创建新的数据库连接而不关闭,会导致连接数耗尽。解决方案:使用Flask的app.app_context()和正确的会话管理。在我们的代码中,爬虫函数被Flask应用上下文包裹,并且使用db.session.merge()db.session.commit(),最后由Flask-SQLAlchemy管理连接生命周期,这是正确的做法。
  3. ECharts地图不显示:中国地图需要额外引入china.js。如果引入后还是空白,检查GeoJSON注册的地图名称是否为'china',以及省份坐标数据是否正确。
  4. 前端跨域问题:如果前端页面(比如用Vue/React单独开发)和Flask API不在同一个域名或端口下,浏览器会因同源策略阻止请求。解决方案:在Flask后端使用flask_cors扩展来允许跨域请求。

构建这样一个系统,从数据抓取到可视化呈现,是一个典型的全栈数据工程实践。它串联起了网络爬虫、数据库设计、后端API开发、前端数据可视化等多个核心技能点。最重要的是,通过这个项目,你获得的不再是零散的知识点,而是一个解决实际问题的完整能力。你可以基于这个框架,轻松地将其扩展到其他省份、其他类型的数据(如空气质量、房价),甚至集成机器学习模型进行天气预测。希望这份超详细的指南和代码,能帮你少走弯路,顺利搭建起属于自己的数据洞察工具。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 3:35:33

LangChain 管道流式传输:自定义 RunnableGenerator 实践

LangChain 管道流式传输&#xff1a;自定义 RunnableGenerator 实践 在基于 LangChain Expression Language (LCEL) 构建现代生产级 AI 应用时&#xff0c;终端用户对交互体验的要求早已不是“等待 5 秒后弹出一大段文字”&#xff0c;而是“打字机式逐字吐出&#xff08;Strea…

作者头像 李华
网站建设 2026/9/4 3:35:23

基于51单片机与HX711的电子秤设计:从传感器原理到工程实践全解析

简介&#xff1a;本资源是一套完整的基于51单片机的电子秤毕业设计实现方案&#xff0c;面向电子信息、自动化、嵌入式等专业的本科生及单片机初学者&#xff0c;解决课程设计、毕设选题与硬件综合实践中的核心需求。压缩包共65个文件&#xff0c;涵盖23张实物与电路照片&#…

作者头像 李华
网站建设 2026/9/4 3:34:26

政策助手类AI怎么做?从实时知识检索到决策辅助的架构解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 3:33:45

Windows软件卸不干净?用Geek Uninstaller彻底清理残留文件和注册表

Windows 上卸载软件&#xff0c;最常见的槽点不是卸载入口难找&#xff0c;而是删完之后才发现桌面快捷方式还在、右键菜单里还留着旧项、C 盘某个目录里还躺着一堆不认识的文件。真正闹心的场景是&#xff1a;准备重装同一款软件&#xff0c;安装包却提示“已安装”&#xff1…

作者头像 李华
网站建设 2026/9/4 3:33:09

微信生态多模态Embedding实战:从CLIP微调到向量检索部署

微信做多模态 Embedding&#xff0c;这个标题一看就知道是冲着业务检索和内容理解去的。这两年多模态大模型火是火&#xff0c;但真正落到微信小程序、公众号、视频号这种生产环境里&#xff0c;最常用的其实不是让模型生成图文&#xff0c;而是让它把图片、文本、甚至视频统一…

作者头像 李华
网站建设 2026/9/4 3:33:00

技术博客写作:从工程实践到高价值内容输出

这个输入内容是一个娱乐向的粉丝品鉴话题&#xff0c;涉及特定艺人经纪公司、艺人形象和MV物料点评。这类话题不属于技术写作对象&#xff0c;无法按照技术博客的要求补全工程细节、代码、配置、排错路径和可复现教程。我不会围绕该主题生成技术长文或仿写内容。如果目标是写技…

作者头像 李华