☰
Flask实战:城市天气可视化分析从入门到部署
2026/10/1 2:24:21 网站建设 项目流程

做这个“基于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, params=params, timeout=10) 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请求可能会卡很久。所以我给每个请求设置了timeout=10秒,并且用try/except包住,保证即使某个城市请求失败也不影响整体服务。

数据存哪里?我选择的是SQLite。原因很简单:不需要单独启一个数据库服务,Flask的SQLAlchemy天然支持SQLite,文件就在项目目录下,方便备份。存表结构大致是这样:

class WeatherRecord(db.Model): id = db.Column(db.Integer, primary_key=True) city = db.Column(db.String(50), index=True) 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, default=datetime.now)

这里我加了city和date的联合唯一约束,避免同一城市同一天重复存储。写入前先查询一下,如果已经有了就更新而不是新增,防止数据越攒越乱。

2.3 缓存策略:不能每次实时拉取

如果用户每次访问页面都去调API,免费额度很快见底,而且接口响应时间长影响体验。我的做法是建立一层缓存:判断当前城市和日期是否已经请求过,如果在有效期内(比如30分钟),就直接从本地数据库读,否则才去调API更新。

def get_cached_weather(city, cache_minutes=30): """返回缓存的天气数据,过期则自动更新""" records = WeatherRecord.query.filter_by(city=city).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=北京&days=7:返回历史查询记录,用于分析趋势。

把所有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加几行配置就能出一个专业级别的图。

引入方式直接用了CDN:

<script src="https://cdn.jsdelivr.net/npm/echarts@5/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虚拟环境的使用。我一开始图省事直接全局装依赖,后来项目多了版本冲突才后悔。现在每个项目都标配venv:

python -m venv venv source venv/bin/activate # Windows上为 venv\Scripts\activate pip install flask flask-sqlalchemy requests

装完之后把依赖导出到requirements.txt,方便以后换个机器一键恢复环境:

pip freeze > requirements.txt

5.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(timeout=15, query_string=True) 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模式改成WAL:

app.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(hours=8)) return utc_dt.replace(tzinfo=timezone.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、前后端交互、数据可视化都有一个很踏实的体感。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询