☰
Python天气数据爬取与可视化分析系统实战详解
2026/10/25 14:51:44 网站建设 项目流程

简介:这是一套面向数据科学初学者与课程实践者的Python气象数据分析实战资源,聚焦真实场景下的数据采集、建模与可视化全流程。系统支持多城市历史天气数据自动化爬取,集成统计分析、线性回归及逻辑回归预测模块,并通过Matplotlib/Seaborn生成温度趋势图、热力图、风力分布等10余类专业图表,适用于本科课程设计、毕业设计及科研入门训练。压缩包共18个文件(2.32MB),含4个核心Python脚本(爬虫、分析、回归建模)、2个CSV原始与输出数据集、5张已生成的可视化PNG图表、3份带版本备份的Markdown文档及1份实验报告DOCX,结构清晰、注释规范,便于理解数据流与模块调用关系。目前已有129人学习下载,读者可直接运行验证全部功能,深入掌握Requests+BeautifulSoup数据获取、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/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.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, timeout=10) 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. 数据清洗与预处理:从脏数据到干净DataFrame

3.1 温标统一与日期标准化

爬虫拿到的原始数据其实已经算比较规整了,但离“可直接分析”还有一段距离。最典型的问题是温度字段解析后带单位符号“℃”,需要清洗掉并转成数值类型;日期字段是字符串格式,需要转成datetime类型并设为索引——这样后续做按年、按月、按季度的聚合统计才会变得非常方便。

我在设计清洗函数时把操作分成了四步:第一步处理缺失值,第二步类型转换,第三步添加辅助列,第四步处理异常值。

def clean_weather_data(df): if df.empty: return df df = df.copy() # 去掉温度中的单位符号并转为数值 df["high_temp"] = pd.to_numeric(df["high_temp"], errors="coerce") df["low_temp"] = pd.to_numeric(df["low_temp"], errors="coerce") # 日期标准化并设为索引 df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d", errors="coerce") 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 df

pd.to_numeric函数里有个errors="coerce"参数非常关键,它的作用是无法转换的字符串会变成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", linewidth=2) ax1.plot(monthly.index, monthly["low_temp"], label="月均最低温", color="#1f77b4", linewidth=2) 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, labels=sorted(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, labels=weather_counts.index, autopct="%.1f%%", startangle=90) ax3.set_title("主要天气状况占比") # 图4:温差与气温相关性散点图 ax4 = axes[1, 1] ax4.scatter(df["avg_temp"], df["temp_range"], alpha=0.4, s=10, color="#2ca02c") ax4.set_title("日均温与温差关系") ax4.set_xlabel("日均温(℃)") ax4.set_ylabel("温差(℃)") fig.tight_layout() plt.savefig("weather_analysis.png", dpi=150) 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", index=True, encoding="utf-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证书链不完整或者本机缺少根证书的时候。紧急处理方式是在请求参数里加verify=False,但这样会有安全隐患,不建议长期用。更好的做法是更新本机的CA证书:macOS用brew安装ca-certificates,Windows用系统自带的证书管理器更新。

如果你发现请求返回了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里加utc=True参数,然后再用tz_convert转换到目标时区。国内天气数据基本都是北京时间,不存在跨时区问题,但如果你抓的是全球城市数据,这个细节就很重要了。

5.3 中文乱码问题速查

中文乱码这个问题的表象很多,我整理了一张速查表供参考:

场景原因对应解决方式
网页源码里中文乱码网页编码不是UTF-8获取resp.encoding后手动指定正确编码
CSV导入Excel中文乱码保存时用了utf-8而非utf-8-sig导出时使用encoding="utf-8-sig"
Matplotlib图上中文是方块默认字体不支持中文配置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_id="101010100") # 城市ID按需替换 raw_df = spider.fetch_history(2021, 2025) df = clean_weather_data(raw_df) df = add_features(df) df.to_csv("weather_data.csv", index=True, encoding="utf-8-sig") plot_weather_analysis(df) print_summary(df)

在crontab中配置每天凌晨1点执行,日志重定向到文件里,方便第二天排查。

5.5 数据源失效的应急方案

最后再说一个所有爬虫项目都会面临的终极问题——数据源改版或失效。我在这个项目上线后大概半年,就遇到过一次目标页面结构调整,旧的解析逻辑完全失效。我的应对策略是提前预留一个数据源适配层,解析逻辑都放在独立的类或者函数里。这样一旦某个数据源失效,只需要新写一个解析函数对接新页面,业务逻辑其他部分完全不用改动。

另外一个更省心的方案是直接对接国内主流天气API服务商。大部分开放平台都提供历史天气查询接口,认证通过后按请求量计费,个人学习使用的免费额度通常足够。这样就不需要自己维护爬虫解析逻辑,稳定性也高一些。不过API的返回字段通常是JSON格式,解析逻辑和网页解析不同,需要专门写一个解析函数做字段映射。如果API返回的数据结构和本地DataFrame不一致,写一个适配层完成字段重命名即可,其余处理流程完全可以复用。

跑完整个项目后我的体会是,这类系统的难点从来不在某一个单独环节,而是你把所有环节串成一个完整闭环的能力。从Requests发请求,到BeautifulSoup解析,再到Pandas清洗、Matplotlib出图,每一步单独拿出来都不复杂,但组合在一起就需要对数据流有全局的理解。踩过几次坑之后,我最大的建议是——不要等代码全部写完才调试,每写完一个模块就立刻跑一遍,确认输入输出符合预期再往下走。比如爬虫写完先print几行数据看看字段对不对,清洗写完先看一下类型转换后有没有NaN,这样定位问题会快得多,而且心里始终有数。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询