
简介本资源是一套基于Python与Flask开发的山东省气象数据全链路分析系统面向气象爱好者、地理信息初学者及Web数据可视化学习者解决实时天气抓取、历史数据存储与多维度图表呈现的一体化实践需求。系统涵盖数据采集requests/BeautifulSoup、清洗存储PandasCSV、后端服务Flask路由与API、前端交互BootstrapjQueryChart.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, headersheaders)使用代理IP池如果单一IP频繁请求很容易被封锁。对于大规模抓取历史数据可以考虑使用付费或免费的代理IP服务并在代码中实现IP轮换。添加延迟Delay在请求之间随机睡眠一段时间模拟人工操作。time.sleep(random.uniform(1, 3))是个简单的办法。处理Cookie和Session有些网站需要维护会话。使用requests.Session()对象可以自动管理Cookie保持登录状态如果需要。解析动态内容如果数据是通过JavaScript动态加载的简单的RequestsBeautifulSoup就无能为力了。这时可以考虑使用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(fTemperature 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_keyTrue) name db.Column(db.String(50), uniqueTrue, nullableFalse) latitude db.Column(db.Float) # 纬度 longitude db.Column(db.Float) # 经度 class RealTimeWeather(db.Model): __tablename__ real_time_weather id db.Column(db.Integer, primary_keyTrue) city_id db.Column(db.Integer, db.ForeignKey(city.id), nullableFalse) 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, defaultdatetime.utcnow) # 抓取时间 # 定义关系方便查询 city db.relationship(City, backrefdb.backref(real_time_records, lazydynamic))4.2 数据去重与更新策略对于实时数据我们可能每小时抓取一次。直接插入会导致大量重复数据。我的策略是唯一性约束为RealTimeWeather表设置(city_id, observation_time)的联合唯一约束。这样尝试插入同一城市同一观测时间的数据时数据库会报错。“Upsert”操作使用SQLAlchemy的db.session.merge()方法或者根据数据库类型使用ON DUPLICATE KEY UPDATEMySQL或INSERT ... ON CONFLICT DO UPDATEPostgreSQL/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(namecity_name).first() if not city: return jsonify({error: City not found}), 404 # 获取该城市最新的一条实时记录 latest_weather RealTimeWeather.query.filter_by(city_idcity.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(funcrun_crawler_job, triggerinterval, minutes30, idweather_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, maxBytes10240, backupCount10) 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 charsetutf-8 title山东天气数据可视化/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script !-- 引入山东地图的JS文件需要额外获取 -- script src/static/js/shandong.js/script /head body div idmapChart stylewidth: 100%; height: 600px;/div div idlineChart stylewidth: 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}?days30) .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进行时区转换和格式化显示。这个项目从构想到实现再到持续运行让我对数据获取、处理、存储和展示的全流程有了更深的体会。技术本身并不高深但将各个环节无缝衔接并稳定运行需要的是细致的思考和持续的打磨。最让我有成就感的时刻不是代码第一次跑通的时候而是系统稳定运行了几个月后我能够随时打开那个仪表盘一眼看清山东全省的天气脉络并能通过历史数据验证自己的一些气候猜想。数据真正变成了触手可及的洞察力。如果你正在开始类似的项目我的建议是从小处着手先实现一个城市、一个功能跑通闭环然后再逐步扩展。遇到问题善用日志和搜索引擎社区的智慧总能给你启发。本文还有配套的精品资源点击获取