基于Flask的城市天气可视化分析系统开发实践与避坑指南

发布时间:2026/10/3 10:01:12
基于Flask的城市天气可视化分析系统开发实践与避坑指南 做了不少Web项目但接到把城市天气做成可视化分析页面这种需求时我还是低估了它的坑。领导嘴上说的是搞个好看点的天气页面实际上要的是能看趋势、能对比、能一眼说明白这周天气到底适不适合安排户外活动。最后我交的是一个基于Flask的城市天气可视化分析系统从数据采集、清洗、缓存、接口到前端绘图、上线部署全链路都自己搭了一遍。这中间踩过的坑我觉得比项目本身更有分享价值所以把它们完整写出来给想做同类Flask小项目的朋友当参考。这个项目的核心工作量其实不在天气本身而在分析和可视化两个词上。你真正要解决的是怎么让原始天气数据变成页面上的结论。我会按照我实际开发的顺序来讲技术选型、数据接入、存储缓存、后端接口、前端可视化、部署上线每一步都会给代码、给理由、给避坑经验。1. 为什么是Flask一个天气可视化项目选型时的取舍1.1 先弄清楚需求的核心到底是什么项目刚接到手我第一件事不是打开编辑器而是把需求拆了一遍。表面看是一个天气页面但拆开后至少包含四个环节天气数据的采集从第三方接口拿数据涉及网络请求和字段解析。数据存储不能每次打开页面都实时请求上游API必须落地成本地数据。后端接口前端图表需要结构化数据后端要负责聚合和格式化。可视化展示温度趋势、湿度变化、空气质量等级都要图表化。可视化分析是重点所以我一开始就决定凡是能在后端算好的指标绝不让前端自己算凡是能缓存的请求绝不多打一次上游API。这个思路直接影响了我后面的所有技术决定。当时也想过是不是直接用Excel拉数据、做个静态页面就完了不行因为需求里明确提了要能看某一天的具体天气还要支持切换城市、自动更新所以必须有一个带后端的小系统Flask这类轻量框架就是最合适的载体。1.2 Flask、Django、FastAPI之间的真实对比我在Flask、Django、FastAPI三个框架里纠结了一阵最后选了Flask。不是因为它最好而是它在这个场景下最合适。我当时的对比逻辑是这样的框架上手成本项目起步速度适合场景我的顾虑Flask低快中小型工具页面、轻量API需要自己组装很多模块Django中高慢大型系统、后台管理复杂自带ORM和Admin但对小项目过重FastAPI中快纯API服务、高并发生态相对年轻模板渲染不是强项为什么最终选Flask三个理由第一这个项目需要一个HTML页面Flask的Jinja2模板可以直接渲染首屏还能顺便把城市名称、页面标题传进去不用单独做前后端分离。第二Flask的路由写起来极简一个天气项目撑死十个接口用Flask几十行代码就能把服务端逻辑写完。Django在这个规模下反而要迁就它的项目结构虽然规范但前期成本高。第三Flask的部署资料最丰富。gunicorn加nginx的经典组合网上案例很多遇到问题好排查。做内部工具类项目我只关心能不能快速上线、稳定运行而不是框架是否现代化。当然如果这个项目未来要支撑几十万请求我会考虑换FastAPI但一个城市天气分析系统远没到那个量级。技术选型最怕的不是选错而是用超出问题规模的方案去解决小问题。1.3 项目目录结构与依赖清单我把项目结构设计成下面这样原则是每个文件只干一件事weather_vis/ ├── app.py # Flask应用入口与路由 ├── config.py # 配置城市、API Key、缓存时间 ├── db.py # SQLite连接与建表逻辑 ├── weather_service.py # 上游天气API请求与数据清洗 ├── tasks.py # APScheduler定时任务 ├── templates/ │ └── index.html # 可视化页面 ├── static/ │ ├── css/ │ │ └── style.css # 页面样式 │ └── js/ │ └── dashboard.js # ECharts图表逻辑 └── requirements.txtrequirements.txt里只有四样东西flask requests apscheduler gunicorn你没看错连pandas都没用。天气数据量不大SQLite的SQL语句加上Python自带的数据结构就够处理了引入pandas反而会让部署体积变大、启动变慢。很多初学者容易犯的毛病就是不管需不需要先把大库装上我的建议是等代码里出现明显的循环计算痛点再引入重型库不然都是负担。2. 天气数据接入接口选型、字段清洗与单位陷阱2.1 数据源选择稳定比免费更重要天气数据源直接决定了整个项目的可靠性。我调研的时候主要看了和风天气、OpenWeatherMap、高德开放平台这三家简单对比一下数据源免费额度单位与字段国内访问情况备注和风天气个人开发版每天1000次公制字段中文友好稳定需要城市IDOpenWeatherMap免费版每分钟60次默认开尔文需转摄氏偶尔超时全球城市用城市名即可高德开放平台有每日限额字段较少稳定更适合地理编码天气只是附带我最终以和风天气作为主数据源原因很简单国内访问稳定免费额度对一个内部项目完全够用而且它返回的天气现象、风向、风力等级都是中文省去了大量翻译映射工作。同时我留了一手写代码时把获取实时天气和获取七日报两个函数独立封装将来哪怕换数据源只需要改weather_service.py里面两个函数页面和数据库完全不用动。这种数据源可替换的设计意识做任何对接外部API的项目都该有。如果只是练手或者担心注册API麻烦可以在weather_service.py里预留一个mock开关本地测试时直接返回构造好的JSON字典等联调时再切到真实接口。我实际开发中就先用mock数据把前端图表跑通了最后接真实API只花了十分钟效率很高。2.2 从城市到数据请求封装流程和风天气的接口风格是按城市ID请求的比如北京是101010100。使用之前先要把城市名和城市ID做个映射表放在config.py里CITY_MAP { 北京: 101010100, 上海: 101020100, 广州: 101280101, 深圳: 101280601, }实时天气请求封装成这样一个函数# weather_service.py import requests import config def fetch_now_weather(city_code: str) - dict: resp requests.get( https://devapi.qweather.com/v7/weather/now, params{location: city_code, key: config.WEATHER_API_KEY}, timeout5 ) payload resp.json() if payload.get(code) ! 200: raise RuntimeError(fWeather API error: {payload.get(code)}) return payload[now]这里有两个细节值得注意一是必须设置timeout。requests默认没有超时时间如果上游接口hang住你的Flask请求也会一直挂着。我后面部署时踩过一次线程阻塞的坑根因就是这里没设timeout后面第6章会细讲。二是不要把API Key写死在代码里。我放在config.py里通过环境变量读取然后靠.gitignore忽略配置文件避免密钥泄露。这个习惯要养成因为公开代码时最尴尬的就是把Key发到网上。七日报接口的逻辑一模一样只是URL换成/v7/weather/7d。我在里面还加了一个可选参数langzh确保返回中文天气描述。2.3 字段清洗单位、时区与状态码拿到原始JSON只是第一步真正坑人的是字段含义和单位。和风天气返回的实时天气里temp是温度摄氏度、feelsLike是体感温度、humidity是湿度百分比、windDir是风向、windScale是风力等级、pressure是气压百帕、precip是降水量毫米。看着都是常见单位但如果你换成OpenWeatherMap它默认返回开尔文温度和百帕气压不转换直接存库图表上就会出现几百度的离谱数据。我写了一个清洗函数把所有字段统一成前端需要的结构def normalize_now(raw: dict, city: str) - dict: return { city: city, temp: float(raw[temp]), feels_like: float(raw[feelsLike]), humidity: int(raw[humidity]), wind_dir: raw[windDir], wind_scale: int(raw[windScale]), pressure: int(raw[pressure]), precip: float(raw.get(precip, 0)), obs_time: raw.get(obsTime, ) }时间字段也要注意。和风天气返回的obsTime是本地时间但OpenWeatherMap返回的是Unix时间戳UTC如果你直接显示在时刻轴上会比北京时间差8小时。我的处理方式很简单清洗函数里统一转成YYYY-MM-DD HH:mm格式并标注时区来源前端拿到的是一个已经可读的字符串不需要再做时区运算。还有一个状态码陷阱和风天气的业务状态码是字符串200不是整数200。我第一次判断payload[code] ! 200结果永远走异常分支排查了半天才发现是类型问题。对接任何API都要先看清楚文档里的类型约定这种低级错误最浪费时间。3. 缓存与存储让上游请求次数从几百次降到个位数3.1 核心思路数据库当缓存定时任务当刷新器一开始我图省事直接在前端页面里每次加载都请求天气API结果测试时发现两个问题第一页面刷新两次免费额度肉眼可见地消耗第二上游API响应慢的时候页面会白屏等好几秒。这个场景让我意识到必须把实时数据和页面展示数据彻底分开。我的思路是SQLite数据库是唯一的真相来源页面只读数据库定时任务定期去上游拉数据更新数据库另外在内存里加一层短TTL缓存避免同一个用户在5秒内多次刷新时反复查库。这样设计之后一个城市哪怕被用户狂刷一天真正打到上游API的请求也只有定时任务那一两次其余全部由本地库和内存缓存兜住。这才是可视化分析系统该有的架构而不是一个天气API的反向代理。3.2 SQLite表结构设计与初始化我的数据库里只放了两张表一张存七日预报趋势一张存最新实时状态CREATE TABLE IF NOT EXISTS weather_daily ( id INTEGER PRIMARY KEY AUTOINCREMENT, city_code TEXT NOT NULL, date TEXT NOT NULL, temp_max REAL, temp_min REAL, humidity INTEGER, wind_scale TEXT, weather_text TEXT, fetched_at TEXT, UNIQUE(city_code, date) ); CREATE TABLE IF NOT EXISTS weather_now ( city_code TEXT PRIMARY KEY, data_json TEXT NOT NULL, updated_at TEXT NOT NULL );weather_daily用来支撑七日温度趋势的可视化weather_now用来支撑页面顶部的当前天气卡片。weather_now直接存JSON字符串是因为实时天气字段杂且变化频繁单独拆列反而不好维护但weather_daily因为要做聚合计算所以拆成了结构化字段。建表逻辑统一放在db.py里Flask启动时自动执行import sqlite3 DB_PATH weather.db def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row return conn def init_db(): with get_conn() as conn: conn.executescript(SCHEMA_SQL)3.3 定时任务与内存TTL缓存定时任务我用APScheduler比较轻量不需要额外部署Celery这种重型任务队列。tasks.py里写一个刷新函数把配置里的城市都遍历一遍from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger def refresh_all_cities(): for name, code in config.CITY_MAP.items(): try: now fetch_now_weather(code) save_now(code, now) daily fetch_daily_forecast(code) save_daily(code, daily) except Exception as exc: app.logger.error(refresh %s failed: %s, name, exc) scheduler BackgroundScheduler() scheduler.add_job(refresh_all_cities, IntervalTrigger(minutes30)) scheduler.start()刷新间隔设为30分钟。对天气场景来说足够了太频繁不仅浪费额度城市天气预报本来也不会半小时内突变。内存缓存我用了一个最简单的字典加时间戳实现没上Redis_cache {data: None, ts: 0} def get_now_with_cache(city_code: str) - dict: if _cache[data] and time.time() - _cache[ts] 10: return _cache[data] row query_now(city_code) _cache.update({data: row, ts: time.time()}) return row这里TTL设为10秒只防手滑连续刷新这种场景。真正的数据新鲜度由表里的updated_at控制如果updated_at超过30分钟还查不到新数据接口层会返回提示而不是永远给旧值。Redis在这个项目里确实是过度设计多一个中间件就多一个部署节点SQLite加Python字典已经完美接住了所有并发量。4. 后端接口设计把天气数据变成前端最想要的样子4.1 接口清单与统一返回协议开发接口前我先定了一个通用返回格式所有接口都遵守{ code: 0, message: ok, data: {} }code为非0时表示业务异常message里写人类可读的错误原因。这样做的好处是前端只需要写一个统一的fetch处理函数不用每个接口单独做错误解析。我实际只需要三个核心接口GET /渲染首页也就是可视化大屏。GET /api/weather/now?city北京返回当前天气对应顶部卡片。GET /api/weather/week?city北京返回七日趋势与聚合指标对应图表。接口参数统一用city值为中文城市名。后端在CITY_MAP里查不到时返回code4001, message不支持的城市。这样前端下拉框里也只有支持的几个选项从源头上避免非法输入。4.2 趋势与指标把分析落成可计算的字段可视化分析不能只是把数据库里的数据原样丢给前端后端应该多做一点加工。我的/api/weather/week接口除了返回每天的日期、最高温、最低温、湿度、天气现象之外还会额外计算几个分析结论def build_week_analysis(records): high max(r[temp_max] for r in records) low min(r[temp_min] for r in records) avg_humidity round(sum(r[humidity] for r in records) / len(records)) max_range_day max(records, keylambda r: r[temp_max] - r[temp_min]) return { high: high, low: low, avg_humidity: avg_humidity, max_range_day: max_range_day[date], max_range: max_range_day[temp_max] - max_range_day[temp_min] }这些指标前端拿到后直接填到页面副标题里比如本周最高26度最低9度温差最大出现在4月12日。如果让前端自己算不仅代码重复而且万一多个页面都需要这个指标逻辑就散架了。后端算好前端只负责渲染这才是分析系统的合理分工。4.3 异常处理上游挂了前端也不能挂一个可视化系统最怕的不是没数据而是接口直接抛500前端白屏。我在所有API路由里都包了异常捕获并设计了降级策略app.route(/api/weather/week) def week_api(): city request.args.get(city, 北京) code config.CITY_MAP.get(city) if not code: return jsonify({code: 4001, message: 不支持的城市}), 200 try: records query_daily(code) if not records: return jsonify({code: 4002, message: 数据采集中请稍后再试}), 200 analysis build_week_analysis(records) return jsonify({code: 0, message: ok, data: {analysis: analysis, records: records}}) except Exception: app.logger.exception(query week failed) return jsonify({code: 500, message: 服务暂时不可用}), 200注意我这里HTTP状态码一律返回200业务错误靠code字段区分。这种做法在前后端分离项目里很有争议但我的理由很实际当页面刷新时如果接口返回非200浏览器控制台会刷满红色报错干扰排查统一200后前端只需要检查code是否为0。当然如果你有完善的监控体系用HTTP状态码表达错误更规范但小项目怎么省心怎么来。如果上游接口挂了但数据库里还有昨天缓存的数据我的降级策略是仍然返回数据但data里加一个stale: true标记前端在页面上显示一个小提示数据更新于xx小时前。宁可给旧数据也不能让用户面对一个空白页面。5. 前端可视化从温度曲线到空气质量雷达图的落地细节5.1 页面布局的决策过程可视化页面的布局我参考了常见数据大屏的做法但没有堆砌酷炫特效重点放在信息传达效率上。页面从上到下分三大块第一块是当前天气状态卡片包含温度、体感温度、湿度、风力、降水。这一块回答的是现在外面什么情况。第二块是七日温度趋势图使用双Y轴折线图左边是温度右边是湿度可以直观看出气温变化和湿度变化的对应关系。这一块回答的是这周天气怎么变化。第三块是风向雷达图与空气质量指示。风向用一周内各方向的频次做成雷达图空气质量用一个仪表盘显示当前AQI值和等级颜色。这一块回答的是这周适不适合户外活动。布局确定后我才去动手写HTML和ECharts没有一上来就堆图表。可视化项目的页面前期规划比写代码更重要图表不是越多越好而是要让人一眼看到重点。5.2 ECharts落地温度双轴曲线与AQI仪表盘的关键配置图表库我直接用了ECharts原因很简单国内文档全、示例丰富、普通图表场景根本不需要自己造轮子。通过CDN引入script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script温度趋势图的核心配置如下const tempChart echarts.init(document.getElementById(tempChart)); fetch(/api/weather/week?city encodeURIComponent(currentCity)) .then(r r.json()) .then(res { const records res.data.records; const dates records.map(r r.date.slice(5)); tempChart.setOption({ tooltip: { trigger: axis }, legend: { data: [最高气温, 最低气温, 平均湿度] }, xAxis: { type: category, data: dates }, yAxis: [ { type: value, name: 温度/℃, scale: true }, { type: value, name: 湿度/%, min: 0, max: 100 } ], series: [ { name: 最高气温, type: line, data: records.map(r r.temp_max), smooth: true }, { name: 最低气温, type: line, data: records.map(r r.temp_min), smooth: true }, { name: 平均湿度, type: bar, yAxisIndex: 1, data: records.map(r r.humidity) } ] }); });有几个细节需要说明scale: true让Y轴从贴近最小值得位置开始这样温度变化幅度在视觉上更明显否则0度和25度的折线会挤在一起。yAxisIndex: 1把湿度柱状图挂到右侧Y轴上温度和湿度量纲不同硬放同一个坐标系会出现一条线趋近于直线的问题。slice(5)只截取日期里的月-日部分图表横坐标显示04-10比2025-04-10清爽很多。空气质量仪表盘更简单它本质上是一个弧形进度条配置一个gauge类型的series配合AQI数值和颜色区域即可。前端通过series.data[0].value动态传入实时AQI再把axisLine的色带设置为绿、黄、橙、红、紫对应优、良、轻度污染、中度污染、重度污染五个等级。5.3 风向雷达图与动态刷新风向雷达图的思路是把一周内出现过的风向按方位归类统计频次后绘制。前端核心配置是把雷达图的indicator设置成北、东北、东、东南、南、西南、西、西北八个方向每个方向的max设为7把每天的wind_dir换算成方位索引后累加。换算逻辑放在后端更合适前端只接收一个现成的数组。比如后端的返回结构是{ wind_freq: [2, 0, 3, 1, 1, 0, 0, 0] }这个过程体现了后端接口设计和前端可视化的协作后端做好统计前端只负责画图。页面加载后我加了一个每5分钟自动刷新当前天气卡片的定时器但趋势图不刷新——因为七日趋势在30分钟内不会有变化频繁请求反而浪费资源。这也是一个优化点不同数据的更新频率不同不要一刀切全部定时刷新。另外ECharts在窗口尺寸变化时需要调用chart.resize()否则图形会被拉伸变形。我在dashboard.js里加了一个监听window.addEventListener(resize, () { tempChart.resize(); aqiChart.resize(); windChart.resize(); });这个小细节很多人会漏掉但桌面端缩放窗口算是高频操作不处理就会出现图表错位的尴尬。6. 部署上线gunicorn加nginx的坑以及一次线程池阻塞定位6.1 生产环境不只是换一个启动命令开发时我习惯用flask run但上线时千万不能用Flask自带的开发服务器它面向的是调试场景性能差、默认单进程、并发能力弱还会在控制台打印大量敏感请求信息。我改用gunicorn作为WSGI服务器gunicorn -w 3 -b 127.0.0.1:8000 app:app --timeout 30-w 3表示开3个worker进程--timeout 30限制单个请求最长30秒。worker数量我按2*CPU核心数1的公式取一台2核的云主机开3个worker足够了。为了让服务在服务器重启后也能自动拉起我写了一个systemd服务文件这是Linux服务器上最标准的长驻进程管理方式[Unit] Descriptionweather-vis gunicorn Afternetwork.target [Service] Userdeploy WorkingDirectory/home/deploy/weather_vis ExecStart/home/deploy/weather_vis/venv/bin/gunicorn -w 3 -b 127.0.0.1:8000 app:app --timeout 30 Restartalways [Install] WantedBymulti-user.target启动时用systemctl enable weather-vis让它开机自启再用journalctl -u weather-vis -f看日志。这套组合在我做过的中小型Flask项目里几乎没有出过问题。6.2 Nginx反向代理与静态文件404gunicorn跑起来之后服务只监听在127.0.0.1:8000外部访问不了所以我还需要一层Nginx反向代理。Nginx负责接收浏览器请求把动态请求转发给gunicorn同时直接接管静态文件。server { listen 80; server_name weather.internal; location /static/ { alias /home/deploy/weather_vis/static/; expires 7d; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里我踩过一个很典型的坑一开始我没配location /static/结果所有JS和CSS都通过gunicorn转发页面加载奇慢而且因为Flask的static_folder路径和实际目录有点偏差控制台一直报404。后来才明白Nginx的location /static/优先级高于location /所以静态请求直接由Nginx返回不会打到gunicorn。配置了Nginx静态托管后Flask的app.static_folder是否匹配已经不重要了因为静态请求根本到不了Flask那一层。如果你不想让Nginx托管静态文件也可以把所有请求都代理给Flask然后让Flask自己找static目录但生产环境务必用Nginx托管静态资源。expires 7d还能让浏览器缓存静态文件刷新页面时少一次资源请求体感速度快很多。6.3 一次线程池阻塞的定位过程上线运行了两天我收到反馈说页面偶尔会卡住几十秒刷新几次又恢复了。这个偶尔最让人头疼。我一开始怀疑是Nginx配置问题但查了Nginx错误日志并没有异常。后来用命令行直接请求后端接口curl -i http://127.0.0.1:8000/api/weather/now?city北京 --max-time 10发现接口有时候会在5秒左右才返回有时候直接超时。再配合gunicorn的access log我注意到一个规律卡住的时间点正好是APScheduler定时任务执行的时间段。问题找到了。Flask在gunicorn下是同步worker模型3个worker同时只能处理3个请求。我的定时任务里用requests.get()请求上游天气API而且早期没有设置timeout一旦上游响应慢定时任务这个请求会一直占着一个worker其他正常的页面请求就只能排队等待。最终表现就是页面卡住、再刷新就恢复、过半小时又卡一次。修复方案分两步第一步给所有上游请求加timeout5快速失败哪怕拿不到数据也不能拖死worker。第二步把定时任务从Flask进程里拆出去改成独立的systemd timer或单独的cron任务。这样定时任务和Web服务互不干扰即使上游API彻底失联也只会影响定时任务不会让用户请求排队。我在项目里用的是一个简单的systemd timer每天每30分钟执行一次tasks.py里的刷新函数。这是比在Flask进程内启动后台线程更健壮的做法。如果项目再复杂一些可以把定时任务独立成一个服务进程用消息队列通信但天气这种场景完全没必要。这次排查给我的经验是后台任务和Web请求一定要隔离。很多人喜欢在Flask启动时顺便开线程做定时任务开发时看着没问题上了gunicorn多进程环境就会出现各种诡异现象比如定时任务在每个worker里各执行一次。把任务独立出去既隔离了故障域也让代码职责更清晰。做完这步页面再也没出现过卡顿。项目的核心功能到此全部闭环数据定时采集、存储、接口、可视化、部署每个环节都经过了实际流量的检验。如果你打算照着这个思路做一个类似的Flask可视化项目我额外建议你把日志体系搭起来gunicorn的访问日志单独文件应用日志单独文件定时任务日志单独文件。这样出了问题第一反应不是猜而是看日志。项目规模再小日志都是救命的。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询