news 2026/9/9 18:11:49

用Python分析北京10年天气:数据抓取到可视化的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Python分析北京10年天气:数据抓取到可视化的完整实战

1. 数据源的选型逻辑:公开API、网页抓取和历史库的取舍分析

先说结论:想做"北京最近10年全年天气变化曲线",最核心的问题其实不是画图,而是数据从哪来。很多人在这一步就卡住了,然后去翻了十二个网站、复制粘贴了三千行数据,最后画出来的曲线还缺牙少齿。我一开始也走过弯路,所以把数据源选型放在最前面讲。

市面上能拿到历史天气数据的途径大致有三类:

第一类:通过各类天气平台的公开页面直接抓取。好处是无需注册、无需密钥,数据可视化程度高、字段直观;坏处是页面结构经常改,反爬手段不一,数据密度和年份覆盖不一定满足10年的需求。比如有的站点只保留近3年数据,或者只提供逐月汇总,拉不到逐日数据,这就会严重限制我们对"全年天气变化曲线"的还原精度。

第二类:调用专业气象数据API。好处是数据结构化好、字段齐全、更新稳定,坏处是大部分都需要企业认证或付费,个人开发者签下免费额度的门槛不低。对于一些想快速出图、不想花太多时间在反爬上的朋友来说,这是最省心的选择,但如果你是第一次尝试,注册、鉴权、请求配额这些环节反而可能比写爬虫本身更浪费时间。

第三类:使用开源历史气象数据集。比如一些数据社区整理好的全球站点逐日数据,好处是下载即用、不需要考虑断线和反爬,坏处是数据更新滞后,而且对于"北京"这种站点,不同数据集的粒度、单位、时区口径可能不一致,处理起来需要额外校验。

我在实际项目中采用的是**"以公开网页抓取为主、以历史气象数据平台的格式要求为辅"**的组合方案。理由也很朴素:

  1. 这张曲线的核心是趋势呈现,需要的是连续10年的逐日数据,而不是某一天的精确温度,所以网页端的显示精度完全够用;
  2. 公开网页的数据字段相对稳定,常规字段(日期、最高温、最低温、天气现象、风向风力)都能拿到,足够做完整的年度曲线分析;
  3. 不依赖第三方密钥,整个流程可复现,换一个城市也能直接改参数跑通。

选型时还有一个关键考量:数据频率。做"全年天气变化曲线",最低要求是逐日数据,最佳状态是能拿到逐小时数据。逐小时数据量更大,但能支撑更细的分析,比如昼夜温差曲线、极端天气过程的温度演变。如果只想画"全年气温走势",逐日数据就够了。我在实践里选择的是逐日最高温/最低温/天气现象三件套,理由是这个粒度既能画清趋势,又不需要处理太多异常值。

提示:动手之前先估算数据规模。10年 × 365天 ≈ 3650条记录,这对Excel、pandas这些常规工具来说是小菜一碟,但千万不要用"每一年手动复制"的方式去收集,除非你享受重复劳动。

2. 抓取与存储实现:从HTTP请求到结构化数据的完整链路

数据源定下来之后,接下来就是实际抓取了。这一节我会把从URL分析、请求发送到数据落地的完整流程拆开讲。以我抓取时的目标站点为例,页面结构大概长这样:一个按月份展示的表格,每一天对应一行,列包含日期、最高温、最低温、天气、风向风力。

2.1 URL结构与请求参数的分析

网页端的天气数据基本都是通过URL中的查询参数来控制城市和日期的,形如:

https://xxx.com/weather/history/54511/202401.html

其中54511是北京站点的区站号,202401代表2024年1月。北京这个站点的区站号是固定的,所以我只需要构造从2014年到2023年的所有月份链接即可,一年12个月,10年共120个页面。

分析URL结构这一步是整个抓取流程里最值得花时间的地方。很多网页的URL变化是有规律的,而规律往往藏在浏览器开发者工具的Network面板里。你只需要在页面上手动切换一个月,观察地址栏的变化,就能推断出URL的构造规则。如果能用这种方式拿到数据,就不要去碰那些加密参数复杂、带有动态token的接口。

我的构造逻辑用Python写出来就是这样:

import requests city_code = "54511" years = range(2014, 2024) months = range(1, 13) def build_url(year, month): return f"https://xxx.com/weather/history/{city_code}/{year}{month:02d}.html"

2.2 请求头与访问频率的控制

页面虽说是公开数据,但爬取时也要有基本的克制。我习惯在请求中携带一个常规的User-Agent,并且把两次请求之间的间隔控制在不小于1秒,避免给目标服务器造成压力。

import time headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } def fetch_html(url): resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text

resp.apparent_encoding这一步容易被忽略,但很重要。网页声明的charset和实际编码有时不一致,直接用resp.text可能出现乱码。用apparent_encoding可以根据内容自动推断编码,实测下来对中文站点的兼容性更好。

2.3 表格解析与字段清洗

拿到HTML之后,用BeautifulSoup定位表格数据。我的做法是先找到所有tr标签,再从每个tr里提取td。但这里有个坑:页面上方可能还有"月累计平均"之类的汇总行,这些行会混在数据里,需要依据行内容的特征做过滤。

from bs4 import BeautifulSoup import pandas as pd def parse_page(html): soup = BeautifulSoup(html, "html.parser") trs = soup.select("table tr") rows = [] for tr in trs: cells = tr.find_all("td") if len(cells) < 4: continue date = cells[0].get_text().strip() if "月" in date or "年" in date: continue high = cells[1].get_text().strip().replace("℃", "") low = cells[2].get_text().strip().replace("℃", "") weather = cells[3].get_text().strip() rows.append({"date": date, "high": high, "low": low, "weather": weather}) return rows

这里我做了两个过滤:第一,跳过列数不足的干扰行;第二,跳过含"月""年"的汇总行。如果你只想要数值,而不想要天气现象文字,完全可以删掉weather字段。但建议保留,因为后续做"极端天气分布"这类分析时,weather字段是很有价值的辅助信息。

清洗温度字段时,注意原网页里温度后面可能带了或空格,需要统一替换掉。还有一个高频坑:个别单元格内容可能是--或空值,代表当天数据缺失。遇到这种情况不要直接删行,更不要让pandas自动推断成NaN了事,我建议保留原始字符串,并在后续统一填充。

2.4 数据落地与增量保存

数据逐月解析完成后,我习惯先追加写入CSV而不是等全部跑完再一次性导出。原因很简单:万一中间断网或程序异常,至少前面的数据保住了,不需要从头再来。

import csv import os csv_path = "beijing_weather.csv" write_header = not os.path.exists(csv_path) with open(csv_path, "a", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["year", "month", "day", "high", "low", "weather"]) if write_header: writer.writeheader() for row in all_rows: writer.writerow(row)

几个细节说明:

  • 编码使用utf-8-sig,这样Excel直接打开CSV不会出现中文乱码;
  • 每次请求完一个页面,先解析、再追加写入,避免把所有数据攒在内存里最后一起写;
  • 如果程序中断重启,可以读取CSV里已有的日期集合,跳过已经抓过的月份,实现断点续抓。

断点续抓的代码逻辑很简单:

def already_fetched(): if not os.path.exists(csv_path): return set() df = pd.read_csv(csv_path, usecols=["year", "month"]) return set(zip(df["year"], df["month"]))

这样一年重建一次数据,无论中途怎么中断,只需要重新运行脚本就能自动补齐缺失月份。

3. 数据清洗与时间对齐:那些坑不会写在文档里

抓下来的数据直接拿去画图,结果大概率是灾难。原因有两类:一是原始数据里有脏值,二是日期字段的格式和year/month/day没有对齐。这一节专门讲清洗链路。

3.1 日期字段的归一化处理

我在解析页面时,将日期拆成了year、month、day三个字段。但有时原网页显示的日期是2024-01-05,有时是1月5日,格式五花八门。所以清洗的第一步,是把这些杂乱的字符串统一转化为标准日期类型。

import pandas as pd df = pd.read_csv("beijing_weather.csv") df["date"] = pd.to_datetime(df[["year", "month", "day"]]) df = df.set_index("date").sort_index()

pd.to_datetime能处理多种日期格式,但它也会在遇到非法日期时抛出异常或产生NaT。所以转换之后一定要检查:

print(df[df.index.isna()])

如果有NaT,基本上就是原始数据里有类似2月30日这样的脏值。这种问题最常出现在2月,需要你用下面的逻辑直接修正:

df = df[(df.index.month == 2) & (df.index.day <= 29) | (df.index.month != 2)]

不过说实话,公开天气页面里基本不会出现2月30日,更常见的是日期完全缺失。此时我建议对缺失的日期行做前向填充或直接剔除,具体看后续分析需求。画年趋势图时,剔除个别缺失值影响不大;但如果要做逐日对比,最好用插值补全。

3.2 温度异常值的过滤逻辑

温度字段常见的脏值有这几类:

  1. 原始字符串残留了、空格、换行等;
  2. 个别温度异常偏高或偏低,比如夏天出现-12℃
  3. 最高温低于最低温的逻辑错误。

清洗时,我先把温度列转成数值类型,再针对每一条记录做一次逻辑判断:

df["high"] = pd.to_numeric(df["high"], errors="coerce") df["low"] = pd.to_numeric(df["low"], errors="coerce") # 1. 删除温度缺失的行 df = df.dropna(subset=["high", "low"]) # 2. 删除高低温倒挂的行 df = df[df["high"] >= df["low"]] # 3. 删除超出合理范围的行(北京近10年极端高温不超45,极端低温不低于零下30) df = df[(df["high"] < 45) & (df["low"] > -30)]

这里的阈值范围是我根据北京气候特征设置的,如果你换城市,可以查一下当地历史极端温度再调整。这套过滤逻辑不会误删正常数据,但能把那些明显影响曲线的异常点清理掉。

3.3 缺测日期的补全策略

3650条记录里,缺个两三天是很正常的。现在分析之前,需要先确认日期是否连续。

full_index = pd.date_range(start="2014-01-01", end="2023-12-31", freq="D") missing_dates = full_index.difference(df.index) print(missing_dates)

如果缺的日期不多,我的习惯是用前后两天的均值做线性插值:

df = df.reindex(full_index) df["high"] = df["high"].interpolate() df["low"] = df["low"].interpolate()

注意,reindex之后缺失的日期对应的weather字段也会变成NaN,如果你后续要做天气类型统计,需要单独处理,比如用前一天的天气填充。温度插值本身没问题,但天气类型不适合插值,因为"晴"和"雨"中间没有任何过渡状态。

3.4 数据结构落地统一

清洗完毕之后,把数据按照统一的格式整理好,方便后续可视化、统计和导出。我最终保存的字段结构如下:

字段类型说明
datedatetime64日期,索引字段
yearint年份
monthint月份
dayint
highfloat当日最高气温(℃)
lowfloat当日最低气温(℃)
weatherstring天气现象文本

这个结构很简单,但足够支撑后面90%的分析任务。

4. 全年曲线可视化:从单年折线到10年趋势叠加

数据干净之后,画图就是水到渠成的事。我用的是Matplotlib,核心目标是画出三种图:单年温度曲线、10年趋势叠加曲线、年度平均气温对比柱状图。这些图合在一起,就是一套完整的"北京近10年天气变化曲线"。

4.1 单年温度曲线:基础折线图的踩坑与调优

先拿2014年做个demo。画出这一年的最高温和最低温曲线:

import matplotlib.pyplot as plt import matplotlib.dates as mdates plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False df_2014 = df[df["year"] == 2014] fig, ax = plt.subplots(figsize=(14, 6)) ax.plot(df_2014.index, df_2014["high"], color="#d9534f", label="最高温") ax.plot(df_2014.index, df_2014["low"], color="#5bc0de", label="最低温") ax.fill_between(df_2014.index, df_2014["low"], df_2014["high"], color="grey", alpha=0.2) ax.set_title("北京2014年气温变化曲线") ax.set_xlabel("日期") ax.set_ylabel("气温(℃)") ax.legend() ax.grid(alpha=0.3) plt.tight_layout() plt.savefig("beijing_2014_temp.png", dpi=150) plt.show()

这里有两个细节很容易被新手忽略:

第一是字体设置。Matplotlib默认字体不支持中文,如果不设置font.sans-serif,图表里的"最高温""最低温"都会变成方块。设置成SimHei之后,中文标题和图例才能正常显示。如果你用的是macOS/Linux,SimHei不一定有,可以换成PingFang SCNoto Sans CJK SC

第二是fill_between我在最高温和最低温之间填充了半透明灰色,这样能更直观地看到温差区间。实测下来,这种画法比两条曲线孤零零地放那里信息量大得多,读者一眼就能看出"夏天温差小、冬天温差大"之类的规律。

4.2 10年数据叠加:突出周期性与极端年份

单年曲线看不出长期趋势,所以我把10年数据叠加到同一张图上,用不同透明度区分年份:

fig, ax = plt.subplots(figsize=(16, 8)) for year in range(2014, 2024): yearly_data = df[df["year"] == year] ax.plot(yearly_data.index.dayofyear, yearly_data["high"], color="blue", alpha=0.3, linewidth=0.8) ax.set_title("北京2014-2023年逐日最高温叠加曲线") ax.set_xlabel("年内第几天") ax.set_ylabel("气温(℃)") ax.grid(alpha=0.3)

dayofyear作为x轴,是为了让不同年份的数据对齐在同一个时间坐标系里。这样每一列竖着看,能直接对比同一天在十年内的温度波动范围;横向看,则能观察到每年温度曲线的整体相位是否一致。

叠加图最大的价值在于观察异常年份。如果哪一年的曲线整体偏离了其他年份的包络带,这一年基本可以确定是冷暖异常年份。比如2018年前后有些天明显高于其他年份,这就是一个值得深挖的信号。

4.3 月度/年度聚合曲线:10年趋势的宏观规律

再往前一步,我们按月份聚合统计10年内的月均最高温:

monthly_mean = df.groupby(["year", "month"])["high"].mean().unstack(0) monthly_mean = df.groupby(["year", "month"])["high"].mean().reset_index() pivot = monthly_mean.pivot(index="month", columns="year", values="high") fig, ax = plt.subplots(figsize=(14, 6)) for col in pivot.columns: ax.plot(pivot.index, pivot[col], marker="o", markersize=4, linewidth=1.2, label=str(col)) ax.set_title("北京2014-2023年月均最高温变化") ax.set_xticks(range(1, 13)) ax.set_xticklabels(["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"]) ax.set_xlabel("月份") ax.set_ylabel("月均最高温(℃)") ax.legend(ncol=5, fontsize=8) ax.grid(alpha=0.3) plt.tight_layout() plt.savefig("beijing_monthly_avg.png", dpi=150) plt.show()

这条曲线对应的表格数据也很有价值,可以直接透视出"哪年最热""哪年最冷""秋季降温速度快慢"这些信息。我通常会把月均最高温、最低温、平均温差这几个统计量再合并成一张汇总表导出。比如10年逐月平均最高温表格可以帮助一眼定位温度峰值出现的月份,十年间该月均温的变化幅度也可以直接读取。

4.4 画图时的三个隐藏优化点

  1. 坐标轴范围:如果只是想看曲线趋势,可以让Matplotlib自动选择y轴范围。但如果要做多图对比,务必要手动固定ax.set_ylim(-20, 40)这种范围,否则不同图之间的视觉比例不一致,读者会产生误判(比如同一温度在不同图里看起来差异很大)。

  2. 图表尺寸:默认的figsize=(6,4)画出来很难看清,建议宽度14以上。上传到博客或社区时,够宽的图不会被压缩得看不清密度变化。

  3. 保存dpi:用savefig(..., dpi=150)保存,既不会让文件体积过大,又能在网页端清晰展示。如果需要打印导出,可以用300dpi。

5. 多维度分析与绘制进阶:只画一条线是不够的

纯画一条温度曲线只能回答"热了还是冷了",但要做出真正有价值的内容,我们还得深入分析极端天气、风力和温差这些维度。这一节聊聊进阶分析的几个方向。

5.1 天气现象的频率统计

从数据里统计"晴天、多云、小雨、大雨、雪"等天气出现的天数,按年度汇总。这个操作在pandas里一行groupby就能搞定,但实际做出来的图表非常能说明宏观气候特征。

weather_counts = df.groupby([df.index.year, "weather"]).size().unstack(fill_value=0)

需要注意,不同站点对天气现象的叫法可能不统一。比如有的页面写"多云转晴",有的写"多云"。这种情况下,我建议先做一次归类映射,把相近的天气现象合并成大类:

weather_map = { "晴": "晴", "多云": "多云", "阴": "阴", "小雨": "雨", "阵雨": "雨", "大雨": "雨", "雷阵雨": "雨", "小雪": "雪", "中雪": "雪", "大雪": "雪", } df["weather_type"] = df["weather"].map(weather_map).fillna("其他")

做完这一步,再按年统计各类天气天数,就能画出一张堆叠柱状图,直观展示"北京的雨雪分布在不同年份的波动情况"。

5.2 温差曲线的价值

单纯看最高温、最低温还远远不够。在气候分析里,昼夜温差(日较差)是衡量大陆性气候特征和人体舒适度的关键指标。所以我习惯再算一列温差:

df["temp_range"] = df["high"] - df["low"]

然后按月份聚合,观察十年里每个月的平均温差走势。北京春季(3到5月)和秋季(9到11月)的温差通常比较大,夏季小一些,冬季受冷空气影响温差也不小。这个曲线放到文章里,比"北京四季分明"这种描述有说服力得多。

有个细节要注意:直接用平均最高温减去平均最低温,和逐日温差取平均值,结果并不相等。后者才是真正的"平均昼夜温差"。为了严谨,我们应该先算逐日温差,再按月份求平均。

5.3 年度平均气温与线性趋势

最后一个进阶方向:用年度平均气温配一条线性回归趋势线,量化升温/降温的速率。这里不需要复杂的统计库,用numpy.polyfit就够了:

import numpy as np yearly_mean = df.groupby(df.index.year)["temp_mean"].mean() coeffs = np.polyfit(yearly_mean.index, yearly_mean.values, 1) trend_line = np.polyval(coeffs, yearly_mean.index)

其中temp_mean是每天最高温和最低温的平均值。拟合出来的斜率就是"每年温度变化的度数"。如果斜率是正数,说明这10年整体在升温;如果是负数,说明偏冷。不过要注意,10年序列的线性趋势受个别极端年份影响大,结论只能作为参考,不能直接拿去当气候变化的证据。

5.4 多图组合输出建议

如果你要写一篇完整的数据分析博客或季度复盘,可以把上面几张图组合成一张大图:

fig = plt.figure(figsize=(16, 12)) # 子图1:逐日最高温最低温包络带 # 子图2:月度平均温度对比曲线 # 子图3:天气现象堆叠柱状图 # 子图4:年度平均气温加趋势线 plt.tight_layout() plt.savefig("beijing_10y_weather_report.png", dpi=200) plt.show()

这样一张总览图放进报告里,信息密度极高,读者不需要滚动多次就能看懂"十年里北京天气是怎么变的"。

6. 常见异常与处理对策:跑数据时最容易翻车的四个瞬间

最后聊聊实际操作中出现过的异常情况,这些话平常教程里不会明确写,但能帮你少走弯路。

第一个坑:请求并发过高导致IP被临时限制。我一开始用异步并发抓取120个页面,速度很快,但跑了30多个页面之后,目标站点开始返回403。解决办法很简单:改回同步请求,每次间隔1到2秒。120个页面最多3分钟就能跑完,完全没必要为了省这半分钟把IP搭进去。

第二个坑:页面结构微调导致解析不到数据。天气平台偶尔会调整表格的CSS类名或列顺序。不要写死某一个CSS类名,而是先打印出tr的总数和前几行内容,人工确认一下再批量解析。脚本里加一个if len(rows) == 0: print("可能结构变了")的断言,能省很多排查时间。

第三个坑:CSV追加写入时表头重复。这个很经典。脚本第一次运行写入了表头,中断后第二次运行时write_header又变成了True,导致CSV里有多个表头行。我在代码里用os.path.exists来判断,但如果CSV已经存在但内容为空,这个判断也会跳过表头写入,导致后续pd.read_csv把第一行数据当列名。稳妥的做法是运行前手动检查CSV文件内容,或者用os.path.getsize(csv_path) > 0来做判断。

第四个坑:Excel打开CSV中文乱码。这个问题在Windows平台尤其常见。解决办法是写入时用utf-8-sig编码,而不是普通的utf-8。如果你已经用错误编码存了很多数据,可以直接用Python读一遍再重新写一遍。

df = pd.read_csv("beijing_weather.csv", encoding="utf-8") df.to_csv("beijing_weather_fixed.csv", index=False, encoding="utf-8-sig")

这四个问题基本覆盖了从抓取到存储再到展示的绝大部分异常场景。整个项目跑下来,我认为最值得沉淀的经验就是:天气数据分析不是一个"调个API画条线"的简单任务,真正的耗时点全在数据清洗和异常处理上,但只要把流程拆成"选源-抓取-清洗-可视化-分析"五个环节,每一步保持数据的可追溯性,整个项目就会非常顺畅。

如果你需要换城市跑同一套代码,只需要改区站号和年份范围,其余流程完全一样。这个项目后续还可以扩展成自动化的月度更新脚本,配合定时任务,就能持续积累新的天气数据,打造一个长期追踪的气候分析工具。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 18:11:44

Excel合并单元格转Key-Value映射:三种自动化实现方案

1. 先搞清楚&#xff0c;合并单元格在数据层面到底“合并”了什么 1.1 合并单元格的真实存储形态 我在处理各种报表时&#xff0c;“合并单元格”这几个字真是又爱又恨。爱是因为它在展示层确实干净利落&#xff0c;恨是因为一旦把数据交给程序去读取&#xff0c;它就成了最大…

作者头像 李华
网站建设 2026/9/9 18:11:36

AI提示词优化器:5 分钟跑通教程

AI提示词优化器&#xff1a;5 分钟跑通教程 【免费下载链接】prompt-optimizer An AI prompt optimizer for writing better prompts and getting better AI results. 项目地址: https://gitcode.com/GitHub_Trending/pro/prompt-optimizer 你把"帮我写首诗"丢…

作者头像 李华
网站建设 2026/9/9 18:10:37

嵌入式C/C++面试笔试核心考点与备考路线全解析

简介&#xff1a;面向嵌入式、C/C方向求职者的面试笔试资料包&#xff0c;汇集Linux设备驱动、C语言经典算法、嵌入式C/C精华文章及Linux与C面试题等核心内容&#xff0c;覆盖校招与社招常见考点&#xff0c;适合准备技术笔试、面试冲刺或系统复习的开发者学习使用。资料中既有…

作者头像 李华
网站建设 2026/9/9 18:10:01

ComfyUI中文提示词插件实战:解决CLIP不认中文的痛点

简介&#xff1a;面向ComfyUI中文用户推出的自定义插件&#xff0c;主要解决AI绘画流程中英文提示词门槛高的问题&#xff0c;适用于个人创作者、设计爱好者与AI绘画入门者&#xff0c;无需编程基础即可上手。它支持在可视化节点操作界面内直接输入中文关键词或指令&#xff0c…

作者头像 李华
网站建设 2026/9/9 18:08:40

抖音SEO关键词排名优化:从搜索流量到内容落地的完整实操指南

做抖音SEO关键词排名&#xff0c;很多人第一步就搞错了方向。我在带团队做抖音运营的时候&#xff0c;收到过太多类似的咨询&#xff1a;“老师&#xff0c;抖音SEO怎么做&#xff1f;是不是把关键词堆到标题里就行&#xff1f;”或者“有没有那种可以把视频直接顶到搜索第一页…

作者头像 李华