ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

用Python分析北京10年天气:数据抓取到可视化的完整实战 1. 数据源的选型逻辑公开API、网页抓取和历史库的取舍分析先说结论想做北京最近10年全年天气变化曲线最核心的问题其实不是画图而是数据从哪来。很多人在这一步就卡住了然后去翻了十二个网站、复制粘贴了三千行数据最后画出来的曲线还缺牙少齿。我一开始也走过弯路所以把数据源选型放在最前面讲。市面上能拿到历史天气数据的途径大致有三类第一类通过各类天气平台的公开页面直接抓取。好处是无需注册、无需密钥数据可视化程度高、字段直观坏处是页面结构经常改反爬手段不一数据密度和年份覆盖不一定满足10年的需求。比如有的站点只保留近3年数据或者只提供逐月汇总拉不到逐日数据这就会严重限制我们对全年天气变化曲线的还原精度。第二类调用专业气象数据API。好处是数据结构化好、字段齐全、更新稳定坏处是大部分都需要企业认证或付费个人开发者签下免费额度的门槛不低。对于一些想快速出图、不想花太多时间在反爬上的朋友来说这是最省心的选择但如果你是第一次尝试注册、鉴权、请求配额这些环节反而可能比写爬虫本身更浪费时间。第三类使用开源历史气象数据集。比如一些数据社区整理好的全球站点逐日数据好处是下载即用、不需要考虑断线和反爬坏处是数据更新滞后而且对于北京这种站点不同数据集的粒度、单位、时区口径可能不一致处理起来需要额外校验。我在实际项目中采用的是**以公开网页抓取为主、以历史气象数据平台的格式要求为辅**的组合方案。理由也很朴素这张曲线的核心是趋势呈现需要的是连续10年的逐日数据而不是某一天的精确温度所以网页端的显示精度完全够用公开网页的数据字段相对稳定常规字段日期、最高温、最低温、天气现象、风向风力都能拿到足够做完整的年度曲线分析不依赖第三方密钥整个流程可复现换一个城市也能直接改参数跑通。选型时还有一个关键考量数据频率。做全年天气变化曲线最低要求是逐日数据最佳状态是能拿到逐小时数据。逐小时数据量更大但能支撑更细的分析比如昼夜温差曲线、极端天气过程的温度演变。如果只想画全年气温走势逐日数据就够了。我在实践里选择的是逐日最高温/最低温/天气现象三件套理由是这个粒度既能画清趋势又不需要处理太多异常值。提示动手之前先估算数据规模。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 fhttps://xxx.com/weather/history/{city_code}/{year}{month:02d}.html2.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, headersheaders, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.textresp.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, encodingutf-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 温度异常值的过滤逻辑温度字段常见的脏值有这几类原始字符串残留了℃、空格、换行等个别温度异常偏高或偏低比如夏天出现-12℃最高温低于最低温的逻辑错误。清洗时我先把温度列转成数值类型再针对每一条记录做一次逻辑判断df[high] pd.to_numeric(df[high], errorscoerce) df[low] pd.to_numeric(df[low], errorscoerce) # 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(start2014-01-01, end2023-12-31, freqD) 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], colorgrey, alpha0.2) ax.set_title(北京2014年气温变化曲线) ax.set_xlabel(日期) ax.set_ylabel(气温℃) ax.legend() ax.grid(alpha0.3) plt.tight_layout() plt.savefig(beijing_2014_temp.png, dpi150) plt.show()这里有两个细节很容易被新手忽略第一是字体设置。Matplotlib默认字体不支持中文如果不设置font.sans-serif图表里的最高温最低温都会变成方块。设置成SimHei之后中文标题和图例才能正常显示。如果你用的是macOS/LinuxSimHei不一定有可以换成PingFang SC或Noto 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], colorblue, alpha0.3, linewidth0.8) ax.set_title(北京2014-2023年逐日最高温叠加曲线) ax.set_xlabel(年内第几天) ax.set_ylabel(气温℃) ax.grid(alpha0.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(indexmonth, columnsyear, valueshigh) fig, ax plt.subplots(figsize(14, 6)) for col in pivot.columns: ax.plot(pivot.index, pivot[col], markero, markersize4, linewidth1.2, labelstr(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(ncol5, fontsize8) ax.grid(alpha0.3) plt.tight_layout() plt.savefig(beijing_monthly_avg.png, dpi150) plt.show()这条曲线对应的表格数据也很有价值可以直接透视出哪年最热哪年最冷秋季降温速度快慢这些信息。我通常会把月均最高温、最低温、平均温差这几个统计量再合并成一张汇总表导出。比如10年逐月平均最高温表格可以帮助一眼定位温度峰值出现的月份十年间该月均温的变化幅度也可以直接读取。4.4 画图时的三个隐藏优化点坐标轴范围如果只是想看曲线趋势可以让Matplotlib自动选择y轴范围。但如果要做多图对比务必要手动固定ax.set_ylim(-20, 40)这种范围否则不同图之间的视觉比例不一致读者会产生误判比如同一温度在不同图里看起来差异很大。图表尺寸默认的figsize(6,4)画出来很难看清建议宽度14以上。上传到博客或社区时够宽的图不会被压缩得看不清密度变化。保存dpi用savefig(..., dpi150)保存既不会让文件体积过大又能在网页端清晰展示。如果需要打印导出可以用300dpi。5. 多维度分析与绘制进阶只画一条线是不够的纯画一条温度曲线只能回答热了还是冷了但要做出真正有价值的内容我们还得深入分析极端天气、风力和温差这些维度。这一节聊聊进阶分析的几个方向。5.1 天气现象的频率统计从数据里统计晴天、多云、小雨、大雨、雪等天气出现的天数按年度汇总。这个操作在pandas里一行groupby就能搞定但实际做出来的图表非常能说明宏观气候特征。weather_counts df.groupby([df.index.year, weather]).size().unstack(fill_value0)需要注意不同站点对天气现象的叫法可能不统一。比如有的页面写多云转晴有的写多云。这种情况下我建议先做一次归类映射把相近的天气现象合并成大类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, dpi200) 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, encodingutf-8) df.to_csv(beijing_weather_fixed.csv, indexFalse, encodingutf-8-sig)这四个问题基本覆盖了从抓取到存储再到展示的绝大部分异常场景。整个项目跑下来我认为最值得沉淀的经验就是天气数据分析不是一个调个API画条线的简单任务真正的耗时点全在数据清洗和异常处理上但只要把流程拆成选源-抓取-清洗-可视化-分析五个环节每一步保持数据的可追溯性整个项目就会非常顺畅。如果你需要换城市跑同一套代码只需要改区站号和年份范围其余流程完全一样。这个项目后续还可以扩展成自动化的月度更新脚本配合定时任务就能持续积累新的天气数据打造一个长期追踪的气候分析工具。
返回列表