简介:本资源是一套基于Python与Flask开发的山东省气象数据全链路分析系统,面向气象爱好者、地理信息初学者及Web数据可视化学习者,解决实时天气抓取、历史数据存储与多维度图表呈现的一体化实践需求。系统涵盖数据采集(requests/BeautifulSoup)、清洗存储(Pandas+CSV)、后端服务(Flask路由与API)、前端交互(Bootstrap+jQuery+Chart.js)及地图热力图(Leaflet)等完整模块,适合作为Web开发与数据分析融合项目的实战参考。压缩包共140个文件,含35个CSV历史气象数据样本、31张可视化效果图(JPG/PNG)、19个JS交互脚本、12个CSS样式文件及4个核心Python后端源码,整体3.26MB,轻量易部署。已有120人学习下载,提供开箱即用的本地运行环境、响应式前端界面、清晰的目录分层(static/css/js、templates、data、app.py)以及配套说明文档,助读者快速掌握从爬虫到可视化的全流程实现逻辑。
1. 项目缘起:为什么我们需要一个自建的天气数据系统?
最近在做一个与区域气候分析相关的个人项目,需要用到山东省内多个城市的历史和实时天气数据。一开始,我尝试了各种公开的天气API,但很快就遇到了几个绕不开的痛点:免费API有严格的调用频率限制,历史数据查询要么收费要么不提供;而一些聚合网站的数据虽然全面,但无法满足我进行本地化存储和深度自定义分析的需求。更重要的是,我希望数据能按我的逻辑结构化存储,并能随时进行多维度的可视化对比,比如对比济南和青岛过去一年的湿度变化趋势,或者看看沿海城市和内陆城市在风速上的差异。这些需求,靠现成的工具很难一站式解决。
于是,我决定自己动手,用Python和Flask搭建一个专属于我的“山东省天气数据爬取及可视化分析系统”。这个系统的核心目标很明确:自动化、持续化地抓取山东省各地市的实时与历史天气数据,并将其存入本地数据库,最后通过一个Web界面,以地图、图表等多种形式直观地展示和分析这些数据。听起来像是一个简单的爬虫加图表项目,但真正做下来,从数据源的稳定性、反爬策略应对、数据库设计优化,到前端可视化组件的灵活调用,每一步都有不少值得分享的细节和踩过的坑。如果你也正打算做一个类似的数据聚合与分析项目,或者想深入学习Flask全栈开发和数据可视化,那么我接下来的分享或许能给你一些直接的参考。
2. 系统架构设计与技术选型思考
在动手写代码之前,合理的架构设计是项目成功的基石。这个系统虽然不复杂,但清晰地划分模块能让后续的开发、调试和维护都轻松很多。我的整体思路是采用典型的前后端分离模式,但这里的前后端分离并非指完全独立的服务,而是指在同一个Flask应用内,逻辑清晰的分层。
2.1 后端核心:Flask + 任务调度 + 数据存储
后端是整个系统的大脑,负责数据抓取、处理和提供API。我选择了以下技术栈:
- Flask框架:作为轻量级的Web框架,Flask足够灵活,没有过多的预设约束,非常适合快速构建这种中小型数据应用。相比于Django的“大而全”,Flask的“微内核”特性让我可以按需引入扩展,控制感更强。
- APScheduler:用于定时任务调度。天气数据需要定时更新(比如每小时抓取一次实时数据),APScheduler可以轻松集成到Flask应用中,实现后台的定时爬取任务,无需依赖操作系统的crontab,部署更简单。
- SQLAlchemy + SQLite/MySQL:作为ORM(对象关系映射)工具,SQLAlchemy让我能用Python类来定义数据表,操作数据库就像操作Python对象一样自然。初期开发和小型部署,使用SQLite足够轻便;如果数据量增长迅速,可以无缝切换到MySQL或PostgreSQL。
- Requests + BeautifulSoup4/PyQuery:这是爬虫的“左右手”。Requests库负责发送HTTP请求,获取网页源码或API的JSON响应。对于需要解析HTML页面的数据源,BeautifulSoup4是经典选择,而PyQuery的jQuery式语法有时写起来更顺手。选择哪个取决于目标网站的结构和个人习惯。
注意:技术选型没有绝对的好坏。选择Flask是因为它轻量、灵活,适合快速迭代。如果你对Django更熟悉,用它来构建同样可行,只是项目结构会有所不同。
2.2 前端可视化:ECharts 与 地图组件
前端的目标是将枯燥的数据变得直观。我几乎没怎么犹豫就选择了ECharts。原因有三点:一是功能极其强大,折线图、柱状图、热力图等应有尽有;二是文档和社区非常成熟,遇到问题基本都能找到解决方案;三是它支持地图可视化,这对于展示山东省内不同地市的天气数据分布至关重要。
为了在Flask中集成ECharts,我只需要在前端HTML中引入ECharts的JS库,然后通过Flask路由提供整理好的JSON数据,最后用JavaScript调用ECharts的API进行渲染即可。这种模式解耦了后端数据处理和前端图表渲染,非常清晰。
2.3 数据流设计
整个系统的数据流可以概括为以下几步:
- 定时触发:APScheduler定时器触发爬虫任务。
- 数据抓取:爬虫模块访问目标数据源(可能是多个气象网站或API),获取原始HTML或JSON数据。
- 数据解析与清洗:使用解析库提取出需要的字段(城市、温度、湿度、风速、降水量、观测时间等),并进行清洗(处理缺失值、统一单位)。
- 数据存储:将清洗后的数据通过SQLAlchemy模型存入数据库。这里设计了两张核心表:
real_time_weather(实时数据)和history_weather(历史数据)。 - API提供:Flask定义相应的路由(如
/api/real_time/<city_name>),当浏览器请求时,从数据库查询数据并返回JSON格式。 - 可视化渲染:前端页面通过Ajax请求调用上述API,获取数据后,调用ECharts生成图表。
这个流程形成了一个从数据采集到展示的完整闭环,并且每个环节都可以独立优化。
3. 爬虫实战:稳定获取天气数据的策略与陷阱
爬虫是系统的数据源头,也是最容易出问题的环节。我的目标是抓取山东省所有地级市的实时数据和历史数据。
3.1 数据源的选择与评估
寻找可靠、稳定且对爬虫相对友好的数据源是第一步。我评估了几个方向:
- 中国天气网等官方渠道:数据权威,但反爬机制严格,页面结构可能频繁变动。
- 心知天气、和风天气等商业API:数据质量高、稳定,但有免费额度限制,超出需付费。
- 一些聚合类天气网站:可能通过反向代理多个源,但法律风险和数据准确性需要仔细甄别。
我的策略是“混合源”:对于实时数据,我选择了一个反爬不那么严厉的聚合网站作为主源,并设置合理的请求间隔(如每10分钟抓取一次所有城市)。同时,我准备了一个备用源(另一个网站),当主源访问失败时自动切换。对于历史数据,由于一次性请求量大,我找到了一个提供历史天气查询的网站,通过构造日期和城市参数的URL进行批量抓取。
3.2 应对反爬虫的常见技巧
即使目标网站没有特别复杂的反爬,一些基本的策略也能大大提高成功率:
- 设置请求头(Headers):模拟真实浏览器的User-Agent是必须的。我通常会轮换几个常见的浏览器UA字符串。
headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } response = requests.get(url, headers=headers) - 使用代理IP池:如果单一IP频繁请求,很容易被封锁。对于大规模抓取历史数据,可以考虑使用付费或免费的代理IP服务,并在代码中实现IP轮换。
- 添加延迟(Delay):在请求之间随机睡眠一段时间,模拟人工操作。
time.sleep(random.uniform(1, 3))是个简单的办法。 - 处理Cookie和Session:有些网站需要维护会话。使用
requests.Session()对象可以自动管理Cookie,保持登录状态(如果需要)。 - 解析动态内容:如果数据是通过JavaScript动态加载的,简单的Requests+BeautifulSoup就无能为力了。这时可以考虑使用Selenium或Pyppeteer这类自动化测试工具来模拟浏览器行为,获取渲染后的页面内容。不过这会大大增加资源消耗和复杂度,非必要不使用。
3.3 数据解析与异常处理
拿到网页响应后,就需要精准地“挖出”我们需要的数据。这里以BeautifulSoup为例:
from bs4 import BeautifulSoup import re soup = BeautifulSoup(html_content, 'html.parser') # 假设温度在一个class为'temp'的span标签里 temp_element = soup.find('span', class_='temp') if temp_element: # 使用正则表达式提取数字部分,处理“12℃”这样的字符串 temp_text = temp_element.get_text() match = re.search(r'(-?\d+)', temp_text) temperature = int(match.group(1)) if match else None else: temperature = None # 记录日志:未找到温度元素,可能页面结构已变化 logging.warning(f"Temperature element not found for {city}.")异常处理至关重要:网络超时、页面结构变化、数据缺失等情况每天都在发生。我的代码里充满了try...except块,确保单个城市的抓取失败不会导致整个任务崩溃,并将错误信息详细记录到日志文件中,方便后续排查。
4. 数据存储:设计一个高效且可扩展的数据库
数据抓下来后,如何存、怎么存,直接影响后续分析的效率和便捷性。
4.1 数据库模型设计
我使用SQLAlchemy的声明式基类来定义模型。核心表结构如下:
- 城市表 (City):存储山东省地市的基本信息,如城市ID、名称、拼音、经纬度(用于地图定位)。这是一个维度表。
- 实时天气表 (RealTimeWeather):存储最新的天气数据。字段包括:记录ID、关联的城市ID、温度、湿度、风速、风向、降水量、天气状况(晴/雨等)、观测时间、数据抓取时间。这里“观测时间”是数据源提供的天气记录时间,“抓取时间”是我们系统抓取该条记录的时间,两者不同。
- 历史天气表 (HistoryWeather):存储过去的气象数据。字段与实时表类似,但通常以“日期”为主键的一部分(结合城市ID),包含最高温、最低温、平均温度等更丰富的历史指标。
使用SQLAlchemy定义模型示例:
from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() class City(db.Model): __tablename__ = 'city' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), unique=True, nullable=False) latitude = db.Column(db.Float) # 纬度 longitude = db.Column(db.Float) # 经度 class RealTimeWeather(db.Model): __tablename__ = 'real_time_weather' id = db.Column(db.Integer, primary_key=True) city_id = db.Column(db.Integer, db.ForeignKey('city.id'), nullable=False) temperature = db.Column(db.Float) # 温度 humidity = db.Column(db.Integer) # 湿度百分比 wind_speed = db.Column(db.Float) # 风速 weather = db.Column(db.String(20)) # 天气状况 observation_time = db.Column(db.DateTime) # 数据观测时间 crawl_time = db.Column(db.DateTime, default=datetime.utcnow) # 抓取时间 # 定义关系,方便查询 city = db.relationship('City', backref=db.backref('real_time_records', lazy='dynamic'))4.2 数据去重与更新策略
对于实时数据,我们可能每小时抓取一次。直接插入会导致大量重复数据。我的策略是:
- 唯一性约束:为
RealTimeWeather表设置(city_id, observation_time)的联合唯一约束。这样尝试插入同一城市同一观测时间的数据时,数据库会报错。 - “Upsert”操作:使用SQLAlchemy的
db.session.merge()方法,或者根据数据库类型使用ON DUPLICATE KEY UPDATE(MySQL)或INSERT ... ON CONFLICT DO UPDATE(PostgreSQL/SQLite 3.24+)语句。这样如果记录存在就更新,不存在则插入。
对于历史数据,通常是按天抓取,可以采用类似策略,以(city_id, date)作为唯一标识。
4.3 数据库性能初步优化
当历史数据积累到几十万条时,查询可能会变慢。
- 索引:在经常用于查询条件的字段上创建索引,能极大提升速度。例如,在
HistoryWeather表的city_id和date字段上创建索引。# 虽然SQLAlchemy模型定义中也可以指定索引,但有时在数据库层面直接创建更直观 # CREATE INDEX idx_history_city_date ON history_weather (city_id, date); - 归档旧数据:实时数据表可能只需要保留最近一周或一个月的数据供实时展示。可以定期将更早的数据转移到历史表或备份存储中,保持实时表的轻量。
5. Flask后端构建:从路由设计到API实现
Flask后端负责协调爬虫任务、处理数据并提供RESTful API。
5.1 应用工厂模式与蓝图
为了保持代码结构清晰,我采用了应用工厂模式,并使用蓝图来模块化路由。
app/__init__.py:创建Flask应用,加载配置,初始化扩展(SQLAlchemy, APScheduler等)。app/models.py:集中定义所有数据库模型。app/crawler/:爬虫模块目录,包含不同数据源的爬虫脚本和调度逻辑。app/api/:API蓝图。在这里定义所有数据接口。__init__.py:创建蓝图api_bp = Blueprint('api', __name__)。weather.py:在蓝图中定义天气相关的路由。
# app/api/weather.py from flask import jsonify from . import api_bp from app.models import db, City, RealTimeWeather from sqlalchemy import desc @api_bp.route('/real_time/<city_name>') def get_real_time_weather(city_name): """获取指定城市的实时天气""" city = City.query.filter_by(name=city_name).first() if not city: return jsonify({'error': 'City not found'}), 404 # 获取该城市最新的一条实时记录 latest_weather = RealTimeWeather.query.filter_by(city_id=city.id).order_by(desc(RealTimeWeather.observation_time)).first() if not latest_weather: return jsonify({'error': 'No data available'}), 404 data = { 'city': city.name, 'temperature': latest_weather.temperature, 'humidity': latest_weather.humidity, 'wind_speed': latest_weather.wind_speed, 'weather': latest_weather.weather, 'observation_time': latest_weather.observation_time.isoformat() } return jsonify(data) @api_bp.route('/history/<city_name>') def get_history_weather(city_name): """获取指定城市的历史天气数据(例如最近30天)""" # 实现逻辑:查询HistoryWeather表,按日期排序,返回JSON列表 pass5.2 集成APScheduler实现定时爬取
在应用工厂函数中初始化调度器,并添加定时任务。
# app/__init__.py from apscheduler.schedulers.background import BackgroundScheduler from app.crawler.main_crawler import run_crawler_job scheduler = BackgroundScheduler() def create_app(config_class): app = Flask(__name__) app.config.from_object(config_class) # ... 其他初始化 ... # 初始化调度器(确保只初始化一次) if not scheduler.running: # 添加任务:每30分钟执行一次爬虫任务 scheduler.add_job(func=run_crawler_job, trigger='interval', minutes=30, id='weather_crawl_job') scheduler.start() # 重要:在应用退出时关闭调度器 atexit.register(lambda: scheduler.shutdown()) return apprun_crawler_job函数封装了抓取所有城市实时天气数据的逻辑。将其放在后台线程中执行,不会阻塞Web主线程。
5.3 错误处理与日志记录
一个健壮的系统必须有完善的错误处理和日志。
- 全局错误处理器:使用Flask的
@app.errorhandler装饰器来捕获404、500等错误,返回统一的JSON格式错误信息。 - 日志配置:在配置中设置日志级别和输出格式,将爬虫的运行状态、错误信息、API访问记录等分别输出到不同的日志文件。
import logging from logging.handlers import RotatingFileHandler # 在create_app中配置 if not app.debug: file_handler = RotatingFileHandler('logs/weather_system.log', maxBytes=10240, backupCount=10) file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' )) file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO) app.logger.info('Weather system startup')
6. 前端可视化:用ECharts打造动态仪表盘
前端页面是系统的门面,目标是将数据直观、美观地呈现出来。我构建了一个简单的仪表盘,包含地图概览、趋势图表和详情面板。
6.1 集成ECharts与获取数据
首先,在HTML模板中引入ECharts库(可以使用CDN)。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>山东天气数据可视化</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <!-- 引入山东地图的JS文件(需要额外获取) --> <script src="/static/js/shandong.js"></script> </head> <body> <div id="mapChart" style="width: 100%; height: 600px;"></div> <div id="lineChart" style="width: 100%; height: 400px;"></div> <script src="/static/js/main.js"></script> </body> </html>在main.js中,我们使用Fetch API或jQuery的$.ajax来调用Flask后端提供的接口,获取数据。
// 获取所有城市的实时数据用于地图渲染 fetch('/api/real_time_all') .then(response => response.json()) .then(data => { initMapChart(data); }); // 获取特定城市的历史温度数据用于折线图 function loadHistoryData(cityName) { fetch(`/api/history/${cityName}?days=30`) .then(response => response.json()) .then(data => { initLineChart(data); }); }6.2 绘制山东省地图热力图
地图热力图非常适合展示全省各城市某一气象要素(如温度)的分布情况。
- 准备地理数据:ECharts需要地理坐标数据。我找到了山东省各地市的GeoJSON数据,并将其转换为ECharts可注册的格式,保存为
shandong.js。 - 初始化图表并注册地图:
function initMapChart(weatherData) { var chartDom = document.getElementById('mapChart'); var myChart = echarts.init(chartDom); // 注册地图 echarts.registerMap('Shandong', shandongGeoJSON); var option = { title: { text: '山东省实时温度分布' }, tooltip: { trigger: 'item', formatter: function(params) { return `${params.name}<br/>温度: ${params.value}°C`; } }, visualMap: { min: -10, // 根据数据范围调整 max: 40, text: ['高温', '低温'], calculable: true, inRange: { color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026'] } }, series: [{ name: '温度', type: 'map', map: 'Shandong', data: weatherData.map(item => ({ name: item.city, value: item.temperature })), label: { show: true } }] }; myChart.setOption(option); }weatherData是从/api/real_time_all接口获取的数组,格式如[{city: '济南', temperature: 25}, ...]。
6.3 绘制多维度趋势图表
除了地图,折线图和柱状图是展示时间趋势和对比的利器。我通常在一个页面内放置多个图表容器。
- 折线图:用于展示单个城市过去一段时间(如30天)的温度、湿度变化趋势。ECharts的
xAxis设置为日期,yAxis为数值,series里包含多条线(最高温、最低温、平均湿度等)。 - 柱状图:用于对比不同城市在同一气象要素上的差异,比如对比今天山东省内所有城市的降水量。这里
xAxis是城市名称,yAxis是降水量,series类型设为bar。
交互联动:一个提升体验的技巧是让图表之间产生联动。例如,点击地图上的某个城市,下方的折线图就动态加载并显示该城市的历史趋势。这只需要在地图系列的emphasis或select事件中,调用loadHistoryData(cityName)函数即可实现。
7. 部署上线与持续运维
开发完成后,我们需要让系统在服务器上7x24小时稳定运行。
7.1 部署选项:传统服务器 vs. 云服务
- 传统VPS/云服务器:我选择了一台Linux服务器(如Ubuntu)。部署步骤包括:
- 将代码上传到服务器(使用Git)。
- 创建Python虚拟环境并安装依赖:
pip install -r requirements.txt。 - 配置生产环境变量(数据库连接、密钥等),通常使用
.env文件或系统环境变量。 - 使用Gunicorn作为WSGI服务器来运行Flask应用,它比Flask自带的开发服务器更稳定、性能更强。
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()" - 使用Nginx作为反向代理,处理静态文件、SSL加密(HTTPS)和负载均衡(如果需要)。
- 使用Supervisor或systemd来管理Gunicorn进程,确保应用崩溃后能自动重启。
- 云平台/容器化:也可以使用Docker将应用容器化,然后部署到云平台(如阿里云、腾讯云的容器服务)。这能实现更好的环境一致性和可扩展性。编写
Dockerfile和docker-compose.yml文件是关键。
7.2 监控与日志查看
系统上线后,监控其健康状况是必要的。
- 进程监控:Supervisor本身提供了进程状态监控。也可以使用
ps、top等命令查看资源占用。 - 日志监控:定期查看
logs/weather_system.log文件,检查是否有爬虫失败、数据库连接错误等异常信息。可以使用tail -f命令实时跟踪日志。 - 数据质量监控:编写一个简单的脚本,定期检查数据库中最新的数据时间戳,如果某个城市的数据长时间未更新,则发送告警邮件或通知。
7.3 数据备份策略
数据是无价的。我设置了简单的定时任务(使用服务器的crontab),定期对SQLite数据库文件进行备份,并压缩传输到另一台机器或云存储中。
# 每天凌晨2点备份 0 2 * * * /usr/bin/cp /path/to/your/weather.db /path/to/backup/weather_$(date +\%Y\%m\%d).db8. 踩坑实录与经验总结
回顾整个项目,有几个坑印象特别深刻,分享出来希望能帮你避过。
坑一:数据源的结构性变动。这是爬虫项目最大的敌人。有一次,目标网站进行了一次前端改版,整个HTML的class名称全变了,导致爬虫解析全部失败。教训:不要将解析逻辑写得太死。尽量使用多个层级、标签组合来定位元素,而不仅仅依赖单一的class或id。同时,将解析规则(如CSS选择器)提取到配置文件中,一旦变化,只需修改配置文件,无需改动核心代码。建立有效的监控告警,能在第一时间发现解析失败。
坑二:数据库连接未正确管理。在定时爬虫任务中,如果每次抓取都新建数据库连接而不关闭,很快会导致数据库连接耗尽。解决方案:使用Flask-SQLAlchemy提供的db.session,并在每次任务结束时显式地db.session.remove(),确保将连接放回连接池。或者,对于独立于Flask应用上下文的脚本(如单独的爬虫脚本),需要自己创建应用上下文来使用数据库。
坑三:前端地图数据加载慢。最初我将包含所有城市边界细节的、非常精细的GeoJSON数据直接用于ECharts地图,导致页面加载缓慢。优化:对GeoJSON数据进行简化,使用地图工具(如mapshaper.org)减少多边形节点的数量,在视觉效果损失不大的前提下,显著减小了文件体积,提升了加载速度。
坑四:忽略时区处理。数据源的时间、服务器时间、数据库存储时间(UTC)、前端显示时间,如果不统一,会造成时间显示的混乱。最佳实践:在数据库中统一使用UTC时间存储。在后端API返回数据时,根据前端需求转换为特定的时区(如东八区)。在前端JavaScript中,也可以使用moment.js或原生API进行时区转换和格式化显示。
这个项目从构想到实现,再到持续运行,让我对数据获取、处理、存储和展示的全流程有了更深的体会。技术本身并不高深,但将各个环节无缝衔接并稳定运行,需要的是细致的思考和持续的打磨。最让我有成就感的时刻,不是代码第一次跑通的时候,而是系统稳定运行了几个月后,我能够随时打开那个仪表盘,一眼看清山东全省的天气脉络,并能通过历史数据验证自己的一些气候猜想。数据真正变成了触手可及的洞察力。如果你正在开始类似的项目,我的建议是:从小处着手,先实现一个城市、一个功能,跑通闭环,然后再逐步扩展。遇到问题,善用日志和搜索引擎,社区的智慧总能给你启发。
本文还有配套的精品资源,点击获取