
简介这是一套面向数据科学初学者与课程实践者的Python气象数据分析实战资源聚焦真实场景下的数据采集、建模与可视化全流程。系统支持多城市历史天气数据自动化爬取集成统计分析、线性回归及逻辑回归预测模块并通过Matplotlib/Seaborn生成温度趋势图、热力图、风力分布等10余类专业图表适用于本科课程设计、毕业设计及科研入门训练。压缩包共18个文件2.32MB含4个核心Python脚本爬虫、分析、回归建模、2个CSV原始与输出数据集、5张已生成的可视化PNG图表、3份带版本备份的Markdown文档及1份实验报告DOCX结构清晰、注释规范便于理解数据流与模块调用关系。目前已有129人学习下载读者可直接运行验证全部功能深入掌握RequestsBeautifulSoup数据获取、Pandas时间序列处理、Scikit-learn建模及图表定制等关键技术环节。 很多人拿到“天气数据爬取与可视化”这样的需求第一反应是赶紧找个天气网站直接开爬然后画两张折线图就算交差。但真正往“系统”两个字上靠的时候你会发现事情远没那么简单——数据源怎么选、请求频率怎么控、数据清洗怎么做、图表怎么布局每一个环节都可能让你卡住半天。这篇文章我就把自己完整跑通的一个基于Python的天气数据爬取与可视化分析系统拿出来拆开讲从数据采集到前端展示所有代码和思路都会讲到适合刚学完Python基础、想做一个能写进简历的实战项目的朋友也适合工作中临时接到数据采集分析任务、想快速上手的人参考。1. 项目整体设计与技术选型1.1 一个天气数据系统到底要做哪些事先说清楚这个项目的边界。很多人把“天气数据爬取”理解成“把网页上的天气信息抓下来”这其实只是最前端的一小步。一个完整的系统至少要包含三个环节数据采集、数据存储与清洗、可视化分析展示。缺了任何一个环节都不能叫“系统”顶多算个脚本。我在设计这个项目时把核心需求拆成了三条。第一条是能稳定地获取指定城市一段时间内的历史天气数据包括日期、最高温、最低温、天气状况、风力风向这些基础字段第二条是采集到的数据不能直接拿来用必须经过清洗和结构化处理因为不同数据源的格式差异很大第三条是能够通过图表直观地看出温度变化趋势、天气分布规律、极端天气出现频率等信息而不是丢给用户一张干巴巴的Excel表。这三个需求分别对应了爬虫模块、数据处理模块和可视化模块。模块之间用标准的数据格式传递——爬虫返回DataFrame数据清洗加工DataFrame可视化读取DataFrame画图。这样每个模块都能独立调试出问题也好定位。1.2 技术栈选型的几个理由我最终选了Requests Pandas Matplotlib这套组合没有上Scrapy也没有用Pyecharts或Plotly。原因很简单这个项目的数据量不大单城市几年历史数据撑死也就几千行用Scrapy这种重型框架属于杀鸡用牛刀而Pyecharts虽然图表好看但它生成的其实是网页交互图在本地脚本里展示反而不如Matplotlib直接。Requests库用来发送HTTP请求拿网页源码配合BeautifulSoup做HTML解析这套组合是Python爬虫的经典方案资料多、坑少、出问题好查。Pandas负责数据清洗和聚合统计它的DataFrame结构做日期索引、分组统计、缺失值处理都非常顺手比纯Python的列表字典操作效率高得多。Matplotlib做静态图表虽然默认样式不太好看但胜在可控性强每个细节都能调整而且导出的图片清晰度高放到报告或者PPT里完全没有问题。如果你以后想做Web化的数据展示完全可以在这个基础上把Matplotlib替换成ECharts或者Plotly但核心的爬虫和数据处理逻辑不用动。这也是我坚持模块化设计的初衷——技术选型要为后续扩展留余地。2. 天气数据爬取层设计与实现2.1 数据源选择选对接口能省80%的功夫爬虫圈有句话叫“你爬得再快不如人家接口给得全”。天气数据这类公开信息很多平台都提供了历史数据查询页面但真正稳定、字段完整、反爬策略不严重的源其实不多。我对比过几个常见渠道最后选择了某天气网站的城市历史天气页面作为数据源因为这个页面的数据结构非常规整每天的气象数据都包在ul标签里一个li对应一天里面依次排着日期、天气状况、最高温度、最低温度、风力风向连拆分的class都给你标好了。选择这个数据源还有一个重要考量——它的URL规则非常简单。城市的ID是固定的数字编号URL格式是“城市ID.html”日期参数直接拼在URL后面。这意味着我们可以通过循环遍历URL来拉取任意时间段的数据不需要模拟点击、不需要处理AJAX异步加载、不需要维护复杂的会话状态。一个普通的GET请求就能拿到全部内容这对于新手来说非常友好也大幅降低了被反爬策略误伤的概率。另一个我在选型时特别看重的点是返回内容的编码。这个网站用的是UTF-8编码省去了GBK转码的麻烦。别看这是个小细节真遇到编码不对的情况你解析出来的中文全是乱码排查起来非常头疼。后面我会专门讲这个坑。2.2 爬虫核心代码请求封装与HTML解析先看请求部分。我不建议直接写成散装的requests.get塞在循环里而是封装成一个类把Headers、超时时间、重试机制都管理起来。这样做的好处是后续如果要加代理、加Cookie、加请求间隔只需要改类内部的方法不用动业务逻辑代码。import requests from bs4 import BeautifulSoup import pandas as pd import time import random class WeatherSpider: def __init__(self, city_id): self.city_id city_id self.base_url https://lishi.tianqi.com/{}.html self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8,en-US;q0.5,en;q0.3, Connection: keep-alive } self.session requests.Session() self.session.headers.update(self.headers) def fetch_page(self, date_str): url self.base_url.format(self.city_id) f?date{date_str} for attempt in range(3): try: resp self.session.get(url, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.text except requests.RequestException as e: print(f第{attempt 1}次请求失败: {e}) time.sleep(2) return None这里有几个细节要重点说明。第一我用了requests.Session而不是直接requests.get因为Session会自动保存Cookie连续请求时服务器更容易把我们当成正常访客。第二超时时间必须设否则某个请求卡住了整个程序都会被拖死。第三重试机制加了三轮每轮失败后休眠2秒这在网络抖动频繁的爬虫场景中非常实用。然后是页面解析。拿到HTML文本之后用BeautifulSoup定位到ul标签再遍历所有的li标签提取每一天的数据。这个页面的HTML结构非常稳定每个li里有六个子节点分别是日期、最高温、最低温、天气、风向和风力。我直接按索引取值然后用strip()去掉多余的空格。def parse_page(self, html_text): if not html_text: return [] soup BeautifulSoup(html_text, html.parser) ul soup.find(ul, class_thrui) if not ul: return [] data_list [] for li in ul.find_all(li): tds [span.text.strip() for span in li.find_all(span)] if len(tds) 6: continue data_list.append({ date: tds[0], high_temp: tds[1].replace(℃, ), low_temp: tds[2].replace(℃, ), weather: tds[3], wind_direction: tds[4], wind_power: tds[5] }) return data_list def fetch_history(self, start_year, end_year): all_data [] for year in range(start_year, end_year 1): for month in range(1, 13): date_str f{year}{month:02d} html self.fetch_page(date_str) if html: month_data self.parse_page(html) all_data.extend(month_data) print(f已抓取 {year}年{month}月共 {len(month_data)} 条数据) time.sleep(random.uniform(1, 2)) return pd.DataFrame(all_data)解析时有一个很重要的处理逻辑温度字段里的“℃”符号必须去掉。因为后续要转成数值类型做计算带着单位字符串就没办法直接用astype(float)转换。这一点很多人第一次写都会忽略等跑到画图那一步才发现数据全是字符串时间序列图根本画不出来。2.3 请求频率控制别把对方服务器搞崩了爬虫写得再好如果请求频率控制不好轻则IP被临时封禁重则给目标站点造成不必要的压力。我在fetch_history里故意加入了time.sleep(random.uniform(1, 2))让每次请求间隔1到2秒的随机时长。这个设计的目的是模拟人的操作节奏——正常人翻网页不可能每秒都在点击服务器端如果看到固定间隔的密集请求很容易通过行为特征识别出是脚本。随机延时的间隔不能太短太短失去了意义也不能太长太长效率太低。我实测下来单城市抓取十年数据也就是120次请求即使间隔2秒总耗时也就不到5分钟。这个时间成本完全可以接受没必要为了省几分钟去冒被封IP的风险。如果你需要抓取多个城市的数据还有一个建议把城市ID列表放外面每抓完一个城市就停止几秒钟再抓下一个这样触发反爬的概率会大大降低。另外务必遵守目标网站的robots协议和版权规定本项目的代码仅用于个人学习研究不要对目标站点发起高频并发请求。3. 数据清洗与预处理从脏数据到干净DataFrame3.1 温标统一与日期标准化爬虫拿到的原始数据其实已经算比较规整了但离“可直接分析”还有一段距离。最典型的问题是温度字段解析后带单位符号“℃”需要清洗掉并转成数值类型日期字段是字符串格式需要转成datetime类型并设为索引——这样后续做按年、按月、按季度的聚合统计才会变得非常方便。我在设计清洗函数时把操作分成了四步第一步处理缺失值第二步类型转换第三步添加辅助列第四步处理异常值。def clean_weather_data(df): if df.empty: return df df df.copy() # 去掉温度中的单位符号并转为数值 df[high_temp] pd.to_numeric(df[high_temp], errorscoerce) df[low_temp] pd.to_numeric(df[low_temp], errorscoerce) # 日期标准化并设为索引 df[date] pd.to_datetime(df[date], format%Y-%m-%d, errorscoerce) df df.dropna(subset[date]) df df.set_index(date).sort_index() # 添加辅助列月份、年份、季节 df[year] df.index.year df[month] df.index.month df[season] df[month].map({12: 冬季, 1: 冬季, 2: 冬季, 3: 春季, 4: 春季, 5: 春季, 6: 夏季, 7: 夏季, 8: 夏季, 9: 秋季, 10: 秋季, 11: 秋季}) # 过滤异常温度 df df[(df[high_temp] -30) (df[high_temp] 50)] df df[(df[low_temp] -50) (df[low_temp] 40)] return dfpd.to_numeric函数里有个errorscoerce参数非常关键它的作用是无法转换的字符串会变成NaN而不是报错。现实中因为网页改版或者数据源临时异常偶尔会混进来一条格式不对的数据如果直接转换整个程序就会崩溃但有了coerce参数后只需要在下一步统一dropna掉就行。这个防御性编程的思路建议大家都养成习惯。3.2 温度相关的扩展特征计算清洗完基础字段后我又做了一些扩展计算其中最常用的是“日均温”和“温差”。日均温我取的是最高温和最低温的平均值虽然是近似值但对于趋势分析已经完全够用。温差是最高温减最低温的差这个指标能反映一天的温度波动幅度对于分析大陆性气候特征很有价值。def add_features(df): df[avg_temp] (df[high_temp] df[low_temp]) / 2.0 df[temp_range] df[high_temp] - df[low_temp] return df这一步看似简单但它能把后续可视化的分析维度从“最高温、最低温”直接扩展到“日均温、温差”图表的解读角度一下多了不少。比如你可以画一张全年日均温的折线图快速看出哪段时间最热、哪段时间最冷也可以画一张温差分布直方图结合当地气候特点判断是海洋性气候还是大陆性气候。这些都是数据清洗阶段“预先埋点”的收益。3.3 缺失值与异常值的处理策略缺失值处理是数据清洗里的重头戏。我抓了五年数据整体完整度很高但偶尔还是会出现个别日期没有数据的情况。我的处理策略是如果缺失占比低于1%直接删除这些行如果缺失占比高于5%说明是某个时间段整体出了问题需要回头检查爬虫日志确认是不是那个月的URL请求失败了。异常值处理则在温度上下界上做了拦截。比如最高温超过50摄氏度或者低于零下30摄氏度这种数据在绝大多数中国城市都不太可能出现大概率是原始页面排版异常导致解析错位的脏数据。直接过滤掉比留着要安全因为它极有可能拉偏后续统计结果。这里有一个经验之谈不要试图在清洗阶段把数据“修得完美”。任何猜测出来的数据都会引入不确定性与其凭空补一个值不如明确标注这个位置为空让下游分析模块自己决定怎么处理。这个原则在真实的数据工程中非常重要。4. 可视化分析与结果呈现4.1 中文字体配置第一个绕不开的坑Matplotlib默认字体是不支持中文的这几乎是所有新手必经的一个坑。不配置字体直接画图图上的中文会变成一个个空心小方块特别难看。解决方案是显式指定一个支持中文的字体比如SimHei或者Microsoft YaHei然后在画图前全局配置好。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False第一行是把默认字体族改成SimHei如果有多个字体Matplotlib会按顺序去找第一个可用的。第二行是解决负号显示问题的——设置了中文字体后坐标轴的负号经常显示成方块需要单独关掉unicode_minus。这个配置必须放在画图代码之前而且建议每个图表脚本都写一次防止别人拿到你的代码在自己的环境里运行时忘记了。我自己的习惯是单独建一个conifg.py文件存放所有全局配置画图脚本里直接import这样每次新建脚本不用重复设置。4.2 图表布局用一个2x2画布集中展示为了把系统做出来我用4张图集中展示分析结果布局为2行2列。这种布局非常适合放在一页报告或者一块大屏上一眼就能看全核心信息。下面是我实际用的画图代码。import matplotlib.dates as mdates def plot_weather_analysis(df): fig, axes plt.subplots(2, 2, figsize(14, 10)) # 图1全年温度变化趋势折线图 ax1 axes[0, 0] monthly df.resample(M).agg({high_temp: mean, low_temp: mean}) ax1.plot(monthly.index, monthly[high_temp], label月均最高温, color#d62728, linewidth2) ax1.plot(monthly.index, monthly[low_temp], label月均最低温, color#1f77b4, linewidth2) ax1.set_title(月均温度变化趋势) ax1.legend() ax1.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) # 图2温度分布箱线图 ax2 axes[0, 1] data_by_year [df[df[year] y][avg_temp].values for y in sorted(df[year].unique())] ax2.boxplot(data_by_year, labelssorted(df[year].unique())) ax2.set_title(各年份日均温分布) ax2.set_xlabel(年份) ax2.set_ylabel(日均温℃) # 图3天气状况占比饼图 ax3 axes[1, 0] weather_counts df[weather].value_counts().head(8) ax3.pie(weather_counts.values, labelsweather_counts.index, autopct%.1f%%, startangle90) ax3.set_title(主要天气状况占比) # 图4温差与气温相关性散点图 ax4 axes[1, 1] ax4.scatter(df[avg_temp], df[temp_range], alpha0.4, s10, color#2ca02c) ax4.set_title(日均温与温差关系) ax4.set_xlabel(日均温℃) ax4.set_ylabel(温差℃) fig.tight_layout() plt.savefig(weather_analysis.png, dpi150) plt.show()图1我用了两条折线分别展示每月平均最高温和平均最低温能直观看出一年里的气温波动曲线。图2改用箱线图每年一箱可以直接对比不同年份的日均温分布情况比如中位数、四分位距、有没有异常低温年份。图3是天气状况的占比饼图我用value_counts统计后只取了前8项避免类别太多导致饼图拥挤。图4是散点图可以观察日均温和温差之间是否存在某种相关性。这里面有几个可视化的细节值得琢磨。第一个是resample(M)的用法它直接把日期索引按月份聚合不需要先手动加月份列再groupby。第二个是boxplot的labels参数必须保持和data_by_year的顺序一致否则标签对不上。第三个是tight_layout()这是防止子图之间标题重叠的关键忘了的话图与图之间会挤得很丑。4.3 统计摘要与季节维度分析除了图表一个合格的分析系统还应该输出文字性的统计摘要。我在主程序里加上了一个简单的统计函数把关键指标打印出来方便直接阅读和存档。def print_summary(df): latest_year df[year].max() data df[df[year] latest_year] print(f{latest_year} 年天气统计摘要) print(f平均最高温: {data[high_temp].mean():.1f} ℃) print(f平均最低温: {data[low_temp].mean():.1f} ℃) print(f极端最高温: {data[high_temp].max():.1f} ℃) print(f极端最低温: {data[low_temp].min():.1f} ℃) print(f温差最大日: {data[temp_range].max():.1f} ℃) print(data.groupby(season)[[avg_temp]].mean().round(1))这一串输出相当于给你的可视化图表配上了解读文字你可以直接引用到报告里。尤其是分组输出季节均温这部分很有说服力。比如你拿到数据后看到夏季均温28度、冬季均温3度就能直接跟图表对应上让结论更有依据。4.4 导出处理结果最后我把清洗后的数据导出成CSV文件这样即使以后爬虫接口变了、数据源改版了这份已经采集好的历史数据还能够继续用于分析和绘图不会因为数据源失效而一夜回到解放前。df.to_csv(weather_data.csv, indexTrue, encodingutf-8-sig)编码这里一定要用utf-8-sig而不是utf-8原因很微妙utf-8-sig会在文件开头写入一个BOM头Excel打开时才不会乱码。直接存utf-8用Excel打开CSV中文全是乱码这不是数据错了而是Excel对无BOM的UTF-8支持不好的老毛病。这个坑我踩过一次所以每次都记得带-sig后缀。5. 常见问题与排查指南5.1 请求报错与反爬识别我在调试过程中遇到过几次请求失败的情况首当其冲的是SSLError。这个问题通常出现在目标网站的SSL证书链不完整或者本机缺少根证书的时候。紧急处理方式是在请求参数里加verifyFalse但这样会有安全隐患不建议长期用。更好的做法是更新本机的CA证书macOS用brew安装ca-certificatesWindows用系统自带的证书管理器更新。如果你发现请求返回了200但解析出来是个空列表那大概率是被反爬拦截了。服务器返回的可能是一个验证页面或者空白页而非正常数据。这时候先手动在浏览器里打开同样的URL确认页面是否正常。如果浏览器正常而代码异常检查一下User-Agent和Headers是否和真实浏览器一致。有时候还需要加上Referer头表示请求是从正常的入口页面跳转过来的。5.2 时间解析与日期索引的坑另一个高频报错是日期解析失败。Pandas的to_datetime对字符串格式非常敏感如果数据源里的日期有两种格式混在一起比如“2023-5-1”和“2023-05-01”不加format参数也能解析出来但加上format参数后反而可能因为格式不匹配报错。我的建议是最开始不要传format让Pandas自动推断等确认数据格式统一后再加上format提升解析速度。如果遇到时区问题导致的偏移可以在pd.to_datetime里加utcTrue参数然后再用tz_convert转换到目标时区。国内天气数据基本都是北京时间不存在跨时区问题但如果你抓的是全球城市数据这个细节就很重要了。5.3 中文乱码问题速查中文乱码这个问题的表象很多我整理了一张速查表供参考场景原因对应解决方式网页源码里中文乱码网页编码不是UTF-8获取resp.encoding后手动指定正确编码CSV导入Excel中文乱码保存时用了utf-8而非utf-8-sig导出时使用encodingutf-8-sigMatplotlib图上中文是方块默认字体不支持中文配置plt.rcParams[font.sans-serif]数据库存储后中文乱码表或字段的字符集不对建表时指定utf8mb4字符集这个表基本覆盖了大多数爬虫项目会遇到的中文编码问题。建议直接收藏遇到问题对着查就行。5.4 定时自动化抓取的一种轻量化方案如果你的数据需要每天更新可以在主程序外部用系统的计划任务来触发。Windows上可以用任务计划程序macOS/Linux上可以用crontab。我的做法是写一个run.py作为总入口里面依次调用爬虫、清洗、可视化和导出四个函数然后让定时任务每天凌晨1点执行一次run.py这样每天早上一睁眼就能看到最新一天的气象分析图。# run.py 总调度脚本 if __name__ __main__: spider WeatherSpider(city_id101010100) # 城市ID按需替换 raw_df spider.fetch_history(2021, 2025) df clean_weather_data(raw_df) df add_features(df) df.to_csv(weather_data.csv, indexTrue, encodingutf-8-sig) plot_weather_analysis(df) print_summary(df)在crontab中配置每天凌晨1点执行日志重定向到文件里方便第二天排查。5.5 数据源失效的应急方案最后再说一个所有爬虫项目都会面临的终极问题——数据源改版或失效。我在这个项目上线后大概半年就遇到过一次目标页面结构调整旧的解析逻辑完全失效。我的应对策略是提前预留一个数据源适配层解析逻辑都放在独立的类或者函数里。这样一旦某个数据源失效只需要新写一个解析函数对接新页面业务逻辑其他部分完全不用改动。另外一个更省心的方案是直接对接国内主流天气API服务商。大部分开放平台都提供历史天气查询接口认证通过后按请求量计费个人学习使用的免费额度通常足够。这样就不需要自己维护爬虫解析逻辑稳定性也高一些。不过API的返回字段通常是JSON格式解析逻辑和网页解析不同需要专门写一个解析函数做字段映射。如果API返回的数据结构和本地DataFrame不一致写一个适配层完成字段重命名即可其余处理流程完全可以复用。跑完整个项目后我的体会是这类系统的难点从来不在某一个单独环节而是你把所有环节串成一个完整闭环的能力。从Requests发请求到BeautifulSoup解析再到Pandas清洗、Matplotlib出图每一步单独拿出来都不复杂但组合在一起就需要对数据流有全局的理解。踩过几次坑之后我最大的建议是——不要等代码全部写完才调试每写完一个模块就立刻跑一遍确认输入输出符合预期再往下走。比如爬虫写完先print几行数据看看字段对不对清洗写完先看一下类型转换后有没有NaN这样定位问题会快得多而且心里始终有数。本文还有配套的精品资源点击获取