
每年到了做课设、准备毕设的节点总有人问我“有没有一个项目能把Python、数据库、前端一整套串起来既不太难又能学到东西”我一般会推荐天气可视化分析系统。这个项目很典型后端用Python抓取或调用天气数据清洗后入库再用Flask之类的框架把数据暴露成接口前端用Vue ECharts展示温度曲线、天气分布、城市对比。别看它听起来简单从前到后跑通一遍Python爬虫、数据库设计、后端API、前端组件化开发这些硬技能基本都能摸到门。这套源码加上数据库和文档适合刚学完Python基础、想接触Vue但缺一个完整实战项目的人也适合正在准备毕业设计或课程设计的学生参考。这套系统的核心价值在于“全链路”它不是孤立的爬虫脚本也不是单独画图的前端页面而是把数据采集、存储、接口、可视化四层串成一条线。做完之后你可以看到某个城市一整年的温度走势按月份分析降雨天数甚至对比几个城市的气候差异。接下来我就从技术选型、环境搭建、数据采集、后端开发、前端可视化到常见问题把这套系统完整拆开讲一遍。1. 系统整体设计与技术选型1.1 为什么选Python Vue这套组合做天气可视化分析系统技术栈其实可以有很多选择。有人用Django做全栈模板直接渲染页面有人用Spring Boot React也有人干脆用Jupyter Notebook画图完事。但如果你想让项目的可扩展性、展示效果和上手难度平衡Python Vue前后端分离是我比较推荐的组合。Python的优势在数据采集和处理requests、pandas、BeautifulSoup这些库用起来实在太顺手。天气数据要么来自公开API要么来自网页抓取这两种方式Python都有成熟的解决方案。加上Flask框架很轻写一个JSON接口就是几行代码的事不需要像Django那样搭一堆中间件和配置。对课设和毕设来说Flask完全够用而且代码好解释。Vue这边核心价值在组件化和响应式。天气可视化页面包含城市筛选、图表展示、统计卡片等多个模块用Vue组件拆开每个功能独立维护比原先那种拿jQuery拼接字符串的方式清爽太多。ECharts是百度开源的可视化库和Vue配合做折线图、饼图、柱状图都有现成方案几乎不需要手写SVG。此外Vue的生态成熟vue-router管路由axios发请求vue-cli或Vite管工程化对于刚学前端的人这些工具链本身就是宝贵的学习素材。有人会问那React不也挺好吗React当然也不错但Vue的中文资料更多在课程设计、毕业设计里遇到问题更容易找到答案。Vue的模板语法对小项目更友好不需要把JSX理解得很透就能上手。对于这套系统的体量Vue2或Vue3都可以下面我会分别说明。1.2 系统架构与数据流向这个项目整体分成四层数据从源头到展示每一步都有明确边界。第一层是数据采集层。它负责调用天气API或爬取天气网站拿到原始JSON或HTML然后解析成结构化的天气记录。这一层尤其要注意数据源的稳定性API需要注册key爬虫需要处理反爬。后续我会给出更稳妥的方案。第二层是数据存储层。采用MySQL存储清洗后的天气数据包含城市表、天气记录表、以及可选的空气质量表。数据库设计决定了统计接口的查询效率。如果表结构没设计好按城市、按月份聚合时SQL会写得非常痛苦。第三层是后端API层。Flask提供RESTful接口比如/API/weather/history?city北京days30前端通过GET请求拿到JSON数据。这一层还要处理跨域请求、参数校验和异常返回。第四层是前端展示层。Vue页面通过axios请求后端接口拿到数据后用ECharts渲染。页面上的城市下拉框、日期范围选择、图表联动都是在这一层实现。四个层级的好处是“各司其职”。你想把MySQL换成SQLite只改数据访问层想把Flask换成FastAPI只改接口层前端想加一个雷达图只要后端数据字段足够基本不用动其他层。这种低耦合的架构在面试或答辩时也容易讲清楚。2. 环境准备与依赖安装2.1 Python环境搭建与pip换源先说Python环境。网上很多教程让你直接去官网下载Python安装包然后一路点“下一步”。这个流程本身没错但有两个容易踩坑的节点一是安装时记得勾选“Add Python to PATH”否则后面pip命令会提示“不是内部或外部命令”二是在Windows上建议用python -m pip而不是直接pip避免多版本Python共存时跑错环境。装好Python后最好创建一个虚拟环境不要直接往全局环境里装包。因为Flask、pandas这些库的版本变化很快虚拟环境能把项目依赖隔离起来不会污染系统Python。具体操作如下# 创建虚拟环境 python -m venv venv # 进入虚拟环境Windows venv\Scripts\activate # 进入虚拟环境macOS/Linux source venv/bin/activate接下来安装项目依赖pip install flask flask-cors sqlalchemy pymysql requests pandas如果你的网络环境比较慢pip下载超时是常事。我的建议是一开始就换成国内镜像源一次性生效pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple这样后续装包速度会快很多。装完之后可以用pip list确认所有包都进来了。虚拟环境这一步千万别省我见过很多同学因为包版本冲突项目今天能跑明天就跑不了最后发现是装到全局环境里的包版本被改了。2.2 Vue前端环境搭建与依赖安装前端这边要先装Node.js。Vue的运行依赖Node的npm包管理工具所以官网下载LTS版本安装即可。装好之后建议也把npm源切换到国内镜像npm config set registry https://registry.npmmirror.com接下来创建一个Vue项目。这里分两种情况如果你用的是Vue 2传统方案是通过vue/cli创建如果是Vue 3可以直接用Vite创建。以Vue 2为例npm install -g vue/cli vue create weather-frontend创建过程中会问你选哪种预设一般课程设计选默认的Vue 2即可。进入项目目录后安装我们需要的依赖cd weather-frontend npm install axios echarts element-ui vue-router3注意Vue 2对应的Element UI是element-uiVue 3对应的是element-plus别装混了。Vue 2的vue-router需要用3版本如果直接npm install vue-router可能会装到4.x和Vue 2不兼容。这是新手最容易踩的版本坑。装完依赖后npm run serve启动开发服务器默认端口是8080。很多人在这个阶段卡住八十成是Node版本和vue-cli版本不匹配。我的建议是Node不要装太新的版本LTS版本就够用如果vue命令找不到先npm uninstall -g vue/cli再重新安装。2.3 数据库初始化与表结构设计数据库我选MySQL原因前面说过它结构清晰、聚合函数强适合做天气数据统计。首先要创建一个数据库字符集用utf8mb4不然存emoji或者特殊天气符号时会报错。CREATE DATABASE weather_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE weather_db;天气可视化系统核心是两张表一张存城市信息一张存每日天气记录。城市表可以这样设计字段名类型说明idINT主键自增city_nameVARCHAR(50)城市名称provinceVARCHAR(50)所在省份create_timeDATETIME记录创建时间天气记录表更核心它要支持按城市、按日期范围查询还要支持按月份聚合统计。所以索引和字段类型要提前想好。字段名类型说明idINT主键自增city_idINT关联城市表的iddateDATE日期high_tempFLOAT最高温度low_tempFLOAT最低温度weather_conditionVARCHAR(50)天气现象比如晴、多云、小雨wind_directionVARCHAR(50)风向wind_powerVARCHAR(20)风力等级humidityINT湿度百分比create_timeDATETIME写入时间UNIQUE KEY uk_city_date (city_id, date)城市日期唯一在city_id和date字段上加联合唯一索引是为了防止重复数据。爬虫脚本一旦误跑两次重复插入数据的噩梦就来了。有了这个唯一键插入时可以用INSERT ... ON DUPLICATE KEY UPDATE来处理。3. 数据采集与清洗入库3.1 天气数据源选型做天气可视化第一步是解决数据从哪来。我试过两条路线各有取舍。第一条路线是调用开放天气API。比如和风天气、心知天气这些平台注册后可以拿到免费key每天有一定数量的免费请求额度。它们的返回格式是标准JSON字段包括温度、天气现象、湿度、风力和风向解析起来非常省事。而且API稳定不会突然因为页面改版导致爬虫失效。缺点是免费额度有限而且只能查少量城市和有限的未来几天数据想要历史一整年的数据需要付费或者找其它途径。第二条路线是爬取公开天气网站。中国天气网的“历史天气”页面有各城市的逐日天气数据。用requests请求页面再用BeautifulSoup解析表格能拿到比较完整的气温、天气、风力、风向信息。优点是免费且历史数据完整缺点是网站结构可能会变而且对请求频率比较敏感频繁访问容易被限制。我的建议是如果你的项目是课程设计短期演示用爬虫是可以的如果想稳定展示或者不希望演示当天被反爬拦住就和风天气或心知天气的API再搭配一部分历史数据缓存到MySQL里。这样数据来源稳定解释起来也显得专业。3.2 Python爬虫脚本实现先说API方式。假设你注册了心知天气的免费账号拿到了API key那么获取实时天气就是一个GET请求的事。不同平台的URL格式不同但思路一样。以某个返回JSON的天气接口为例import requests import json def fetch_weather_from_api(city_name): url https://your-api-provider.com/weather params { city: city_name, key: 你的API_KEY, unit: metric } resp requests.get(url, paramsparams, timeout10) if resp.status_code 200: return resp.json() return None如果是爬中国天气网的历史天气代码会稍微复杂一些。要模拟浏览器的请求头否则可能拿不到完整页面。一个典型的请求头至少包含User-Agent和Referer。import requests from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def parse_weather_page(url): resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) # 解析表格行提取日期、高温、低温、天气等字段 rows soup.select(table tr) data [] for row in rows[1:]: cells row.find_all(td) if len(cells) 5: item { date: cells[0].text.strip(), high_temp: cells[1].text.strip().replace(℃, ), low_temp: cells[2].text.strip().replace(℃, ), weather: cells[3].text.strip(), wind: cells[4].text.strip() } data.append(item) return data注意任何爬虫脚本都要控制请求频率建议在循环里加time.sleep(1)或更大间隔。这么做既是对目标网站的尊重也是防止自己的IP被临时限制。3.3 数据清洗与去重入库拿到原始数据后不能直接入库。天气网站的字段格式五花八门温度有时候带着“℃”日期有时候是“2025-04-01周六”这种混着文本的格式湿度可能是空的。这些都需要清洗。我用pandas处理这些脏数据比较顺手。先读成一个DataFrame然后统一做类型转换和缺失值处理import pandas as pd import re def clean_weather_data(raw_rows): df pd.DataFrame(raw_rows) # 去掉温度和日期里的非数字字符 df[high_temp] df[high_temp].astype(str).str.extract(r(-?\d\.?\d*))[0] df[low_temp] df[low_temp].astype(str).str.extract(r(-?\d\.?\d*))[0] # 日期统一转成 yyyy-mm-dd df[date] pd.to_datetime(df[date], format%Y-%m-%d, errorscoerce) # 删除没有日期或没有温度的行 df df.dropna(subset[date, high_temp, low_temp]) # 缺失的湿度可以填0或置空 df[humidity] df.get(humidity, 0) return df入库这一步用pymysql写批量插入效率更高。在关闭自动提交的情况下一次executemany能把几百条记录写进去。import pymysql def insert_weather_data(conn, city_id, df): cursor conn.cursor() sql INSERT INTO weather_record (city_id, date, high_temp, low_temp, weather_condition, wind_direction, wind_power, humidity) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE high_temp VALUES(high_temp), low_temp VALUES(low_temp), weather_condition VALUES(weather_condition) rows [ (city_id, row[date], float(row[high_temp]), float(row[low_temp]), row.get(weather), row.get(wind_direction), row.get(wind_power), int(row.get(humidity))) for _, row in df.iterrows() ] cursor.executemany(sql, rows) conn.commit() cursor.close()这个ON DUPLICATE KEY UPDATE是关键。如果某天数据已经存在它会更新温度区间和天气现象不会新增重复记录。这样定时任务即使反复跑数据也不会乱。4. 后端API开发4.1 Flask应用结构与接口设计后端我用Flask来做主要原因是轻。写一个小型应用一个app.py文件就能起服务不会像大型框架那样有一堆自动生成的项目结构。但为了项目能扩展我建议把路由按蓝图拆分比如api.py里放所有接口。初始化的核心代码长这样from flask import Flask, jsonify, request from flask_cors import CORS app Flask(__name__) CORS(app) app.route(/api/health, methods[GET]) def health(): return jsonify({status: ok}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)CORS是必须加的否则Vue在8080端口访问5000端口的接口时会被浏览器的同源策略拦住。加上flask-cors之后跨域问题就省心多了。接口设计不要等到前端写完再去定。先把前端需要的数据列出来反推接口定义接口路径方法参数返回内容/api/citiesGET无所有城市列表/api/weather/historyGETcity_id、start_date、end_date指定城市的每日天气记录/api/weather/statsGETcity_id、year月度平均气温、降雨天数等统计/api/weather/compareGETcity_ids多个城市同期的温度对比数据这样设计的好处是前端页面每个图表对应一个接口数据边界清晰。比如折线图吃history接口统计卡片吃stats接口城市对比吃compare接口。4.2 参数校验与日期范围处理接口写多了你会发现一半的bug都不是业务逻辑问题而是参数没验透。比如前端可能传了一个不存在的city_id或者start_date大于end_date这些情况后端必须优雅地返回错误信息而不是抛个500。app.route(/api/weather/history, methods[GET]) def weather_history(): city_id request.args.get(city_id) start_date request.args.get(start_date) end_date request.args.get(end_date) if not city_id: return jsonify({error: city_id is required}), 400 try: city_id int(city_id) except ValueError: return jsonify({error: city_id must be an integer}), 400 # 日期解析失败时返回默认范围或报错 try: start parse_date(start_date) end parse_date(end_date) except Exception: return jsonify({error: invalid date format, expected yyyy-mm-dd}), 400 records query_weather_records(city_id, start, end) return jsonify({city_id: city_id, data: records})日期范围的处理有个细节如果前端只传了end_date没传start_date可以默认返回最近30天的数据如果两个都不传就返回该城市最新30天。这样既方便前端调试也避免一次性把整年数据都查出来拖慢页面。4.3 SQL统计与参数计算天气分析的核心统计包括每月平均最高温、平均最低温、每月降雨天数、风向频率。这些统计数据如果写在Python里要把所有记录都查出来再循环计算数据量大时既慢又浪费内存。更优的做法是交给SQL聚合函数。求指定城市某个年份的月度平均温度SQL可以这样写SELECT MONTH(date) as month, ROUND(AVG(high_temp), 1) as avg_high, ROUND(AVG(low_temp), 1) as avg_low, COUNT(*) as record_days FROM weather_record WHERE city_id %s AND YEAR(date) %s GROUP BY MONTH(date) ORDER BY month;这里的GROUP BY对month字段分组正好对应饼图或柱状图的横轴。AVG函数在计算时会忽略NULL所以清洗时留下空值不会影响平均结果。降雨天数怎么算得先定义什么算“降雨”。通常把天气描述里包含“雨”或“雪”的记录都算作降水日。SELECT COUNT(*) as rain_days FROM weather_record WHERE city_id %s AND YEAR(date) %s AND (weather_condition LIKE %雨% OR weather_condition LIKE %雪%);这种统计方式的好处是规则清晰在后端代码里只要拼字符串就行。但要注意字段里的天气现象最好统一格式不要一会儿“小雨”一会儿“小到中雨”尽量按气象预报的口径存储。5. 前端可视化页面实现5.1 Vue项目结构与路由后端接口ready之后前端就可以开工了。我用Vue 2加vue-router来组织页面。src目录下大致是这样的结构src/ api/ weather.js components/ CitySelect.vue TemperatureChart.vue WeatherPie.vue StatCards.vue views/ Dashboard.vue router/ index.js App.vue main.js路由配置非常简单只有一个主页面Dashboard。后面如果想加“历史对比”、“城市管理”等页面新增路由就行。import Vue from vue; import Router from vue-router; import Dashboard from /views/Dashboard.vue; Vue.use(Router); export default new Router({ routes: [ { path: /, name: Dashboard, component: Dashboard } ] });把不同图表拆成独立组件Dashboard只负责放置组件和处理状态。比如TemperatureChart管温度折线图WeatherPie管天气占比饼图StatCards管顶部统计卡片。这样每个组件内部代码量不大出问题也好排查。5.2 axios请求封装axios如果每个组件里都写一遍默认配置代码会非常冗余。我在api目录下建一个weather.js统一封装接口请求。import axios from axios; const request axios.create({ baseURL: http://localhost:5000, timeout: 10000 }); export function getCityList() { return request.get(/api/cities); } export function getWeatherHistory(params) { return request.get(/api/weather/history, { params }); } export function getMonthlyStats(params) { return request.get(/api/weather/stats, { params }); }这里baseURL指向后端地址。如果上线部署可以改成环境变量避免写死。开发阶段直接写localhost:5000没问题但要注意Flask必须允许跨域否则axios请求会被浏览器拦下。5.3 ECharts图表配置要点先拿温度折线图举例。它的核心是把后端返回的日期数组和温度数组映射到ECharts的x轴和y轴。template div refchart classchart/div /template script import * as echarts from echarts; export default { props: { dates: Array, highTemps: Array, lowTemps: Array }, mounted() { this.chart echarts.init(this.$refs.chart); this.renderChart(); }, methods: { renderChart() { this.chart.setOption({ tooltip: { trigger: axis }, legend: { data: [最高温, 最低温] }, xAxis: { type: category, data: this.dates }, yAxis: { type: value, name: 温度(℃) }, series: [ { name: 最高温, type: line, data: this.highTemps }, { name: 最低温, type: line, data: this.lowTemps } ] }); } }, watch: { highTemps() { this.renderChart(); } } }; /script style scoped .chart { width: 100%; height: 400px; } /styleECharts有一个特别常见的问题图表渲染不出来或者只占了页面左上角一个小点。绝大多数原因是图表容器没有高度。div设置了width: 100%但height却是0。所以样式里的height: 400px一定不能少。饼图、柱状图的配置思路也一样。饼图的数据格式是[{name: 晴, value: 120}, {name: 雨, value: 35}]柱状图的data数组和xAxis.category的data一一对应。拿到后端接口的数据后用map和filter转换一下就行。5.4 城市切换与图表联动一个体验好的可视化页面一定不是静态图表而是可以和用户交互。我在Dashboard页面里放了一个城市下拉框切换城市后所有图表重新请求接口更新数据。大体逻辑是export default { data() { return { cityList: [], currentCityId: null, weatherData: {}, statsData: {} }; }, async mounted() { const res await getCityList(); this.cityList res.data.data; this.currentCityId this.cityList[0].id; await this.loadData(); }, methods: { async onCityChange() { await this.loadData(); }, async loadData() { const historyRes await getWeatherHistory({ city_id: this.currentCityId, days: 90 }); this.weatherData historyRes.data.data; const statsRes await getMonthlyStats({ city_id: this.currentCityId, year: 2024 }); this.statsData statsRes.data.data; } } };切换城市时组件接收的新props会触发watch图表自动重新渲染。整个过程不需要刷新页面这就是前后端分离加响应式数据的优势。6. 常见问题排查与避坑实录6.1 环境与启动问题速查表我在接手这类项目时发现很多人不是业务代码不会写而是卡在环境搭建。把最容易出现的问题整理成一张表照着检查能省不少时间问题现象常见原因解决方法pip不是内部或外部命令Python没有添加到PATH重新安装并勾选Add Python to PATH或使用python -m pipnpm install报错npm源不稳定或Node版本不匹配切换npm镜像源将Node换到LTS版本vue命令找不到vue/cli未安装或安装失败npm install -g vue/cli然后重新打开终端MySQL连接报错服务未启动、密码错误或字符集问题检查MySQL服务状态确认pymysql连接参数数据库连接字符串加charsetutf8mb4前端请求接口时报CORS错误后端未开启跨域安装并初始化flask-corsECharts图表空白容器高度为0或数据为空给图表div固定高度检查接口数据是否映射正确这里面最隐蔽的是CORS问题。有时候前端能请求到数据浏览器控制台也打印了响应但页面就是没有渲染很可能是请求被浏览器拦截根本到不了页面代码。打开开发者工具Network看请求状态如果是红色的cors error优先处理后端跨域。6.2 数据获取异常与反爬处理爬虫方式获取天气数据时最常见的报错是请求超时或返回空页面。网站可能因为频繁访问临时限制了你的IP也可能是页面结构改版之前的CSS选择器失效了。如果是请求超时可以通过增加time.sleep和重试机制来缓解。比如循环里每次请求后停1秒连续失败3次就跳过该城市。如果页面结构变了用requests.get请求URL把返回的HTML存成文件手动确认新的表格class或id再调整选择器。我的经验是爬虫代码一定要写日志。每爬取一个城市打印成功条数失败时打印URL和状态码。没有日志的话几万个请求跑下来出问题你根本不知道错在哪。对于这个天气项目我更推荐用免费的天气API作为主要数据来源爬虫作为补充。免费API虽然限制多但胜在稳定不会因为页面改版半夜给你搞挂掉。6.3 前后端联调时的字段不一致问题前后端分离项目最大的隐性坑是字段名约定不一致。Python端习惯用snake_case比如high_temp前端JavaScript习惯用camelCase比如highTemp。如果后端返回high_temp前端写成highTemp页面就能显示undefined。解决的办法有两种一种是后端在序列化时显式转换成camelCase另一种是前端统一约定读取snake_case字段。我个人更推荐第二种因为Python代码里的字段名和数据库列名保持一致少一层转换逻辑。前端拿到数据后可以做一个映射函数把数据转成组件需要的格式。还有一个调试技巧让后端先返回一条固定的测试数据而不是真实数据库数据。这样可以快速验证前端图表有没有接对字段然后再把真实数据接上避免链条太长找不到问题。6.4 关于项目答辩和文档整理的额外建议如果你做这个系统是为了提交课程设计或毕业设计除了把代码跑通一定要把数据库设计文档、接口文档和运行说明写清楚。特别是数据库表之间的关系ER图能画就画。文档里要写明如何初始化数据库、如何启动后端、如何启动前端让别人拿到你的源码后能在十分钟内复现。另外源码里不要写死任何个人信息。数据库密码、API key这些尽量放到配置文件中比如config.py或.env文件。这既是为了安全也是项目工程化的基本素养。7. 完整运行流程与验收清单最后把这套系统的标准运行流程整理一遍。假设你刚从别人那里拿到一份“天气可视化分析系统(源码数据库文档)”的项目压缩包拿到手之后按下面步骤操作基本不会乱。第一步检查目录结构。正常情况下应该有backend、frontend、database、docs四个目录。backend里放着Flask源码frontend里是Vue工程database里是SQL初始化脚本docs里是项目文档和接口说明。如果缺少某个目录启动时会缺东西。第二步初始化数据库。打开MySQL新建一个数据库然后执行database目录下的weather_db.sql脚本。执行成功后用SELECT检查一下weather_record表里有没有数据。如果没有数据先运行backend目录下的爬虫脚本或数据导入脚本把原始数据灌进去。第三步启动后端。进入backend目录创建虚拟环境并安装依赖然后修改config.py里的数据库连接信息确保用户名、密码和数据库名正确。运行python app.py看到“Running on http://127.0.0.1:5000”就说明启动成功。可以先用浏览器访问/api/cities返回JSON表示接口正常。第四步启动前端。进入frontend目录npm install安装依赖再npm run serve。浏览器打开http://localhost:8080页面能看到城市下拉框和图表再选择不同城市确认图表能联动变化。验收时重点检查三个方面数据是否真实准确图表是否全部渲染功能是否完整闭环。比如温度折线图是否随着城市切换而变化统计卡片里的数据是否和接口返回一致。这三个方面都通过项目就算真正跑通了。按照我个人的实际操作经验第一次完整启动这套系统顺畅的话大概需要一上午大部分时间都花在环境配置和依赖安装上。如果你遇到了和上面表格里都不匹配的问题建议优先看后端控制台的报错日志它通常比浏览器里的信息更直接。把每一步的日志保存下来这个排除过程本身就是做项目最有价值的收获。