
2. 开头其实应该跳过直接从主体开始这个天气可视化分析系统项目我拿到手第一时间的感觉就是典型的全栈练手项目但麻雀虽小五脏俱全——数据采集、数据清洗、数据库建模、后端接口、前端可视化全占了。你要是正在做课程设计、毕业设计或者刚学完Python基础想找个完整项目练手这个项目就是个很好的样板。它不只是简单的天气查询而是把从网上拿到天气数据 → 存进数据库 → 通过网页展示并分析整条链路跑通让你知道真实项目里数据是怎么流动的。我基于Flask实现的这套系统整体设计时没有引入特别重的组件框架用Flask数据库用MySQL也可以用SQLite做本地替代前端用ECharts画图表。最大的感受是这个项目拿来学习技术栈串联再合适不过它强迫你去理解前后端交互、数据库设计和爬虫基础而不是停留在看教程的层面。1. 项目整体设计与思路拆解1.1 为什么要选 Flask 而不是 Django做这类中小型可视化分析系统选框架时大家最纠结的就是Flask和Django。我的建议很直接除非你的系统里有用户权限分级、后台管理面板这种重型功能必须依赖Django自带的Admin和ORM否则Flask是更合适的切入点。Flask的核心理念是微框架但它绝不是功能残缺的玩具。项目里我用到了Flask的几个核心组件Blueprint蓝图用来拆分业务模块避免把所有路由都堆在app.py里Jinja2 模板引擎渲染HTML页面把Python变量直接嵌入模板WerkzeugFlask自带的WSGI工具集密码加密、debug模式、静态文件服务都靠它对比下来Flask的优势在项目里体现得很直观。一个天气可视化系统核心就三块数据展示页面、数据查询接口、数据采集脚本。Flask写这三个模块非常轻快每个功能用到的代码量都很少而且Python新手看Flask的路由装饰器app.route要比看Django的urls.py views.py settings.py那一堆配置好理解得多。简单说如果项目是我要做一套完整的内容管理后台选Django如果项目是我做几个页面展示数据外加几个接口提供给前端调用Flask就够了而且能让你把注意力集中在业务逻辑上而不是被框架配置拖累。1.2 数据可视化系统的通用架构分层我在设计这套系统时参考了企业级数据可视化项目的分层思路但没有为了架构而架构只保留了三层数据采集层负责从天气API获取原始数据并做基础清洗业务逻辑层Flask应用本体负责处理前端请求、从数据库取数、调用采集层更新数据展示层HTML CSS JavaScript ECharts负责把数据变成图表和卡片这样的分层带来的最大好处是可替换性。举个例子数据采集层最初我用的是某免费天气API后面发现免费额度不够用换成另一个数据源时只需要修改采集层的逻辑Flask的业务代码完全不用动。这就是所谓低耦合的意义——你可能觉得一个课设项目不需要考虑这些但当你做毕业设计答辩老师问你的项目可以扩展吗的时候分层的价值就体现出来了。从另一个角度说这样的分层也符合高内聚原则每层只做自己的事。前端页面不用关心数据是来自数据库还是来自第三方接口后端不用关心数据是怎么被画成图表的。排查问题的时候思路也清晰前端图表显示不对就去查接口返回的数据结构接口返回不对就去查数据库的取值逻辑数据库数据不对就去查采集层一层一层定位。2. 环境准备与核心依赖安装2.1 Python虚拟环境搭建动手写代码之前先把环境弄干净。我见过太多人把自己系统里的Python环境搞得一团糟装了一堆包互相冲突最后连Flask都跑不起来。我的建议是每个项目都建独立虚拟环境不要怕麻烦。# 创建虚拟环境 python -m venv weather_env # Windows激活 weather_env\Scripts\activate # macOS/Linux激活 source weather_env/bin/activate虚拟环境的原理其实很简单本质就是在项目目录下生成一个独立的Python解释器目录你安装的所有包都装进这个目录里和全局Python环境隔离。这样A项目用的Flask 2.xB项目用的Flask 3.x互不干扰。激活虚拟环境后用pip安装核心依赖pip install flask pip install requests pip install pandas pip install pymysql pip install flask-sqlalchemy pip install flask-cors这里多说一句版本问题一定要留意。Flask 3.x和Flask 2.x在路由语法上有一些细微差别有些老教程用的是app.route(/api, methods[POST])这种写法在3.x里依然兼容但有些第三方扩展不一定兼容新版Flask。如果安装依赖时提示某些包冲突最简单的办法是把Flask锁在2.2.x版本这个版本相当稳定兼容性最好数据库课设项目用这个版本足够了。2.2 数据库选型MySQL 还是 SQLite技术选型的时候我在MySQL和SQLite之间权衡过。两者对比大概是这样的对比项MySQLSQLite部署方式独立数据库服务需要安装和配置嵌入式数据库只需一个文件并发能力支持并发读写写操作锁粒度大不适合高并发权限管理有完整的用户、权限体系无权限管理适用场景公开展示、多人访问、数据量大本地单机、课程设计演示上手难度需要额外安装客户端和配置零配置安装驱动即用如果只是自己电脑上跑通系统SQLite很简单快捷不用装MySQL服务省很多麻烦。但考虑到你要交数据库课程设计那还是建议上MySQL。因为课程设计答辩时老师大概率会问你数据库设计的问题MySQL表结构、SQL语句、数据库连接这一套是必须走一遍的。用SQLite的话底层是文件存储很多数据库概念体现不出来答辩时容易被问住。我的最终方案是代码中使用flask-sqlalchemy作为ORM层这样数据库连接字符串修改就能切换数据库类型。SQLite连接串是sqlite:///weather.dbMySQL连接串是mysqlpymysql://用户名:密码localhost:3306/weather_db。先用SQLite快速开发调试稳定后再切换MySQL部署。这个习惯挺好用也推荐给你。2.3 Flask项目的目录结构规划好的目录结构是项目的骨架规划清楚后再写代码思路会很顺畅。我用的结构如下weather-visualization/ ├── app.py # Flask应用入口 ├── config.py # 配置文件数据库连接、密钥等 ├── models/ │ ├── __init__.py │ ├── user.py # 用户模型 │ └── weather.py # 天气数据模型 ├── routes/ │ ├── __init__.py │ ├── auth.py # 登录注册模块 │ ├── weather.py # 天气查询模块 │ └── analysis.py # 数据分析模块 ├── utils/ │ ├── __init__.py │ ├── data_fetcher.py # 天气数据采集模块 │ └── db_helper.py # 数据库初始化脚本 ├── static/ │ ├── css/ │ ├── js/ │ └── img/ ├── templates/ │ ├── base.html # 基础模板 │ ├── index.html # 首页 │ ├── login.html │ ├── register.html │ └── analysis.html └── requirements.txt有了这个结构项目的每个文件职责都很明确。后续接第三方API、加城市管理、改图表类型都不用动不相关的文件。实际操作中你会发现一个清晰的目录结构比多写三五个功能都有价值因为代码定位快出问题的概率就低。3. 数据采集与数据库设计3.1 天气数据来源API 调用与爬虫方案对比做天气可视化系统第一步是拿到数据源。综合稳定性和合法性我优先推荐用天气API而不是去爬网页。目前主流天气API/数据源有这几种数据源免费额度返回格式是否需要注册和风天气每日1000次开发版够用JSON是心知天气每日50次JSON是OpenWeatherMap每分钟60次JSON是中国天气网无限制但无官方接口HTML需解析否API调用天然比爬虫稳定。它返回的是结构化JSON数据字段明确更新频率可控调用方式就是HTTP请求加参数拼接代码只需要处理好异常和限流就行。爬网页最大的问题是页面结构随时可能变你可能今天写的选择器明天就失效了。结合免费额度和国内节点速度我最终选用和风天气作为采集源。注册账号后创建应用拿到API Key接入流程大概这么几步import requests def fetch_weather(city_id101010100): # city_id 北京传入具体城市id可切换 url https://devapi.qweather.com/v7/weather/now params { location: city_id, key: 你的API_KEY } response requests.get(url, paramsparams) return response.json()这个API返回的核心数据大致是温度、体感温度、天气现象、湿度、风向风力、能见度、气压、云量等实时天气数据。对于课程设计来说字段完全够用。3.2 数据库三张核心表的设计思路天气数据拿到后不能直接丢给前端展示要设计好存储结构。我建了三张表每张表都对应一个业务维度users 用户表字段名类型说明idINT 主键自增用户IDusernameVARCHAR(50) 唯一用户名password_hashVARCHAR(128)密码哈希created_atDATETIME注册时间favorite_cityVARCHAR(100)默认关注城市cities 城市表字段名类型说明idINT 主键自增城市IDcity_nameVARCHAR(50)城市名city_codeVARCHAR(20)天气API对应城市代码provinceVARCHAR(50)所属省份update_timeDATETIME最后更新时间weather_data 天气数据表字段名类型说明idINT 主键自增记录IDcity_idINT 外键关联城市表tempFLOAT温度(℃)feels_likeFLOAT体感温度humidityINT湿度(%)pressureINT气压(hPa)wind_dirVARCHAR(20)风向wind_scaleVARCHAR(20)风力等级visibilityINT能见度(km)weather_conditionVARCHAR(50)天气现象record_dateDATE记录日期这个设计当然不算复杂但对于记录某城市某时刻的天气情况这个需求来说字段是合理的。有两点建议你注意首先weather_condition字段建议存晴/多云/小雨这类中文不要存100这种编码值。虽然API返回时带有一个天气代码字段但代码映射到中文需要前端再做一层翻译不如直接存中文省事。其次record_date加一个普通索引。因为做历史数据分析时基本所有的查询条件都是按时间范围检索有了索引查询速度快很多。虽然数据量小了感受不到区别但这个习惯在真实项目里能省你很多功夫。3.3 数据库增删改查封装用flask-sqlalchemy定义模型后增删改查的封装可以参考下面的写法from app import db class Weather(db.Model): __tablename__ weather_data id db.Column(db.Integer, primary_keyTrue) city_id db.Column(db.Integer, db.ForeignKey(cities.id)) temp db.Column(db.Float) humidity db.Column(db.Integer) record_date db.Column(db.Date) staticmethod def get_history(city_code, days7): 查询某城市最近N天的天气记录 return Weather.query.join(City).filter( City.city_code city_code, Weather.record_date date.today() - timedelta(daysdays) ).order_by(Weather.record_date.asc()).all()这里有个细节值得注意我写的get_history用的是filter而不是filter_by。filter_by的写法是filter_by(city_codecity_code)只能做简单的等值判断而filter可以使用这种复杂条件也能参与表连接灵活性更高。项目里带条件的查询都用filter简单等值才用filter_by。数据库设计的核心原则就是按查询方式设计表。你要做什么分析就设计对应的表结构和索引。如果一上来想不清楚就先把需求列出来比如查看最近7天温度曲线、查看最热城市排行、查看某天全国天气分布再倒推需要哪些字段和表思路就清晰了。4. Flask 后端接口与业务逻辑实现4.1 项目初始化与蓝图注册后端部分首要任务是写好Flask应用入口。用蓝图拆分模块之后app.py看起来非常清爽# app.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from config import Config db SQLAlchemy() def create_app(): app Flask(__name____) app.config.from_object(Config) db.init_app(app) from routes.auth import auth_bp from routes.weather import weather_bp from routes.analysis import analysis_bp app.register_blueprint(auth_bp, url_prefix/api/auth) app.register_blueprint(weather_bp, url_prefix/api/weather) app.register_blueprint(analysis_bp, url_prefix/api/analysis) return app if __name__ __main__: app create_app() app.run(debugTrue, host0.0.0.0, port5000)create_app这种工厂函数模式看上去比直接全局new一个app绕了一层但它的好处是测试时可以创建独立的测试应用实例生产时加载生产配置不会跑到一半发现诶我的数据库连接怎么用的开发库。注册蓝图时用url_prefix统一给接口加前缀也让路由命名更规范。4.2 登录注册接口与密码安全用户模块是这个系统里最接近真实业务的部分。登录注册最核心的问题不是功能而是密码安全。绝对不要用明文存密码这是我反复强调的底线问题。即使用户只是本地测试也应该养成密码加密的习惯。Flask里做密码加密不需要额外引入第三方库。Werkzeug自带了两个现成的方法就是generate_password_hash和check_password_hashfrom werkzeug.security import generate_password_hash, check_password_hash # 注册时 user User(usernameusername, password_hashgenerate_password_hash(password)) # 登录时 user User.query.filter_by(usernameusername).first() if user and check_password_hash(user.password_hash, password): # 登录成功generate_password_hash内部用的是pbkdf2算法并且每次生成的哈希值都带随机盐同一个密码两次生成的哈希完全不同。这是防彩虹表攻击的标准做法。另外一个建议是接口层统一返回JSON格式错误信息方便前端判断是用户名不存在还是密码错误但不要把明确提示给到前端——比如用户名不存在和密码错误最好统一返回用户名或密码错误这是为了安全防止用户被枚举出合法用户名。课程设计里可能不需要这么严格但养成这个意识真的挺重要。登录状态保持方面小车方案是用session存用户ID。Flask的session是签名Cookie不是放到服务器内存里的所以要在config.py里设置一个SECRET_KEYclass Config: SECRET_KEY your-secret-key-change-me SQLALCHEMY_DATABASE_URI mysqlpymysql://root:passwordlocalhost:3306/weather_db这个SECRET_KEY一旦泄露攻击者可以伪造session值伪造用户身份。所以正式部署一定要改掉默认值并且不要传到Git仓库里。4.3 天气查询与历史数据的接口设计系统的核心接口是天气数据查询与分析。我对外提供三个主要接口接口一实时天气查询weather_bp.route(/current/city_code, methods[GET]) def current_weather(city_code): data fetch_weather(city_code) # 数据清洗去掉不需要的字段 result { city: data[city], temp: data[now][temp], condition: data[now][text], humidity: data[now][humidity], wind: f{data[now][windDir]} {data[now][windScale]}级, update_time: data[updateTime] } # 同时写入数据库归档 save_weather_record(city_code, result) return jsonify(code200, dataresult)这个接口实际上做了两件事把外部API的数据清洗成内部统一格式返回给前端同时把数据归档写进数据库。为什么要写库因为实时天气API返回的只是当下数据但你要画过去24小时温度变化曲线靠实时请求是没用的必须依靠数据库里的历史记录。接口二历史趋势查询analysis_bp.route(/history/city_code, methods[GET]) def history_analysis(city_code): days request.args.get(days, 7, typeint) records Weather.get_history(city_code, days) labels [r.record_date.strftime(%m-%d) for r in records] temps [r.temp for r in records] humidity [r.humidity for r in records] return jsonify( code200, data{ labels: labels, temp: temps, humidity: humidity, city: get_city_name(city_code) } )接口三城市对比分析这个接口接收两个或多个城市的城市代码返回它们在同一时间段内的温度、湿度、天气状况对比数据。这个功能做成柱状图和雷达图非常直观前端画出来就能直观看出同一个时间段哪个城市更热、哪个更干。接口设计时有个心得返回的数据结构一定要给前端省事。比如前端要画chart你就直接把labels和values分开给他而不是给一堆原始JSON字段让他自己拆。前端拿到什么就能直接用而不用再写一遍数据清洗逻辑接口的可复用性就高出很多。5. 可视化页面与前端展示5.1 ECharts 与 Jinja2 模板的结合思路后端接口通了剩下的就是把数据变成看得懂的页面。前端可视化这块我用的是Apache ECharts它内部基于Canvas渲染图表类型丰富配置相对简单课程设计答辩时展示效果好是关键。ECharts的引入有两种方式一种是在模板里直接CDN引用script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script另一种是把echarts.min.js下载到static/js目录下。如果答辩现场或演示环境的网络状况不稳定强烈建议下载到本地再引用避免演示到一半图表加载不出来。Jinja2模板和ECharts结合核心逻辑是Jinja2负责页面渲染ECharts图表需要的动态数据通过Ajax请求后端接口获取。这样做的原因是ECharts图表往往需要在数据到达后动态更新而Jinja2的模板语法{{ }}只能静态渲染服务端传过去的变量。所以我的方案是页面初始框架用Jinja2渲染页面加载完成后发起Ajax请求拿JSON数据数据拿到后再初始化ECharts图表。5.2 实时天气卡片与趋势曲线的实现拿七日温度趋势曲线这个功能举例。核心代码其实不长async function loadHistoryChart(cityCode) { const response await fetch(/api/analysis/history/${cityCode}?days7); const result await response.json(); if (result.code ! 200) { alert(数据加载失败); return; } const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ title: { text: ${result.data.city} 近7日温度趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: result.data.labels }, yAxis: { type: value, name: 温度(℃) }, series: [{ name: 温度, type: line, data: result.data.temp, smooth: true, areaStyle: { opacity: 0.3 } }] }); }代码不复杂但有三个细节值得注意。第一echarts.init之前要确保DOM元素已经被渲染出来了。如果脚本放在页面底部或者用了defer属性通常没问题但如果脚本放在头部又直接调用了init拿到的是undefined图表一定显示不出来。第二setOption是ECharts的核心方法。它支持多次调用并且多次调用时会自动做增量更新。比如你先设置了x轴数据用户切换城市后再次调用setOption不用重新创建图表实例直接更新数据即可。第三图表容器必须有固定的宽高。ECharts的init方法会读取容器元素的尺寸如果容器高度是0比如父级div没设高度图表就渲染不出来。我的做法是CSS里明确设置#trendChart { width: 100%; height: 400px; }。5.3 全国天气分布热力图与今日城市排行比较能体现可视化分析功能的是两个页面一个是全国天气分布地图一个是今日城市温度排行。全国天气地图用ECharts的map系列。ECharts本身不自带中国地图的geoJSON数据需要额外引入。处理方式是把china.json的geoJSON放到static目录下然后通过echarts.registerMap(china, geoJSON)注册地图。地图上的城市用散点表示散点大小对应温度值颜色对应温度区间——低温用蓝色高温用红色中间用渐变过渡。这个功能视觉冲击力很强答辩时老师普遍会对这种偏数据分析的页面留下印象。今日城市排行做成了一个横向柱状图。前端拿到的数据按温度降序排列展示Top10城市。这个页面实现起来比趋势图还简单核心就一个bar类型的seriesxAxis用category表示城市名yAxis用value表示温度。不过在真正做的时候最好把城市对应的天气现象图标也显示出来比如晴多云小雨这样信息量更丰富一眼扫过去就能知道全国大致天气状况。前端这块我的体会是不要一开始就把页面做得太花哨。先把基础功能跑通数据能在页面上显示出来然后再逐步加样式。可视化项目最容易出现的问题是图表画完了但数据对不上所以我的调试顺序永远是先用浏览器的Network面板看接口返回的JSON是否正常再去检查图表为什么没渲染对。前端出问题八成是接口数据格式问题剩下两成才是ECharts配置写错了。6. 常见问题排查与排坑实录6.1 数据库连接与中文乱码写这种数据库课设项目踩坑最多的还是数据库。我整理了一份高频问题速查表问题现象可能原因解决方案Access denied for user rootlocalhostMySQL密码错误检查config.py连接串密码Unknown database weather_db数据库未创建先执行CREATE DATABASE weather_db数据库中文全部显示为???表字符集不是utf8建库时用utf8mb4连接串加charsetutf8mb4Cant connect to MySQL serverMySQL服务未启动Windows打开服务管理器启动MySQLmacOS用brew services start mysql端口被占用Address already in use5000端口被其他进程占用lsof -i:5000查看占用进程并kill或换端口字符集问题特别容易忽略。MySQL的默认字符集如果不是utf8mb4插入中文就会变成问号。创建数据库时最好显式指定CREATE DATABASE weather_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后config.py里的连接串也要带上?charsetutf8mb4参数两端一致才不会乱码。6.2 API调用常见报错与限流问题调用天气API时最常遇到的是两类错误401鉴权失败和429请求过多。401通常是API Key配错了检查有没有复制完整、有没有空格。429是超出了免费额度解决方法是程序里加一个简单的请求间隔import time def fetch_weather_with_limit(city_id): time.sleep(1) # 每次请求间隔1秒避免触发限流 return fetch_weather(city_id)另外API Key千万不要提交到Git仓库。课程设计如果上传到GitHub用公开仓库Key被爬走了人家就能白嫖你额度严重的还会被风控封号。建议把Key写在环境变量或者一个不入库的config.local.py里再在.gitignore里排除掉这个文件。6.3 前端图表不显示的三个常见原因ECharts图表显示不出来的问题我统计了一下大部分是下面三种情况容器没有高度。前面提过图表容器必须有明确高度百分比高度在某些嵌套情况下会失效。最稳妥的是直接给px固定值。JS执行顺序问题。如果页面加载时后端还没返回数据图表初始化就可能会拿到空数据。我的做法是用async/await确保数据拿到后再setOption避免拿到空的[]直接渲染。ECharts版本和配置项不匹配。ECharts 4和5有些配置项有差异比如visualMap的某些属性在不同版本表现不一样。如果是从网上抄的配置代码出现不生效优先去官网查一下对应版本的文档。6.4 时区与日期处理的隐藏坑最后说一个容易被忽略的点时区问题。天气API返回的时间字段通常是UTC格式比如2025-01-15T08:00:0008:00如果你直接把这串字符串存进MySQL的DATETIME字段或者在前端直接解析很容易发现日期比实际少了一天或者时间差8小时。我的处理方式是统一在采集层做转换用Python的datetime处理from datetime import datetime, timezone, timedelta def to_local_time(utc_time_str): utc_time datetime.fromisoformat(utc_time_str) local_time utc_time timedelta(hours8) # 北京时间东八区 return local_time.strftime(%Y-%m-%d %H:%M:%S)这个问题的坑在于不是所有API都会返回带时区信息的ISO格式有的API直接给你一个UTC时间戳10位或13位数字。如果用标准的datetime.fromtimestamp转换要考虑系统本身的时区设置。最稳妥的方式是统一用带时区的时间对象处理最后入库时再格式化成字符串。最后的实操经验分享写到这我把自己做完这个项目的几点体会分享一下。第一项目验收看的从来不只是功能还有代码结构。哪怕课程设计只要目录结构清晰、数据库设计合理、注释到位老师印象就会好很多。建议花点时间写README文档把怎么安装依赖、怎么初始化数据库、怎么跑起来写得清清楚楚这份文档在答辩时作用和代码一样大。第二开发过程中养成配合接口调试工具的习惯。项目开发阶段我基本都是用Postman先测接口确认返回JSON正常才去写前端页面。全部用浏览器测会比较低效而且不容易看清返回的原始JSON。你写代码时如果用Flask自带的debug调试配合Postman效率会高很多。第三这个项目其实是个很好的扩展起点。等基础功能做完你可以往这些方向延展加一个定时任务每天凌晨自动抓取天气数据并归档加一个简单的数据分析模块算出本月各城市的平均温差或者升级成天气预报模型。这些扩展不必一次性做完但系统留着这些口子后续想做的时候加模块就行。做项目最怕的是只抄代码不思考。把这套系统的数据流动逻辑吃透你收获的不只是一个天气网站而是对一个完整的Web应用是怎么从需求变成产品这个概念的真实体验。