资讯动态

Flask实战:城市天气可视化分析从入门到部署

发布时间:2026/10/1 2:24:26 来源:尧图企业网站定制
做这个“基于Flask的城市天气可视化分析”项目最初只是想给自己日常出门做个参考小工具但做着做着就发现它其实踩遍了Web开发里最典型的一整条链路数据抓取、清洗存储、后端接口、前端图表渲染再到部署上线。整个项目麻雀虽小五脏俱全正好适合拿来当成Flask从入门到实战的完整练手项目。今天把这套东西从思路到实现完整拆一遍包括为什么选Flask、数据怎么拿、图表怎么做、部署要注意什么以及那些跑完一遍才会发现的坑。适合刚学完Flask基础语法想找个完整项目练手的人也适合想快速搭一个数据可视化Demo用于展示的开发者。1. 项目整体设计与思路拆解1.1 核心需求解析这个项目的核心需求其实就一句话输入一个城市名看到这个城市最近一段时间比如一周或一个月的天气变化趋势并且用图表直观呈现。听起来很简单但拆开以后至少有四个独立模块需要处理天气数据从哪来需要找一个可靠的天气数据源可能是免费API也可能是爬虫抓取。数据怎么存抓下来的数据是直接每次现取还是缓存到本地数据库这关系到性能。后端接口怎么设计Flask怎么接收前端请求怎么把数据整理成前端能用的格式。前端怎么展示用什么样的图表库展示哪些指标页面布局怎么组织。这四个模块任何一个单独拎出来都能水一篇教程合在一起才是这个项目的完整价值。我最初犯的错误就是一上来就写Flask路由结果前端要数据格式、后端不知道API返回结构、数据库字段不匹配全乱成一锅粥。正确做法是先把数据流画清楚用户点选城市 - 前端发请求 - Flask接收并读取数据可能触发爬取 - 格式化JSON返回 - 前端图表渲染。这个流程想清楚后面每一步都是填细节。1.2 为什么选Flask而不是Django或FastAPI很多人会问现在FastAPI这么火为什么还用Flask我的选择逻辑很实际这个项目的核心是可视化分析不是高并发API服务。Flask足够轻量上手门槛低模板渲染方便生态里Flask-SQLAlchemy、Flask-CORS、Flask-Caching这些插件完全覆盖需要。而且网上关于Flask的踩坑资料最多遇到问题容易搜到解决方案。Django适合大而全的后台管理系统自带Admin、ORM、认证但如果我只是要一个渲染页面加几个API接口Django多少有点杀鸡用牛刀。FastAPI确实性能更好自带OpenAPI文档但对前端模板渲染不那么直接而且如果用同步爬虫库FastAPI的优势也体现不出来。综合下来Flask是最省心的选择。当然项目结构上要有点讲究不能把所有代码写在一个app.py里。我最终的结构是这样weather_analysis/ ├── app.py # Flask应用入口注册蓝图 ├── config.py # 配置文件API密钥、数据库地址 ├── requirements.txt # 依赖列表 ├── models.py # 数据库模型 ├── weather_service.py # 天气数据获取与处理逻辑 ├── visualizer.py # 数据统计分析工具函数 ├── static/ │ ├── css/ │ ├── js/ │ └── charts.js # 前端图表渲染逻辑 ├── templates/ │ ├── index.html # 主页面 │ └── report.html # 分析报告页 └── data/ └── weather_cache.sqlite # 缓存的天气数据虽然规模不大但按功能拆分文件后维护起来比单文件舒服太多了。以后想加个新的数据源只需要改weather_service.py不会动到路由。2. 天气数据的获取与存储方案2.1 数据源比较与选择这个项目最关键的数据源问题。可选的方案大概有这几种第一使用免费天气API。比如OpenWeatherMap、WeatherAPI、和风天气这些注册个账号就能拿到免费额度。优点是有结构化JSON数据文档清晰不用自己解析网页。缺点是免费账户通常有每分钟请求次数限制比如每分钟60次并且历史数据支持有限只能拿到未来几天预报或当日实时。第二爬取公共天气网站。比如通过网页接口获取城市天气。优点是不需要注册API key数据来源广泛缺点是页面结构可能变化需要定期维护解析逻辑而且有些站点的数据接口是加密的解析成本高。第三购买付费API。数据最稳定但个人项目没必要。我最后用的是和风天气的免费API。原因有几个国内城市覆盖好中文城市名可以直接匹配免费版每天有访问量限制但个人开发完全够用返回的字段里既有当前实况也有未来几天的预报。关键是不用费劲去解析网页JSON直接能用适合把精力放在可视化上。2.2 数据抓取与清洗的细节调用API时有个坑一定要说免费版API返回的数据字段和文档里写的可能不完全一致。我拿到的数据里某些城市夜间天气字段是空的温度可能是字符串也可能是数字。所以拿到数据后必须先做一次清洗和标准化不能直接往数据库里塞。我的weather_service.py里有一段核心逻辑import requests import json from datetime import datetime def fetch_weather(city): 从和风天气API获取指定城市的实时天气和未来3天预报 params { key: API_KEY, location: city, unit: metric, lang: zh } # 这里省略实际API访问代码示例仅说明结构 resp requests.get(API_URL, paramsparams, timeout10) data resp.json() # 清洗处理缺字段和类型不一致 if data.get(now): temp data[now].get(temp) # 有些字段可能是字符串转成float并做兜底 try: temp float(temp) except (TypeError, ValueError): temp None data[now][temp] temp return data一个很容易被忽视的细节是请求超时处理。爬天气接口时如果网络波动默认的requests请求可能会卡很久。所以我给每个请求设置了timeout10秒并且用try/except包住保证即使某个城市请求失败也不影响整体服务。数据存哪里我选择的是SQLite。原因很简单不需要单独启一个数据库服务Flask的SQLAlchemy天然支持SQLite文件就在项目目录下方便备份。存表结构大致是这样class WeatherRecord(db.Model): id db.Column(db.Integer, primary_keyTrue) city db.Column(db.String(50), indexTrue) date db.Column(db.String(20)) temp_max db.Column(db.Float) temp_min db.Column(db.Float) weather_desc db.Column(db.String(50)) humidity db.Column(db.Float) wind_dir db.Column(db.String(20)) wind_scale db.Column(db.Float) created_at db.Column(db.DateTime, defaultdatetime.now)这里我加了city和date的联合唯一约束避免同一城市同一天重复存储。写入前先查询一下如果已经有了就更新而不是新增防止数据越攒越乱。2.3 缓存策略不能每次实时拉取如果用户每次访问页面都去调API免费额度很快见底而且接口响应时间长影响体验。我的做法是建立一层缓存判断当前城市和日期是否已经请求过如果在有效期内比如30分钟就直接从本地数据库读否则才去调API更新。def get_cached_weather(city, cache_minutes30): 返回缓存的天气数据过期则自动更新 records WeatherRecord.query.filter_by(citycity).all() if not records: return fetch_and_store(city) latest max(r.created_at for r in records) delta (datetime.now() - latest).total_seconds() / 60 if delta cache_minutes: return fetch_and_store(city) return format_records(records)这样做的另一个好处是当API临时挂掉时用户至少还能看到以前缓存的数据不至于报错。这个兜底机制在演示时特别有用。3. Flask后端API设计3.1 路由设计思路整个项目涉及的路由并不多但每个路由的职责要清晰。我设计了三个主要的接口/首页渲染城市选择页面和默认城市数据。/api/weather?city北京根据城市名返回天气数据JSON核心接口。/api/history?city北京days7返回历史查询记录用于分析趋势。把所有API统一放在/api/前缀下是为了后续扩展方便。前端如果需要支持更多功能比如对比两个城市就再加一个/api/compare不会破坏已有接口。首页的渲染直接用Flask的render_template把城市列表、默认图表数据作为模板变量传给前端。而不是让前端页面加载后额外发起一次请求这样首屏速度更快。3.2 天气数据接口的返回格式设计接口设计上有个容易犯的错误把后端数据原封不动返回给前端。比如API返回的中文天气描述字段是cond_txt如果后端直接返回这个键名前端的代码可读性会非常差。我选择在后端做一次字段映射返回给前端的是统一的、结构清晰的JSON。{ city: 北京, updated_at: 2024-01-15 20:30:00, now: { temperature: 22.5, feels_like: 24.0, humidity: 45, weather: 晴, wind: 东北风 3级, pressure: 1013 }, forecast: [ {date: 2024-01-15, temp_max: 27.0, temp_min: 18.5, weather: 晴}, {date: 2024-01-16, temp_max: 26.0, temp_min: 19.0, weather: 多云} ], history: [ {date: 2024-01-08, temp_max: 25.0, temp_min: 16.0}, {date: 2024-01-09, temp_max: 24.0, temp_min: 16.5} ] }这样前端拿到数据后不需要知道API原始字段长什么样逻辑全封装在后端。同时后端也做了数据校验如果城市不存在返回return jsonify({error: city not found, code: 404}), 404让前端可以根据HTTP状态码做出不同提示。3.3 历史数据分析接口与聚合逻辑这个项目不能只看实时还得有分析。所谓“可视化分析”我理解至少要做两件事温度趋势变化、天气分布统计。为了方便前端绘图后端直接提供一个聚合好的数据接口。这个聚合逻辑放在visualizer.py里比如计算一周内最高温的平均值、每天温差、天气出现频次等。核心是减少前端计算负担让图表直接绑定数据。def summarize_trends(records): 把一系列天气记录转成趋势统计 temps [r.temp_max for r in records if r.temp_max is not None] summary { avg_high: round(sum(temps) / len(temps), 1) if temps else None, max_high: max(temps) if temps else None, min_high: min(temps) if temps else None, weather_counts: {} } for r in records: key r.weather_desc or 未知 summary[weather_counts][key] summary[weather_counts].get(key, 0) 1 return summary聚合接口被前端调用后可以生成柱状图展示各天气类型的天数或者折线图展示温差变化。这种“后端给数据、前端画图”的分工最舒服。4. 前端可视化展示的实现4.1 图表库选型ECharts的取舍前端图表库我试过好几个最后选了ECharts。Chart.js更简洁但对复杂图表的定制能力弱D3.js功能最强大但入门曲线陡峭做这个项目没必要。ECharts中文文档友好、支持异步加载数据、内置多种交互效果而且模板渲染起来简单一个div加几行配置就能出一个专业级别的图。引入方式直接用了CDNscript srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script如果是内网环境部署建议把ECharts文件下载到static/js目录下避免离线时页面光秃秃的。4.2 温度趋势折线图的实现温度趋势是天气可视化里最直观的图表。我做了双折线一条最高温一条最低温一眼看出温差变化。核心配置项需要注意连续数据的x轴格式化。function renderTempTrend(containerId, history, forecast) { var chart echarts.init(document.getElementById(containerId)); var option { title: { text: 近7天气温变化, left: center }, tooltip: { trigger: axis }, legend: { data: [最高温, 最低温], bottom: 0 }, xAxis: { type: category, data: history.map(item item.date.slice(5)) }, yAxis: { type: value, name: 温度(°C), scale: true }, series: [ { name: 最高温, type: line, data: history.map(item item.temp_max), smooth: true, lineStyle: { width: 3 }, areaStyle: { opacity: 0.1 } }, { name: 最低温, type: line, data: history.map(item item.temp_min), smooth: true, lineStyle: { width: 3 } } ] }; chart.setOption(option); window.addEventListener(resize, () chart.resize()); }有个细节当数据量只有一条或没有数据时图表会显示得很奇怪所以前端渲染前要先判断数据长度为空时显示一个“暂无数据”的覆盖层而不是让ECharts硬画。4.3 天气分布饼图与数据钻取除了温度折线图我还用饼图展示各天气类型占比晴、多云、阴、小雨等。饼图配置相对简单但有两个坑得提醒颜色要自己定义一套默认颜色有些和白色背景不搭当某个分类占比太小文字标签会重叠。我采用对小于5%的分类隐藏文字标签的策略。function renderWeatherPie(containerId, weatherCounts) { var data Object.keys(weatherCounts).map(function(key) { return { name: key, value: weatherCounts[key] }; }); data.sort(function(a, b) { return b.value - a.value; }); var option { tooltip: { trigger: item, formatter: {b}: {c}天 ({d}%) }, series: [{ type: pie, radius: [40%, 70%], avoidLabelOverlap: true, label: { show: true, formatter: function(params) { var percent params.percent; if (percent 5) return ; return params.name percent %; } }, data: data }] }; }这里我加入了“数据钻取”的思路饼图只能看比例想看具体对应哪天的天气怎么办我实现了一个简单的交互点击饼图扇区时触发事件页面下方显示该天气类型下的日期列表。这在ECharts里通过chart.on(click)完成。除了图表页面明显还缺一个数字卡片区。我做了四个指标卡片当前温度、今日最高/最低、风力、湿度。这部分不需要图表库直接用后端传过来的JSON绑定到HTML元素即可。卡片的好处是扫一眼就能获取重点信息不必盯着图表看。5. 项目部署与运行优化5.1 本地开发环境的搭建开发环境的搭建其实很简单但要强调Python虚拟环境的使用。我一开始图省事直接全局装依赖后来项目多了版本冲突才后悔。现在每个项目都标配venvpython -m venv venv source venv/bin/activate # Windows上为 venv\Scripts\activate pip install flask flask-sqlalchemy requests装完之后把依赖导出到requirements.txt方便以后换个机器一键恢复环境pip freeze requirements.txt5.2 生产环境的部署方式开发时直接python app.py跑Flask内置服务器没问题但如果放到公网服务器上内置服务器扛不住并发。我的部署组合是Gunicorn Nginx。Flask应用由Gunicorn启动Nginx负责反向代理和静态文件服务。Gunicorn启动命令通常长这样gunicorn -w 4 -b 127.0.0.1:8000 app:app-w 4表示4个worker进程。具体worker数量一般按服务器CPU核心数的2倍加1来估算。比如2核的服务器就用5个worker但4也够用。加--timeout 120防止耗时请求卡死worker。Nginx配置里关键就是把/代理到本机的8000端口同时把/static路径指到静态文件目录location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /path/to/weather_analysis/static/; }注意Gunicorn只能通过本地socket访问不能让外部直接访问8000端口。Nginx用root权限监听80端口然后和Gunicorn之间走127.0.0.1这样对外只暴露Nginx既安全又能利用Nginx的静态文件处理能力。5.3 常用优化技巧项目在运行中遇到了几个性能问题我做了针对性优化第一数据库连接优化。Flask-SQLAlchemy默认每次请求用完就关闭连接SQLite本身并发能力有限。我启用了连接池设置engine_options但要注意SQLite不适合多个worker共享写连接。最稳妥的方式是把数据查询逻辑尽量放在创建连接后尽快提交避免长事务。第二缓存。我除了用数据库做缓存还加了Flask-Caching装饰器把API响应直接缓存几分钟from flask_caching import Cache cache Cache(app, config{CACHE_TYPE: simple}) app.route(/api/weather) cache.cached(timeout15, query_stringTrue) def weather_api(): ...这样就算数据库查询频繁同一个城市在15秒内的请求都会直接走缓存响应时间降到毫秒级。第三前端静态文件压缩。ECharts的min版大概1MB虽然带宽影响不大但为了速度我后来换成按需构建的echarts核心包只引入折线图和饼图相关模块文件体积降到不足300KB。这个优化对移动端尤其明显。6. 常见问题与排查技巧实录6.1 数据库锁定的诡异问题用SQLite做缓存时遇到最经典的坑就是并发写导致的database is locked错误。有时页面里同时发起多个请求后端的多个请求线程同时写SQLite一个写锁没释放另一个写操作就被阻塞并报错。排查过程我先看Gunicorn是否开启了多个worker发现4个worker确实可能同时访问同一个SQLite文件。然后查Flask-SQLAlchemy的配置发现默认check_same_thread对SQLite是关闭的但依然会有锁定问题。解决方案也很直观把SQLite的写操作都封装到一个专门的函数里加上重试机制。如果遇到锁定就等一会儿再试from sqlalchemy.exc import OperationalError import time def retry_on_lock(func): def wrapper(*args, **kwargs): for i in range(3): try: return func(*args, **kwargs) except OperationalError as e: if locked in str(e): time.sleep(0.5) continue raise return wrapper这个技巧实测下来能解决绝大多数轻微锁冲突。如果还是频繁出现可以把SQLite的journal模式改成WALapp.config[SQLALCHEMY_ENGINE_OPTIONS] { connect_args: {timeout: 15} }并确保每个请求结束后就调用db.session.remove()。6.2 城市名编码带来的数据不匹配用户输入城市名时有些带“市”字“北京市”有些不带“北京”还有些是拼音。API对这种模糊匹配的容忍度不一致就会导致查询结果不准确。我做的处理是写了一个城市名归一化函数去掉“市”“省”后缀统一转小写并维护了一个常用别名映射表CITY_ALIASES { bj: 北京, sh: 上海, shanghai: 上海, guangzhou: 广州, sz: 深圳, shenzhen: 深圳, 成都: 成都, }另外在前端写了一个城市下拉框只允许选择预先支持的几十个城市就完全避开自由输入的各种方言表达问题。如果用自由输入建议在后端做一个模糊匹配加提示而不是直接报错。6.3 API请求超时与备用数据源切换免费API偶尔会抽风响应时间超过10秒或者直接返回5xx。为了不让用户看到报错页面我写了一个降级逻辑主API失败时尝试从备用数据源拉取备用数据源也失败时返回数据库中最接近当天日期的缓存数据。这个降级策略的代码不复杂但价值很大def fetch_with_fallback(city): for source in [primary_api, backup_api]: try: data source.fetch(city) if data: return data except Exception: continue # 返回缓存中最新的数据 return get_latest_cached(city)如果所有数据源都不可用最后再返回一个结构为空的JSON前端会显示“天气数据暂时不可用”的友好提示。千万别让Flask直接抛500异常一是不美观二是不专业。6.4 前端图表渲染空白的问题ECharts图表偶尔出现一片空白最常见的原因有三个一是容器div没有设置高度。ECharts的init需要容器有确定的宽高如果div样式只写了width:100%没有height图表画不出来。这点非常隐蔽因为不报错只是空白。二是动态切换到隐藏的tab时初始化图表此时容器宽度为0图表无法正常渲染。解决办法是在容器可见后再调用chart.resize()。三是数据中包含null或undefined导致坐标轴数据不连续。我前端统一把null值过滤或补成0或者在ECharts的series里设置connectNulls: false让断连点不连线。6.5 时间时区问题的坑记录天气数据时我一开始直接用datetime.now()存本地时间。但服务器可能部署在别的时区导致缓存过期判断混乱。后来统一改用UTC存储在展示时再转成北京时间from datetime import datetime, timezone, timedelta def utc_to_cst(utc_dt): cst timezone(timedelta(hours8)) return utc_dt.replace(tzinfotimezone.utc).astimezone(cst)这样不管服务器在哪个时区前端展示的时间永远正确。一个很小的改动但能避免很多怀疑人生的bug。7. 经验心得与扩展思路7.1 做这类数据展示项目的流程建议做完整个项目我最大的体会是要先把数据流跑通再考虑美化。第一步先确认API能不能用返回的数据结构长什么样。第二步把数据结构、表结构打印出来看清楚确认每个字段的类型。第三步才写Flask路由用curl测试JSON接口。第四步才是挑图表库画图。很多人一上来就折腾页面结果数据一直不通来回改代码非常痛苦。如果你也想复现这个项目我给一个精简的起步清单申请一个免费天气API的key用requests直接调一次保存JSON样本。创建SQLite表和Flask应用把一次请求的数据成功写入库并打印出来。写一个最简单的/api/weather路由返回一条记录用浏览器访问确认。引入ECharts用静态的假数据先画一个折线图。把假数据替换成真实接口数据调通整链路。最后再逐步加入城市切换、历史趋势、部署上线。7.2 后续还可以扩展哪些功能这个项目目前只支持单城市展示但框架上可以很容易扩展多城市对比后端增加一个/api/compare接口同时返回几个城市的数据前端用多条线画在同一张图上。历史深度分析如果积累数据超过几个月可以做温度距平、季节性分析日历热力图展示全年气温分布。空气质量订阅接入更多数据源把PM2.5、AQI也加入图表。自动化运行用定时任务每天自动拉取所有关注城市的数据存库这样历史记录不会因为用户不访问就缺失。这些扩展都不需要改动项目根基只要在service层加方法在模板里加图表即可。7.3 最后想说的实际体会说实话这个项目最让我有成就感的不是图表有多炫而是把一条完整的数据链路跑通并稳健运行。用Flask做可视化分析本质上是把“获取数据—处理数据—展示数据”这三个环节用一种统一的技术栈串起来其中任何一环的质量都会影响最终效果。开发过程中踩过的坑比如时区问题、SQLite锁定、ECharts空白几乎都是真实工作里会遇到的问题解决一个就长一分经验。如果你也想练手建议别只看不练直接拿一个周末把代码写出来。数据源可以先用固定的Mock数据代替先把链路搞通再替换成真实数据。等整个项目跑起来你会对Flask、前后端交互、数据可视化都有一个很踏实的体感。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑