资讯详情

天气预报数据可视化分析系统:Python+Flask+ECharts实战

📅 2026/9/17 6:14:05 | 华诺云谱 👁 阅读
天气预报数据可视化分析系统:Python+Flask+ECharts实战
简介这是一份基于Python Flask框架的天气预报数据可视化分析系统毕业设计资源包面向计算机相关专业学生、初级开发者及气象数据处理入门者。系统通过和风天气接口自动获取全国实时与历史天气数据经清洗后存入MySQL并利用ECharts实现全国综合天气、各城市天气及历史趋势的可视化展示同时包含用户登录注册、数据修改等管理模块适合课程设计、毕业设计或项目实训参考。 压缩包共206个文件约33MB以Python源码与编译文件、Flask模板、前端js/css、HTML页面及SQL脚本为主py/pyc负责数据处理与后端逻辑SQL脚本导入后即可建库js/css/HTML及pug/scss/less模板支撑前端展示docx与txt提供说明文档。整体结构清晰便于按模块拆解学习。目前已有735人学习下载适合需要快速理解系统架构、复用数据采集与可视化代码的读者。1. 天气预报数据可视化分析系统到底在解决什么问题天气预报数据可视化分析是 Python 毕设的高频题目表面只有拿数据、画图表。拉开差距的是数据层是否干净、接口层是否规范、可视化层有没有分析结论。Flask 做后端、HTML 做页面只是骨架能讲清温度趋势和降雨规律才有说服力。反直觉的是最花时间的不是写页面而是数据清洗和接口参数设计。原始接口字段乱、单位不统一、日期有空缺直接丢给前端画图一问口径就露馅。可复现的做法是采集、清洗、接口、渲染四层拆开每层只做一件事。这套方案适合 Python 入门者和准备答辩的毕业生。下文按常见路线展开requests 采集pandas 清洗Flask 出 JSON 接口HTML 配 ECharts 展示最后补部署验证与性能技巧代码都能直接落地。2. 天气数据采集与清洗从公开 API 到干净的 DataFrame2.1 数据源选型为什么优先选公开 API毕设里最常见的做法是调公开天气接口而不是去爬网页。天气网站的 HTML 结构改版频繁今天能解析的选择器下周就可能失效而公开 API 返回的是 JSON字段结构固定省去了解析 DOM 的体力活。像和风天气、OpenWeatherMap、高德天气这类服务都提供历史天气和逐天预报接口注册后拿一个 key 就能请求免费额度对课程设计完全够用。选型时主要看三点返回字段是否覆盖要分析的项目温度、降水、风速、空气质量历史数据能拉多长单位是否统一。下面这张表是常用的对比口径数据源返回格式覆盖字段主要限制公开天气 APIJSON温度/降水/风/空气质量有 QPS 限制需注册 key网页爬虫HTML随页面结构变化无 key 成本但易失效历史气象数据集CSV/JSON字段最全数据量大需本地存储提示key 是账号凭证别写死在页面里。毕设一般只在后端用路由里做一层参数白名单前端页面不要暴露。我现在对这类毕设的建议是主数据源用公开 APIrequests 直接请求如果题目要求体现爬虫能力可以把爬网页作为补充数据源但别把主流程押在爬虫上。答辩时讲清为什么这样选比堆技术点更有说服力。2.2 requests 采集最小代码与重试参数下面是采集模块最常见的最小实现URL 用伪地址占位实际替换成申请到的接口。这段代码的逻辑是构造参数、发起请求、校验状态码、返回 JSON所有网络异常都留给上层处理import requests def fetch_weather(city: str, days: int 30) - dict: url https://api.example.com/weather/history params { city: city, days: days, key: YOUR_API_KEY, unit: metric, # 摄氏别用 imperial } resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json()代码不复杂但三个参数值得较真。timeout10 表示连接和读取总共不超过 10 秒不设这个参数requests 可能长时间挂起Flask 进程会被占住raise_for_status() 在状态码非 2xx 时直接抛异常让失败快速暴露而不是拿着错误页面继续解析unitmetric 把温度统一成摄氏省去后面清洗时再做换算。网络请求偶尔会失败免费接口在高频请求时尤其明显。常见做法是加重试指数退避比固定间隔更温和既能给限流窗口留出恢复时间也不会在服务刚挂起时继续施压import time def fetch_with_retry(city: str, tries: int 3): for attempt in range(tries): try: return fetch_weather(city) except requests.RequestException: if attempt tries - 1: raise time.sleep(2 * (attempt 1)) # 2s、4s 递增time.sleep(2 * (attempt 1)) 在第一次失败后等 2 秒第二次等 4 秒。最长等待 6 秒加上 timeout 10 秒单城市最坏情况十几秒不会拖垮整体采集。2.3 pandas 清洗字段映射、缺失值与日期对齐接口返回的字段通常不适合直接画图。常见问题有三类字段名和展示名不一致tmax 实际上是最高温单位不统一有的字段已经是摄氏、有的还是华氏日期列有空缺接口抽风会少记录。pandas 处理这三类问题的代码是固定的import pandas as pd df pd.DataFrame(data.get(records, [])) df[date] pd.to_datetime(df[obs_time], errorscoerce) df[temp_max] pd.to_numeric(df[tmax], errorscoerce) df[temp_min] pd.to_numeric(df[tmin], errorscoerce) df[precip] pd.to_numeric(df[precipitation], errorscoerce) df df.dropna(subset[temp_max, temp_min]) df df.sort_values(date).drop_duplicates(subsetdate, keeplast) df df.reset_index(dropTrue)to_datetime 和 to_numeric 的 errorscoerce 是清洗的关键遇到暂无数据这类字符串或空值程序不中断而是变成 NaN 留待后续处理。sort_values 之后接 drop_duplicates是应对接口重复推送同一天可能推了两条记录keeplast 保留后一条。reset_index 让行号连续避免后面窗口计算时索引断层。清洗完之后建议补一步单位校验。接口文档写的是摄氏但实际返回偶尔会变成华氏这种变化往往到画图阶段才暴露。简单做法是用业务知识设一个阈值50 摄氏度以上的值统一按华氏换算if df[temp_max].max() 50: df[temp_max] df[temp_max] * 9 / 5 32 # 华氏转摄氏这个判断看起来很糙但在答辩里很实用一旦单位变化至少能在图表上发现异常而不是拿着一百摄氏度的正常曲线往下讲。2.4 清洗结果落盘CSV 还是 SQLite清洗后的数据如果只在内存里服务每次重启都要重跑一遍既浪费时间又可能触发限流。常见做法是落盘数据量在几年以内选 CSV 就够但要支持按城市查询和增量更新SQLite 更合适import sqlite3 conn sqlite3.connect(weather.db) df.to_sql(weather_daily, conn, if_existsappend, indexFalse) conn.close()to_sql 的 if_existsappend 表示追加不覆盖配合清洗时的去重实现每天增量入库。采集任务每天跑一次接口层直接读 SQLite数据链路变成采集、清洗、存储、查询四段每段都能独立验证这是这个题目最简单的可维护形态。3. Flask 后端把分析结果封装成可消费的 JSON 接口3.1 最小 Flask 应用与中文 JSON 配置后端这层只做一件事把清洗好的 DataFrame 转成前端能直接消费的 JSON。用 Flask 写接口层常见结构是一个 app.py 加一个数据模块app.py 里只放路由数据读取和聚合放到独立函数里接口层薄、逻辑层厚答辩时好讲分工。from flask import Flask, jsonify, request app Flask(__name__) app.json.ensure_ascii False # 中文以明文返回而不是 \uXXXX app.route(/health) def health(): return jsonify({status: ok})注意 Flask 2.3 之后jsonify 的中文转义开关从 JSON_AS_ASCII 配置项改成了 app.json.ensure_ascii False。网上大量老教程还停在 JSON_AS_ASCII False在新版本里会报 DeprecationWarning 甚至完全不生效。刚装的环境大概率是 Flask 3.x以 ensure_ascii 为准。提示代码里不要出现真实的 api key用 os.environ.get(WEATHER_KEY) 读取答辩演示时也避免密钥泄露。3.2 动态路由 city 与 days 的参数校验接口设计成 /api/weather/ 的语义一个城市一个资源days 作为查询参数控制时间范围。前端调用清楚后续加城市也不用改路由结构VALID_CITIES {北京, 上海, 广州, 深圳} app.route(/api/weather/city) def weather(city): if city not in VALID_CITIES: return jsonify({error: unsupported city}), 404 days request.args.get(days, default30, typeint) days min(max(days, 1), 90) # 限制在 1~90 天 df load_weather_data(city) recent df.tail(days) return jsonify({ city: city, days: int(len(recent)), dates: recent[date].dt.strftime(%Y-%m-%d).tolist(), temp_max: recent[temp_max].tolist(), temp_min: recent[temp_min].tolist(), precip: recent[precip].tolist(), summary: { avg_high: round(float(recent[temp_max].mean()), 1), avg_low: round(float(recent[temp_min].mean()), 1), rain_days: int((recent[precip] 0).sum()), } })参数说明request.args.get 的 typeint 值得养成习惯前端传 daysabc 时不会抛 500而是回落默认值 30。min(max(days, 1), 90) 把范围夹在 1 到 90 天防止一键拉走全量数据。JSON 里不要直接放 numpy 类型int64/float64 不是 Python 原生类型jsonify 序列化会失败用 int()/float()/tolist() 显式转换。3.3 日期序列化与月度聚合的两种写法DataFrame 的 datetime 列直接 tolist() 得到的是 Timestamp 对象JSON 序列化必报错所以上面用 dt.strftime 先转成 2025-01-01 字符串。如果前端要做时间轴缩放可以返回毫秒级时间戳int(ts.timestamp() * 1000)ECharts 的 xAxis 能直接消费。聚合分析放后端还有一个好处前端拿到的数据量小。比如月度平均气温不必把 90 天原始值全丢给前端算后端先聚合再返回monthly df.set_index(date).resample(MS).agg( avg_high(temp_max, mean), rain_total(precip, sum), ).round(1).reset_index()resample(MS) 按自然月分组MS 是 month start 的缩写直接用 M 在较新 pandas 里会报警告。agg 里 (列名, 聚合函数) 的元组写法比旧版 agg({列: mean}) 语义更明确多列聚合时代码也短。round(1) 在聚合后立刻做避免浮点误差一路带到 JSON。monthly 里仍含 Timestamp 的日期列返回前同样要 strftime。3.4 lru_cache 缓存与多 worker 下的边界数据清洗若每请求都跑一遍并发一高就是反复 IO 和类型转换。常见做法是给加载函数加 LRU 缓存from functools import lru_cache lru_cache(maxsize32) def load_weather_data(city: str) - pd.DataFrame: return read_and_clean(city) # 内部只读 SQLite app.route(/api/weather/city) def weather(city): df load_weather_data(city) # 命中缓存时直接返回 DataFrame return jsonify(...)lru_cache 的 maxsize 表示最多缓存多少个参数组合的结果按城市缓存时 32 个坑位足够。边界在于 DataFrame 是可变对象缓存命中后上游若原地修改会对后续请求造成脏数据稳妥做法是路由里只读或者拿 df.copy() 后再操作。多 worker 部署时每个进程各有一份缓存属于可接受现象不用为此上 Redis。缓存方案失效机制适用场景lru_cache进程内存无过期时间数据不频繁更新的单机部署flask-caching SimpleCache按 key 设置 TTL需要定时更新的场景Redis跨进程共享、可持久化多 worker 且数据需即时更新4. HTML 与 ECharts温度曲线和降雨图的页面实现4.1 页面骨架与静态资源引入方式前端用最朴素的结构一个 templates/index.html按 Flask 约定放在项目 templates 目录下静态资源放 static。ECharts 从 CDN 引入最省事但答辩现场可能断网把 echarts.min.js 下载到 static/js 下更保险!DOCTYPE html html langzh-cn head meta charsetutf-8 title天气预报数据可视化分析/title script src{{ url_for(static, filenamejs/echarts.min.js) }}/script /head body select idcitySelect/select div idtempChart stylewidth:100%;height:420px;/div div idrainChart stylewidth:100%;height:320px;/div script src{{ url_for(static, filenamejs/main.js) }}/script /body /html用 url_for 而不是写死 /static 路径防止 Flask 应用挂在子路径下时资源全部 404。div 的高度要在 ECharts 初始化之前固定否则初始化时拿到 0 高度图表画出来是空白。主路由返回模板并传城市列表from flask import render_template app.route(/) def index(): return render_template(index.html, cities[北京, 上海, 广州, 深圳])cities 经 Jinja2 传给页面下拉框的选项在后端生成城市列表变动只改一处。4.2 fetch 调用接口并拆分渲染函数main.js 核心是数据加载函数。不需要引入 jQuery原生 fetch 够用答辩讲原生 API比讲为了交互引了 90KB 库清爽const citySelect document.getElementById(citySelect); async function loadWeather(city, days 30) { const resp await fetch(/api/weather/${city}?days${days}); if (!resp.ok) throw new Error(HTTP ${resp.status}); const data await resp.json(); renderTemp(data); renderRain(data); } citySelect.addEventListener(change, (e) { loadWeather(e.target.value).catch((err) { document.getElementById(errorBox).textContent err.message; }); }); function buildCityOptions(cities) { citySelect.innerHTML cities .map((c) option value${c}${c}/option) .join(); }resp.ok 的检查不能省接口返回 404 或 500 时resp.json() 仍会尝试解析错误页抛出的异常信息不可读。渲染函数和加载逻辑分离后续加图表只需要新增 render 函数不动 fetch 部分。cities 列表由模板变量注入const cities {{ cities | tojson }}; buildCityOptions(cities); loadWeather(cities[0]);tojson 过滤器把 Python 列表转成合法 JSON 数组比手工拼字符串安全自动处理引号转义。4.3 ECharts 折线图与柱状图的参数配置温度图用两个 series 画最高温和最低温两条曲线配置集中在 setOption 里function renderTemp(data) { const chart echarts.init(document.getElementById(tempChart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [最高温, 最低温] }, grid: { left: 50, right: 20, top: 40, bottom: 40 }, xAxis: { type: category, data: data.dates, boundaryGap: false }, yAxis: { type: value, name: ℃ }, dataZoom: [{ type: inside }, { type: slider }], series: [ { name: 最高温, type: line, data: data.temp_max, smooth: true, areaStyle: { opacity: 0.15 } }, { name: 最低温, type: line, data: data.temp_min, smooth: true } ] }); }几个参数值得展开boundaryGap: false 让折线从坐标轴边缘开始画日期型数据默认留白时间序列图几乎都要关smooth: true 是对折线做插值平滑不是改数据答辩有人问为什么和 Excel 不一样答案就是 smoothdataZoom 的 inside 支持滚轮缩放slider 提供底部拖动条90 天数据不加缩放工具看不清细节areaStyle 加半透明面积视觉上强调高温区间。ECharts 参数作用适用场景boundaryGap: false折线从坐标轴边缘绘制日期型连续数据smooth: true折线平滑插值温度趋势展示dataZoom inside滚轮缩放长周期细节查看dataZoom slider底部拖动条答辩交互演示showLoading({text})空数据提示接口慢或数据缺失降雨量图换柱状图配置结构相同把 series 的 type 改成 bar颜色单独指定function renderRain(data) { const chart echarts.init(document.getElementById(rainChart)); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: data.dates }, yAxis: { type: value, name: mm }, series: [{ name: 降水量, type: bar, data: data.precip, itemStyle: { color: #4a90d9 } }] }); }4.4 图表自适应和空数据兜底图表渲染完还有两个边界情况。一是窗口变化echarts.init 出来的实例不会自动跟随容器需要监听 resizewindow.addEventListener(resize, () { echarts.getInstanceByDom(document.getElementById(tempChart))?.resize(); });二是后端返回空数组时ECharts 会画一张空坐标轴图用户以为页面坏了。渲染函数开头要判断if (!data.dates || data.dates.length 0) { chart.showLoading({ text: 暂无数据 }); return; }showLoading 的 text 参数按需设置默认转圈动画会让人误以为还在请求。这两个兜底加起来不到十行页面观感差别很大属于做了不扣分、不做必被问的细节。5. 部署验证与压测技巧用 p95 定位慢接口5.1 用 gunicorn 替换开发服务器flask run 自带的是开发服务器单进程答辩演示开两个标签可能就卡住。常见做法是用 gunicorn 起多 workerpip install gunicorn gunicorn -w 2 -b 0.0.0.0:8000 app:app-w 2 表示两个进程-b 绑定端口。Windows 原生不支持 gunicorn毕设环境是 Windows 的话改用 waitresswaitress-serve --port8000 app:app效果一样。5.2 并发压测脚本与 p95 口径写一个并发脚本验证接口真实表现用并发请求而不是浏览器手动刷新import time import urllib.request from concurrent.futures import ThreadPoolExecutor URL http://127.0.0.1:8000/api/weather/北京?days30 def hit(_): start time.perf_counter() with urllib.request.urlopen(URL, timeout5) as r: r.read() return (time.perf_counter() - start) * 1000 with ThreadPoolExecutor(max_workers20) as pool: costs list(pool.map(hit, range(100))) costs.sort() print(favg{sum(costs)/len(costs):.1f}ms p95{costs[95]:.1f}ms max{costs[-1]:.1f}ms)20 个并发线程打 100 次请求输出平均耗时、p95 和最大耗时。p95 比平均值更能反映真实体验平均值会被少数慢请求拖低p95 超过 500ms 就说明接口层有阻塞最可能的两处是缓存未命中后重复读 SQLite或者实时聚合大 DataFrame。5.3 预聚合内存表把分析前移到加载时实测里最常见的慢点不是路由本身而是接口里实时 resample 聚合。优化办法是预聚合服务启动时把日、月两张表一次算好接口直接查内存字典_PRECOMPUTED {} def precompute_all(): for city in VALID_CITIES: df load_weather_data(city) _PRECOMPUTED[city] { daily: df, monthly: df.set_index(date).resample(MS).agg( avg_high(temp_max, mean), rain_total(precip, sum), ).round(1).reset_index(), } precompute_all() # 模块加载时执行一次接口里改成查 _PRECOMPUTED 字典路由函数只剩取值和序列化响应时间从几十毫秒降到个位数毫秒。代价是数据更新后要手动重建缓存对课程设计的数据量完全值得。优化的验收标准很简单重跑压测脚本p95 从 200ms 量级降至 20ms 以内再把两个数字写进报告。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。