ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/4 3:36:20 拓冰建站 浏览量
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后端模块前端采用BootstrapFont AwesomejQuery生态结构清晰、开箱即用。已有120人下载学习提供可直接运行的完整工程目录、配套静态资源与基础数据集省去环境配置与接口调试环节便于快速理解Web爬虫、数据清洗与前后端协同可视化全流程。1. 项目缘起为什么我们需要一个自己的天气数据系统作为一名长期和数据打交道的开发者我经常遇到一个看似简单却颇为棘手的需求需要某个地区比如山东省连续、稳定、多维度的天气数据来做分析。无论是为了研究气候变化趋势还是为某个智能农业、物流调度项目提供决策支持亦或是单纯想做一个酷炫的个人数据看板你都会发现直接可用的、干净的、结构化的历史与实时天气数据源要么收费昂贵要么接口不稳定要么数据维度单一。市面上有很多天气API但它们往往有调用频率限制历史数据查询要么不提供要么是付费服务。更重要的是这些数据是“别人的”你无法按照自己的业务逻辑进行深度定制化的存储、清洗和分析。比如你想分析过去五年济南市夏季高温天与降水量的相关性并在地图上用热力图展示各城市的高温分布用折线图追踪某个城市全年的湿度变化——这种复合型需求通用API很难一站式满足。于是自己动手搭建一个“山东省天气数据爬取及可视化分析系统”的想法就变得非常实际。这不仅仅是一个技术Demo它是一个能解决真实问题的生产级工具雏形。通过Python的灵活性和Flask框架的轻量级Web能力我们可以构建一个从数据采集、存储、处理到可视化展示的完整闭环。这个系统能让你真正“拥有”数据并按照你的想法去“驾驭”数据。接下来我将详细拆解如何从零开始构建这样一个系统其中会包含大量我在实际开发中踩过的坑和总结的经验。2. 系统架构设计与技术选型背后的思考在动手写代码之前清晰的架构设计能避免后期大量的返工。我们这个系统的核心目标是稳定获取数据、可靠存储数据、灵活分析数据、直观展示数据。围绕这个目标我设计了如下架构并解释了每一个技术选型背后的理由。整个系统可以划分为四个核心层数据采集层、数据存储层、数据处理层和可视化应用层。它们通过Flask这个轻量级的Web框架有机地整合在一起。2.1 数据采集层爬虫的稳健之道数据源是系统的血液。对于天气数据我们需要两类实时数据和历史数据。实时数据源目标是获取山东省各个地级市当前最新的温度、湿度、风速、风向、降水量、气压等要素。我选择从中国天气网等提供公开数据的网站进行抓取。为什么不直接用API因为免费的公共API往往不稳定或有限制而爬取公开网页虽然需要解析HTML但数据源相对稳定可控。工具选型requestsBeautifulSoup4。这是Python生态中最经典、最稳定的HTTP请求和HTML解析库组合。requests用于模拟浏览器发送请求BeautifulSoup4用于从复杂的HTML标签中精准提取我们需要的数据。关键考量必须处理反爬机制。这类气象网站通常会有简单的反爬比如检查请求头User-Agent。我们的爬虫需要伪装成正常的浏览器访问并合理设置请求间隔避免对目标服务器造成压力这也是职业道德和可持续爬取的前提。注意爬虫的伦理和法律边界必须遵守。我们只爬取公开的、非敏感的气象数据用于个人学习和分析绝不进行商业牟利或恶意攻击。在代码中务必设置合理的延时如time.sleep(2)并尊重网站的robots.txt协议。历史数据获取历史数据是分析趋势的基础。一个可靠的来源是国家气象科学数据中心等官方机构它们通常会提供历史气象数据集的下载。另一种方案是利用一些聚合了历史数据的第三方API可能有免费额度。在我们的系统中我假设我们已经通过合法渠道获得了一份结构化的山东省历史天气CSV或Excel文件作为系统的初始化数据。2.2 数据存储层SQLite与MySQL的抉择爬取到的数据需要持久化存储。这里面临一个经典选择轻量级的SQLite还是功能更全面的MySQL/PostgreSQLSQLite它是一个服务器进程、零配置的数据库整个数据库就是一个文件。对于个人项目、原型系统或中小型数据量比如单个省几年的天气数据总量在几十万到百万条记录级别来说SQLite是绝佳选择。它部署简单无需安装和配置独立的数据库服务用Python标准库sqlite3即可直接操作。MySQL/PostgreSQL更适合企业级应用支持高并发、复杂的用户权限管理、存储过程等。如果预期未来数据量极大数亿条或者需要多用户同时进行复杂的读写操作应该选择它们。我的选择与理由对于这个“山东省天气数据”系统我优先推荐使用SQLite。理由如下简化部署项目可以一键运行无需用户额外安装配置数据库环境降低了使用门槛。性能足够山东省17个地级市假设每天采集4次数据一年约17 * 4 * 365 ≈ 2.5万条记录。存储10年也才25万条对SQLite来说游刃有余。开发便捷在Flask中集成SQLite极其简单配合SQLAlchemyORM对象关系映射工具可以用Python类来定义数据表让数据库操作像操作普通Python对象一样直观。当然我们在设计数据表时会保持ORM模型的独立性。这意味着如果未来数据量激增需要迁移到MySQL只需修改Flask的数据库连接配置即可业务代码几乎不用改动。这是使用ORM带来的巨大优势。2.3 数据处理与服务层Flask与Pandas的黄金组合这是系统的“大脑”负责协调所有模块并提供数据接口。Flask框架它是一个“微”框架核心简单但扩展性强。我们用它来做什么构建Web应用承载我们的数据可视化前端页面。提供RESTful API为前端图表提供数据接口。例如前端需要济南市2023年的温度数据就向Flask服务器发送一个请求到/api/temperature/jinan/2023Flask处理后返回JSON格式的数据。调度后台任务我们可以利用APScheduler等库在Flask应用中定时执行爬虫任务实现数据的自动更新。Pandas库它是数据分析的“瑞士军刀”。当我们需要从数据库中查询出一段时间的数据进行分析时比如计算月平均温度、找出风速最大的十天用Pandas进行分组、聚合、筛选、计算比直接写复杂的SQL语句要直观和高效得多。Flask处理完API请求后可以很方便地调用Pandas进行数据加工再将结果返回给前端。2.4 可视化展示层ECharts的强大与易用前端图表库的选择至关重要它直接决定最终展示效果的美观度和交互性。我强烈推荐Apache ECharts。为什么是ECharts功能全面完美支持我们需要的地图热力图、折线图、柱状图以及散点图、饼图等几乎所有常见图表类型。地图支持好对中国的省市县地图支持非常完善可以轻松绘制山东省地图并在地图上以热力图或散点图形式展示各城市的气象数据分布。配置灵活通过JSON配置项就能实现极其丰富的视觉效果和交互功能学习曲线相对平缓。社区活跃中文文档丰富遇到问题容易找到解决方案。前端页面我们使用简单的HTML、CSS和JavaScript通过Ajax技术调用Flask提供的API获取数据后动态渲染ECharts图表。这样前后端分离结构清晰。3. 从零开始环境搭建与核心代码实现理论讲完我们进入实战环节。我会手把手带你搭建环境并讲解核心模块的代码。请确保你的电脑上已经安装了Python3.7及以上版本。3.1 项目初始化与依赖安装首先创建一个项目文件夹例如sd-weather-system。在文件夹内我们建议使用虚拟环境来管理依赖避免污染全局Python环境。# 在项目根目录下 python -m venv venv # 创建虚拟环境 # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 激活后命令行提示符前会出现 (venv) 标识接下来创建一个requirements.txt文件列出所有需要的库Flask2.3.3 requests2.31.0 beautifulsoup44.12.2 pandas2.0.3 sqlalchemy2.0.19 apscheduler3.10.4然后使用pip安装(venv) pip install -r requirements.txt3.2 数据库模型设计在项目根目录下创建app.py作为主程序入口同时我们创建一个models.py来定义数据模型。models.pyfrom flask_sqlalchemy import SQLAlchemy from datetime import datetime # 先创建db对象在app.py中初始化 db SQLAlchemy() class City(db.Model): 城市表 id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(20), uniqueTrue, nullableFalse) # 城市名如‘济南’ code db.Column(db.String(10), uniqueTrue) # 城市代码用于爬虫定位 # 定义关系一个城市有多条天气记录 weather_records db.relationship(WeatherRecord, backrefcity, lazyTrue) class WeatherRecord(db.Model): 天气记录表存储实时和历史数据 id db.Column(db.Integer, primary_keyTrue) city_id db.Column(db.Integer, db.ForeignKey(city.id), nullableFalse) date db.Column(db.Date, nullableFalse) # 日期 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 }设计思路将城市和天气记录分开成两张表这是典型的数据库范式化设计避免了数据冗余。如果每个天气记录都重复存储城市名既浪费空间更新城市信息时也会很麻烦。WeatherRecord表包含了我们关心的所有气象要素字段。date和time字段分开存储便于按天或按小时进行聚合查询。UniqueConstraint唯一约束是关键。它能防止调度任务意外重复运行时向数据库插入重复的实时数据确保数据的唯一性。to_dict方法是一个实用技巧它让我们在将SQLAlchemy对象通过Flask的jsonify返回给前端时能轻松地将其转换为字典格式。3.3 实时数据爬虫实现创建一个spider.py文件。这里以模拟抓取为例实际爬取需要你根据目标网站的HTML结构调整解析逻辑。spider.pyimport 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 fhttp://www.weather.com.cn/weather/{code}.shtml print(f正在抓取 {city_name} 的天气...) resp requests.get(url, headersheaders, timeout10) 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(namecity_name).first() if not city: city City(namecity_name, codecode) db.session.add(city) db.session.flush() # 获取city的id # 创建天气记录 now datetime.now() record WeatherRecord( city_idcity.id, datenow.date(), timenow.time(), temperaturetemperature, humidityhumidity, wind_speedwind_speed, wind_directionwind_direction, precipitationprecipitation, pressurepressure ) # 使用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()关键点解析错误处理网络爬虫必须健壮。我们用try...except包裹了每个城市的抓取过程即使某个城市抓取失败也不会影响其他城市并且会打印出错误信息便于排查。请求头设置User-Agent是绕过基础反爬的最基本措施。延时time.sleep(1)是必须的体现了对目标网站的尊重也是保证爬虫能长期稳定运行的基础。数据库操作使用db.session.merge(record)是一个高级技巧。它基于模型定义的主键或唯一约束我们定义的UniqueConstraint来判断是插入新记录还是更新已有记录。这完美解决了定时任务可能导致的重复数据问题。会话管理所有的数据库增删改操作最后需要通过db.session.commit()一次性提交如果中间出错则db.session.rollback()回滚保证数据一致性。3.4 Flask应用整合与API设计现在我们在app.py中整合所有部件并创建Web服务和数据API。app.pyfrom 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(funcfetch_real_time_weather, triggerinterval, hours1, idfetch_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(days30)).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(namecity_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(debugTrue, host0.0.0.0, port5000)代码详解Flask配置非常简单主要就是配置数据库连接。SQLALCHEMY_TRACK_MODIFICATIONS设置为False是为了避免不必要的内存开销。定时任务使用APScheduler在后台启动一个调度器每隔1小时自动执行一次fetch_real_time_weather函数。这样我们的系统就具备了自动更新数据的能力。BackgroundScheduler确保任务在独立的线程中运行不会阻塞Flask的主Web线程。API设计/api/current_weather这个接口的查询有点技巧。为了获取每个城市最新的一条数据我们使用了SQLAlchemy的子查询subquery和分组聚合max函数。这是数据库查询中一个经典的“分组取最新”问题。返回的数据可以直接用于首页展示当前天气概况或地图热力图。/api/historical_weather这是为折线图/柱状图提供数据的核心接口。它接收城市名、起止日期和要查询的数据类型如温度、湿度作为参数。这里展示了Pandas的强大之处我们从数据库拿到原始数据后用Pandas的groupby和mean方法轻松地计算出了每天的该气象要素的平均值这对于趋势分析非常有用。/api/city_list一个简单的辅助接口用于前端动态生成城市选择下拉框。参数处理使用request.args.get()安全地获取URL查询参数并提供了默认值增强了API的健壮性。3.5 前端可视化页面实现前端我们创建一个简单的index.html放在项目根目录下的templates文件夹中。这里使用ECharts来绘制地图和图表。templates/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title山东省天气数据可视化分析系统/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script script srchttps://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 classcontrols h3数据筛选/h3 label选择城市/label select idcitySelect option value济南济南/option !-- 其他城市将通过JS动态加载 -- /select label选择数据类型/label select iddataTypeSelect option valuetemperature温度 (°C)/option option valuehumidity湿度 (%)/option option valuewind_speed风速 (m/s)/option option valueprecipitation降水量 (mm)/option /select label开始日期/label input typedate idstartDate label结束日期/label input typedate idendDate button onclickloadHistoricalChart()加载历史趋势图/button button onclickloadMapChart()刷新实时地图/button /div div classcontainer div idmapChart classchart-box/div div idlineChart classchart-box/div div idbarChart classchart-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前端逻辑解析动态加载页面加载后首先通过/api/city_list接口获取城市列表动态填充下拉选择框使系统更具扩展性。地图热力图loadMapChart函数调用/api/current_weather获取最新数据并将其与预设的城市经纬度结合通过ECharts的heatmap类型在地图上渲染。颜色映射 (visualMap) 根据温度值自动生成渐变效果直观展示全省温度分布。历史图表联动loadHistoricalChart函数根据用户选择的城市、数据类型和日期范围调用/api/historical_weather接口。获取到按日聚合的数据后同时用折线图展示趋势用柱状图展示具体数值。这种多图表联动可以从不同维度观察数据。用户体验设置了默认的日期范围最近30天并添加了窗口 resize 事件监听确保图表能自适应浏览器窗口大小变化。4. 系统部署、优化与踩坑实录一个能跑起来的系统只是第一步要让其稳定、高效地运行还需要考虑部署和优化。这里分享几个关键点和踩过的坑。4.1 部署到生产环境在本地开发时我们使用app.run(debugTrue)启动的Flask开发服务器它性能低、不安全不能用于生产环境。推荐部署方案Gunicorn NginxGunicorn一个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管理。Nginx作为反向代理和静态文件服务器。反向代理将外部的80/443端口请求转发给内网的Gunicorn8000端口。处理静态文件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_id和date查询。应该在这两个字段上建立复合索引可以极大提升历史数据查询接口的速度。# 在模型定义中添加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接口中添加page和per_page参数并使用SQLAlchemy的.paginate()方法。爬虫稳定性异常重试网络请求可能失败。可以为requests.get添加重试机制可以使用tenacity库。代理IP池如果目标网站反爬严厉单IP频繁访问会被封。需要考虑使用代理IP池但这会显著增加系统复杂度。对于天气数据这种更新频率不高小时级的爬取通过设置足够长的请求间隔如2-3秒一个城市和伪装请求头通常可以稳定运行。日志记录不要只用print。使用Python的logging模块将爬虫的运行状态、错误信息记录到文件方便后期排查问题。前端图表性能数据量过大当折线图需要展示数年的每日数据时上千个数据点会导致浏览器渲染缓慢。解决方案有两个一是后端进行数据聚合比如返回月平均值二是使用ECharts的数据采样sampling功能在保持趋势的前提下减少渲染点数。定时任务管理使用数据库存储任务状态APScheduler默认将任务存储在内存中。如果Flask应用重启所有的定时任务都会丢失。在生产环境中应该配置一个作业存储器Job Store比如使用SQLAlchemyJobStore将任务信息持久化到数据库中这样即使应用重启任务也能恢复。避免任务重叠如果爬虫任务执行时间超过1小时比如网络很慢下一个定时任务又会启动可能导致数据库锁冲突或资源竞争。可以在爬虫函数开始和结束时加锁或者使用APScheduler的max_instances参数限制同一任务的最大并发实例数。4.3 我踩过的那些“坑”时区问题服务器时间和本地时间可能不同。爬虫记录的datetime.now()是服务器所在时区的时间。如果服务器在国外数据的时间戳就乱了。解决方案在应用开始时统一设置时区。app.config[JSONIFY_PRETTYPRINT_REGULAR] False这个配置无关应该用import pytz并在记录时间时使用datetime.now(pytz.timezone(Asia/Shanghai))。数据库连接泄露在定时任务中如果每次爬虫都创建新的数据库连接而不关闭会导致连接数耗尽。解决方案使用Flask的app.app_context()和正确的会话管理。在我们的代码中爬虫函数被Flask应用上下文包裹并且使用db.session.merge()和db.session.commit()最后由Flask-SQLAlchemy管理连接生命周期这是正确的做法。ECharts地图不显示中国地图需要额外引入china.js。如果引入后还是空白检查GeoJSON注册的地图名称是否为china以及省份坐标数据是否正确。前端跨域问题如果前端页面比如用Vue/React单独开发和Flask API不在同一个域名或端口下浏览器会因同源策略阻止请求。解决方案在Flask后端使用flask_cors扩展来允许跨域请求。构建这样一个系统从数据抓取到可视化呈现是一个典型的全栈数据工程实践。它串联起了网络爬虫、数据库设计、后端API开发、前端数据可视化等多个核心技能点。最重要的是通过这个项目你获得的不再是零散的知识点而是一个解决实际问题的完整能力。你可以基于这个框架轻松地将其扩展到其他省份、其他类型的数据如空气质量、房价甚至集成机器学习模型进行天气预测。希望这份超详细的指南和代码能帮你少走弯路顺利搭建起属于自己的数据洞察工具。本文还有配套的精品资源点击获取