资讯动态

Python爬虫实战:抓取全年天气数据并做可视化分析

发布时间:2026/9/7 18:11:24 来源:尧图企业网站定制
前两天有个朋友想分析北京全年气候问我一月到底能冷到什么程度、秋冬雾霾集中在哪几个月我当时第一反应不是去翻统计年报而是直接写个爬虫把全年天气数据抓下来存成表格再做可视化。这个项目麻雀虽小但把爬虫、数据清洗、数据分析和可视化全串起来了特别适合刚学会 requests、又不想停留在课本案例的 Python 新手拿来练手。这篇文章我会从数据源选择、爬虫代码实现、数据分析思路到可视化图表一一拆开讲把你可能踩的坑也一起列出来。我用的方案是 requests 抓公开的天气历史数据接口每天最高温、最低温、天气现象、风向风力、空气质量这些字段都有拿到手之后用 pandas 清洗再用 matplotlib 画几张能直接放进报告里的图。整体代码量不大核心爬虫部分大概 80 行就能搞定后面分析展示另算。1. 项目整体设计与技术选型思路1.1 一个完整爬虫数据分析项目的基本拆解很多人一提到爬虫就想到各种花哨的框架其实对这种个人分析向的小项目最重要的不是技术炫技而是把链路跑通。整个项目拆开看就四步找数据源、写爬虫抓数据、清洗数据、可视化分析。每一步都有独立的坑但每一步也都不算复杂。找数据源是最先要做的事。天气数据在国内有大量公开来源有的是网页版的历史天气查询点一下日期翻一页有的是后端有 JSON 接口浏览器打开就能看到结构化数据。对爬虫新手来说优先找有 JSON 接口的省去解析 HTML 的麻烦。如果只能拿到网页用 BeautifulSoup 或 lxml 去解析表格也完全可行只是要多写几行提取代码。数据抓下来之后不是直接就能分析得先过一遍 pandas。比如有些接口返回的高温、低温字段是字符串不转数值没法画图日期字段可能是 2023-01-05 这样的字符串不转 datetime 类型没法按月份聚合。这些清洗步骤看着琐碎但少做一步后面画图就会出错。最后才是可视化分析。这一步的要求不是图好看而是能回答你最初的问题全年的温度趋势是什么、哪几个月降水最多、不同季节的天气现象分布有什么差异。所以先想清楚要分析什么再决定画什么图顺序不要反。1.2 为什么选 requests pandas matplotlib 这套组合选技术栈的时候我也犹豫过要不要上 scrapy、要不要用 pyecharts 画交互图后来还是压住了冲动用最简单的组合。requests 是 Python 生态里最常用的 HTTP 库API 设计简单get 请求、带参数、带请求头都是几行代码的事。这个项目只需要请求 12 次接口每次拿一个月的历史数据完全没有必要上 scrapy。scrapy 的优势在于大规模采集、并发调度、中间件扩展用在 12 个请求上属于杀鸡用牛刀反而把学习成本拉高了。pandas 做数据清洗和聚合是验证过无数次的方案。DataFrame 的 to_datetime 转换、groupby 聚合、rolling 窗口计算、value_counts 统计几乎覆盖了这个项目所有需要的数据操作。如果不用 pandas纯靠 Python 原生列表和字典也能做但代码量至少翻一倍而且月份分组、缺失值处理这类操作你会写得很痛苦。matplotlib 是可视化的万金油稍微有点老但胜在稳。折线图、柱状图、散点图、极坐标图都能画官方文档和网上的示例也多。pyecharts 确实交互效果更好鼠标悬停能看数据但如果你想把图表嵌进 Word 或 PDF 报告里还是 matplotlib 导出的静态图片更方便。我的做法是分析阶段用 matplotlib 快速出图最后如果有展示需求再用 pyecharts 补两张交互图。1.3 数据源选择与合规底线这几年各平台对爬虫的态度越来越严格个人学习项目尤其要注意边界。我这次选数据源的原则很简单只抓公开可访问、不需要登录、不需要破解验证码的数据请求频率严格控制抓下来的数据只用于个人学习分析不商用也不二次传播。具体到这个项目我用的是一家公开天气站点的历史数据接口它加载的是一整年的分月 JSON 数据字段清晰、结构稳定非常适合教学演示。接口地址在浏览器开发者工具的 Network 面板里能看到前端请求什么参数我们就模拟什么参数这样整个流程下来你能理解真实的数据采集是怎么一回事。如果你不想用接口也可以退一步用公开的网页版历史天气页面requests 拿 HTML 再用 BeautifulSoup 解析。两种方案代码结构类似只是解析层不同。无论哪种方案我的建议都是先确认该站点的抓取协议再观察自己的请求频率保持礼貌爬取数据够用就停。2. 核心细节解析从网页到结构化数据2.1 动手写代码前先干三件事很多新手一上来就写代码结果连目标页面长什么样都不知道光调参就调半天。我拿到这个项目后先做了三件事这三件事比你想象中重要得多。第一件事是打开浏览器开发者工具切到 Network 面板手动把目标网页翻一遍找到真正返回数据的那个 XHR 请求。你会发现网页上显示的温度、日期、风向其实都是前端通过这个请求拿到的 JSON 数据渲染出来的。找到请求之后看一下它的 URL、请求方法、查询参数和返回结构这是后面写代码的直接依据。第二件事是确认数据字段有哪些。以天气接口为例一般会包含日期、最高温、最低温、天气现象、风向、风力、空气质量等。这些字段不是每个都有用先记下来后面爬的时候也知道要保留哪些列。我一般会顺手把原始 JSON 保存一份方便后面反复比对和调试。第三件事是判断这个接口有没有反爬措施。常见的反爬包括 User-Agent 检查、Referer 检查、Cookie 依赖、请求频率限制等。短期内判断的方法很简单用 requests 不带任何请求头去访问看返回的是正常 JSON 还是错误提示再带上浏览器的完整请求头试一次看结果是否不同。如果带请求头就正常那基本可以确定这个接口至少做了 UA 或 Referer 校验。这三件事做完你对目标接口的了解程度其实已经超过很多写了一半就放弃的人。磨刀不误砍柴工说的就是这个。2.2 爬虫代码的骨架与核心参数设计这个项目的爬虫部分可以用一个函数来承载核心逻辑给定城市代码、年份、月份返回这个月的天气记录列表。城市代码是接口要求的参数一般是一串数字比如北京是 101010100 这种格式代表行政区划编码。具体数值需要从接口或网页里查不同的城市对应不同的代码。考虑到接口可能对请求头做校验我先创建一个 requests.Session统一设置 User-Agent、Referer 等常用头信息后面所有请求都通过这个 Session 发出。Session 还能自动管理 Cookie如果接口在访问过程中设置了会话标记也能保证一致性。import requests import time import random import json import os import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://weather.example.com/, Accept: application/json, text/javascript, */*; q0.01, })接下来是核心的抓取函数。接口用的是 GET 请求参数按接口的要求组织成字典形式requests 会自动做 URL 编码。函数里我加了超时设置和异常捕获避免某一月请求失败时整个程序直接崩溃。def fetch_month_history(city_code, year, month): url https://weather.example.com/Pc/GetHistory params { areaInfo[areaId]: city_code, areaInfo[areaType]: 2, date[year]: year, date[month]: month, } try: resp session.get(url, paramsparams, timeout10) resp.raise_for_status() data resp.json() except (requests.RequestException, ValueError) as e: logging.error(f抓取 {year}-{month:02d} 失败: {e}) return [] return data.get(list, [])这里有几个细节值得展开说。timeout10 是给每个请求设置最大等待时间防止某个接口卡住导致程序挂死。raise_for_status() 会在 HTTP 状态码不是 2xx 时主动抛出异常这样遇到 403、404、500 时我们能及时发现而不是拿到一个无意义的响应继续往下走。resp.json() 用来解析 JSON但如果接口返回的不是合法 JSON比如反爬页面返回的是 HTMLjson() 会抛 ValueError所以要在异常捕获里把它接住。2.3 反爬、重试与异常处理策略我实测下来这种公开天气接口最常见的反爬手段就是请求头校验和频率限制。请求头校验通过伪装浏览器 UA 和 Referer 就能解决频率限制则需要控制请求间隔。控制请求间隔最简单的方式是在每次请求后随机 sleep 1 到 3 秒。随机而不是固定值是为了避免形成规律的请求节奏更容易被识别为脚本。实测中我用固定 2 秒也跑完了但随机一下更稳妥。对于请求失败的情况理论上有重试机制会更稳但这个项目只有 12 个月的数据就算失败一两个月也可以人工补抓。我的做法是把失败信息记录到日志里程序继续跑后面的月份跑完之后根据日志再补。这里我建议不要用无限重试因为如果接口已经封了你的 IP无限重试只会让封禁时间更长。最多重试 2 次就够了。还要注意一个细节别小看日志的作用。print 输出确实简单但窗口一滚动就看不到了文件日志能让你跑完整个爬虫后回溯哪个月失败了、失败原因是什么。我习惯用 Python 自带的 logging 模块输出到控制台的同时可以配置写入文件方便后续排查。3. 实操过程全年爬取、清洗与落盘3.1 先抓单月验证数据结构拿到接口后不要急着写全年循环先把某一个月的数据抓下来看看结构。我一般先在 Jupyter Notebook 里手动调一次接口把返回的 JSON 打出来确认字段名和数据类型再决定后续怎么清洗。这一步非常关键。你看到的字段可能是英文的比如 date、high、low、weather、wind_direction、wind_level、aqi也可能是中文的比如日期、最高温、最低温、天气、风向、风力、空气质量。字段名不同后面的处理代码就要跟着变。另外还要确认数值字段是不是带单位比如有些接口高温字段是 5有些是 5℃这些都要在清洗时处理掉。我抓了一个月数据后发现接口返回的高低温字段确实是纯数字字符串天气、风向是中文描述AQI 大部分时候有值个别极端天气下可能缺失。基于这个确认我把字段统一命名然后才进入全量抓取阶段。3.2 设计断点续传的全年抓取流程全年12个月的数据虽然请求量不大但网络抖动、接口限流这种事情谁也说不好。为了不出现“跑到11月挂了、前面全白跑”的尴尬情况我建议从一开始就给程序加上断点续传能力。实现方式很简单每个月的原始 JSON 单独存一个文件文件名为{year}_{month}.json每次都先检查文件是否已存在存在就跳过不存在才去请求。这样即使中途断了重新运行时会自动跳过已经抓好的月份只抓剩下的。def crawl_year(city_code, year, save_dirdata/raw): os.makedirs(save_dir, exist_okTrue) all_rows [] for month in range(1, 13): cache_file os.path.join(save_dir, f{year}_{month:02d}.json) if os.path.exists(cache_file): with open(cache_file, r, encodingutf-8) as f: rows json.load(f) logging.info(f读取缓存 {cache_file}共 {len(rows)} 条) else: rows fetch_month_history(city_code, year, month) if not rows: logging.warning(f{year}-{month:02d} 无数据稍后重试) continue with open(cache_file, w, encodingutf-8) as f: json.dump(rows, f, ensure_asciiFalse, indent2) time.sleep(random.uniform(1, 3)) all_rows.extend(rows) logging.info(f{year}-{month:02d} 累计 {len(all_rows)} 条) return all_rows这段代码里有个小设计抓完后每月的 JSON 都直接落盘后面即使改了分析逻辑也不需要重新跑爬虫只重新读取原始 JSON 就行。这比把所有数据都存在内存里、最后一次性写文件要稳妥得多。跑完整个流程之后把全年的数据合并成一个 DataFrame 或者 CSV下一步的清洗才有意义。这里我专门把原始数据和清洗后的数据分开存储就是避免后面的分析需求变化时还要重新浪费请求次数。3.3 数据清洗与 CSV 落盘清洗阶段我推荐用 pandas核心操作就几行代码但每一步都有讲究。import pandas as pd df pd.DataFrame(all_rows) df df.rename(columns{ date: 日期, high: 最高温, low: 最低温, weather: 天气, wind_direction: 风向, wind_level: 风力, aqi: 空气质量指数, }) df[日期] pd.to_datetime(df[日期]) df[最高温] pd.to_numeric(df[最高温], errorscoerce) df[最低温] pd.to_numeric(df[最低温], errorscoerce) df[温差] df[最高温] - df[最低温] df[月份] df[日期].dt.monthpd.to_datetime 会把字符串日期转成 datetime 类型这样后面按月份聚合、按时间排序都很方便。pd.to_numeric 会把最高温、最低温的字符串转成数值遇到无法转换的值会变成 NaN不会直接报错。这个设计在数据质量不稳定的场景下非常实用至少不会因为你的一行脏数据把整个程序搞挂。CSV 落盘时有一个很实用的坑值得提醒如果用默认的 utf-8 编码写入Excel 打开时中文会乱码。需要在 to_csv 时指定 encodingutf-8-sigExcel 才能正确识别。我当年第一次存 CSV 就踩过这个坑后来就再也没忘过。df.to_csv(data/weather_2023.csv, indexFalse, encodingutf-8-sig)存完后先别急着做分析打开 CSV 扫一眼日期是否连续、有无明显异常值。数据质量过关了再往下走。3.4 数据质量校验别让脏数据带偏结论数据清洗完不等于数据没问题我习惯在动手画图之前先做一轮简单的数据质量校验。这一步花不了几分钟但能避免你拿着错误数据画出一张精致的假图表。先检查缺失值。用 df.isna().sum() 看看哪些字段有缺失、缺多少。如果缺失量很少比如全年就缺一两天可以直接把这几行的缺失字段剔除来保证图表完整如果缺失较多就要考虑是不是接口本身某些时段就没有这个字段决定是否需要用其他方式补足。再检查异常值。天气数据里比较典型的异常包括最高温超过 50 度或者低于 -60 度、温差超过 30 度、空气质量指数为负数等。这种数据可能是接口的默认值或者异常记录直接保留会明显扭曲最终图表。处理方式也简单把明显超出物理常识的记录筛出来看一下能修就修修不了就删。最后是检查数据量。一年应该是 365 条或 366 条如果你最后拿到的数据量明显少于这个数说明有月份抓取失败或者解析时丢了数据。这时候回头查日志、补抓缺失的月份比你在图表上纠结半天更有意义。4. 可视化分析让天气数据开口说话4.1 全年气温走势直接画折线还是加滑动平均画全年最高温和最低温的走势最简单的做法是直接把每天的数值画成折线用 linewidth0.8透明度调低这样能把全年的波动全貌展现出来。但直接画原始折线有一个问题数据点太多线条会非常密集整体趋势反而看不清。这时候滑动平均就派上用场了。pandas 的 rolling(7).mean() 可以算一个 7 天平均相当于把短期波动平滑掉保留更明显的气候趋势。我把原始线画成浅色细线把 7 日均线画成深色粗线两张图叠在一起既能看到每日波动也能看清整体走势。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, PingFang SC] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(16, 6)) ax.plot(df[日期], df[最高温], linewidth0.8, alpha0.6, label每日最高温, color#E74C3C) ax.plot(df[日期], df[最低温], linewidth0.8, alpha0.6, label每日最低温, color#3498DB) ax.plot(df[日期], df[最高温].rolling(7).mean(), linewidth2.0, color#C0392B, label最高温7日均线) ax.plot(df[日期], df[最低温].rolling(7).mean(), linewidth2.0, color#2471A3, label最低温7日均线) ax.set_title(f{df[日期].dt.year.iloc[0]} 年全年气温走势) ax.set_ylabel(温度℃) ax.legend() plt.tight_layout() plt.savefig(output/全年气温走势.png, dpi150) plt.show()如果你用的环境是 Mac把中文字体换成 PingFang SC 或 Arial Unicode MS 也行Linux 环境则可能需要先安装中文字体或者指定系统已有的字体路径。这里有个通用经验先在你的环境里执行matplotlib.font_manager.fontManager.ttflist查看可用字体选一个能显示中文的再用不要硬试。4.2 月度气温分布柱状图加箱线图信息量更足全年走势图能看出季节变化但月份的冷热差异还是不够直观。我一般会再画一张月度柱状图展示每个月的平均最高温和平均最低温。monthly df.groupby(月份)[[最高温, 最低温]].mean() months range(1, 13) fig, ax plt.subplots(figsize(12, 5)) ax.bar([m - 0.2 for m in months], monthly[最高温], width0.4, label月均最高温, color#E74C3C) ax.bar([m 0.2 for m in months], monthly[最低温], width0.4, label月均最低温, color#3498DB) ax.set_xticks(list(months)) ax.set_xticklabels([f{m}月 for m in months]) ax.set_ylabel(温度℃) ax.legend() plt.tight_layout() plt.savefig(output/月度均温.png, dpi150) plt.show()如果还想看每个月内部的温度分布区间可以加一张箱线图。箱线图能展示每个月的最高温中位数、四分位数和异常值哪些月温度波动大一眼就能看出来。我尤其推荐分析 3 到 5 月这种换季月份箱线图能明显看到倒春寒造成的气温反复。4.3 天气与降水的关系用堆叠柱状图统计天气现象字段一般是“晴”、“多云”、“阴”、“小雨”、“中雨”、“大雨”、“雷阵雨”、“雪”这类中文描述适合做频次统计。把每个月不同天气现象的出现天数统计出来就能看出不同季节的气候特点。weather_by_month pd.crosstab(df[月份], df[天气]) weather_by_month.plot(kindbar, stackedTrue, figsize(14, 6)) plt.xticks(rotation0) plt.title(各月份天气现象分布) plt.ylabel(天数) plt.legend(locupper left, bbox_to_anchor(1.0, 1.0)) plt.tight_layout() plt.savefig(output/月度天气分布.png, dpi150, bbox_inchestight)堆叠柱状图的优点是可以同时看到每个月的总天数构成也能看出不同天气现象的比例变化。我实际跑出来的结果里夏季雷阵雨明显增多冬季晴天和阴天占比高这种季节规律用表格看很枯燥用堆叠柱状图一眼就明白。如果接口里还有降水量的数值字段也可以按月汇总成柱状图分析雨季分布。不过需要注意很多免费天气接口只有天气现象描述没有精确降水量这时候用天气现象频次来代替降水情况是合理的近似方案。分析的时候心里要清楚这个近似结论描述上不要写成“降水量”而是写“降雨天数”。4.4 风向玫瑰图与 AQI 散点图两个容易被忽视的视角温度和天气现象是两个最常见分析维度但我会额外加两个视角风玫瑰图和空气质量指数走势图。这两个分析能让你对这座城市的气候特点有一个更立体的认识。风玫瑰图用极坐标展示各风向出现的频率特别适合分析一个城市的盛行风向。以北京为例常年偏北风占主导夏季偶尔有东南风。风向字段通常是“北风”、“东北风”、“东风”这类 8 方位描述先映射成角度再用 matplotlib 的极坐标投影画柱状图。import numpy as np dir_map { 北风: 0, 东北风: 45, 东风: 90, 东南风: 135, 南风: 180, 西南风: 225, 西风: 270, 西北风: 315, } order [北风, 东北风, 东风, 东南风, 南风, 西南风, 西风, 西北风] counts df[风向].value_counts().reindex(order).fillna(0) angles np.deg2rad([dir_map[d] for d in order]) width np.deg2rad(45) fig plt.figure(figsize(7, 7)) ax plt.subplot(111, projectionpolar) bars ax.bar(angles, counts.values, widthwidth, alpha0.7, color#27AE60) ax.set_xticks(angles) ax.set_xticklabels(order) ax.set_title(全年风向分布风玫瑰图) plt.tight_layout() plt.savefig(output/风向玫瑰图.png, dpi150) plt.show()空气质量指数 AQI 适合画时间序列散点图同时用一条参考线标出 100 这个轻度污染的阈值。这样能直观看到一年里哪些时段污染风险高。我拿北京的数据跑过一次很清晰地看到 11 月到次年 1 月这段采暖季 AQI 明显偏高和风向、气温叠加起来看能解释很多现象。fig, ax plt.subplots(figsizeshape) ax.scatter(df[日期], df[空气质量指数], s6, alpha0.5, color#8E44AD) ax.axhline(100, colorred, linestyle--, linewidth1.2) ax.set_title(全年 AQI 走势) ax.set_ylabel(AQI) plt.tight_layout() plt.savefig(output/AQI走势.png, dpi150) plt.show()到这里这个项目的可视化分析部分就算完整了。从气温、天气、风、空气质量四个维度交叉去看一整年的气候画像就出来了。5. 常见问题与排查技巧实录5.1 高频问题速查表下面这几类问题是我在实际写爬虫项目时几乎每次都会遇到的整理成表格方便你对照排查。现象可能原因解决办法请求返回 403UA 被识别或缺少 Referer伪装浏览器请求头补上 Referer 和 CookieJSON 解析报错拿到的是反爬 HTML 或 gzip 压缩内容先打印 resp.text 前 200 字符确认返回内容再处理中文乱码响应编码推断错误设置 resp.encoding resp.apparent_encoding爬到一半被限流请求频率太高每次请求后随机 sleep 1~3 秒降低并发数据有 NaN 或缺失接口某天无数据或字段为空用 fillna 补充或直接删除该行分析时注明Excel 打开 CSV 乱码编码用了 utf-8 而非 utf-8-sigto_csv 时指定 encodingutf-8-sig接口字段对不上站点改版或接口变更重新用开发者工具抓包检查新字段名图表中文显示为方块matplotlib 缺少中文字体按系统环境设置 plt.rcParams[font.sans-serif]5.2 爬虫项目里容易犯的细节错误除了表里那些问题我再分享几个光看报错很难发现、但实际很影响体验的细节。第一个是别把全部数据都放在内存里跑完了再存盘。万一某个月的请求一直卡住不返回你的进程就一直在等待前面的数据全在内存里进程一杀全没了。配合断点续传的思路每个月的数据抓完就写盘是成本最低的容灾方案。第二个是接口返回的数据顺序不一定是按日期排的。有些接口是乱序返回有些是按月份内日期正序我都遇到过。所以拿到数据后不要假设顺序直接用 pd.to_datetime 转日期再用 sort_values 按日期排序。少一个排序动作你的时序图就可能出现乱序的回折线。第三个是请求频率的控制。天气接口虽然有公开数据但它不是给你无限刷的。我建议每次请求之间至少间隔 1 秒12 个月的数据最多也就十几秒的耗时完全没有必要为了省十几秒去触发限流。数据抓下来了你的分析需求也满足了没必要做让服务器不舒服的事。5.3 独家避坑先做一个小样再跑全量这是我从很多个项目里总结出来的习惯任何爬虫项目拿到接口后先抓一个小样本比如只有几天的数据仔细看结构确认没问题再扩展到整月、整年。千万不要一上来就跑全年循环万一字段名理解错或者解析逻辑写错你 12 个月的数据可能全是错的还得重新跑。先小样、再全量看起来多花了几分钟实际上帮你省掉了大量返工时间。另外抓下来的原始 JSON 我建议长期保留不要分析完就删。后续如果想换一个维度看数据比如从“按月看”切换到“按季节看”或者想算一下全年平均昼夜温差只需要重新读原始 JSON 清洗就行不需要再去请求接口。6. 一点个人体会完成比完美重要这个项目我从开始到跑完不到半天大部分时间花在确认接口字段和调试中文字体上真正写爬虫代码的时间反而很短。做完之后我最大的体会是天气数据爬虫虽然简单但它是一个难得的闭环项目它逼着你把请求、解析、清洗、存储、分析、可视化所有环节都走一遍。如果你真想练爬虫和数据分析建议不要停留在跟着教程敲一遍。拿这个项目当模板换一个城市换一年或者换一个数据源分析维度也换成你真正感兴趣的比如分析某个城市的供暖季 AQI、分析某个月份的降雨天数同气温的关系。只有自己设计过、踩过坑、改过需求那些技术点才会真正变成你的能力。最后再分享一个小技巧分析完天气数据后可以顺便把它做成一个简单的年度气候报告配上几张图放在你的个人博客或者 GitHub 上。这种既包含爬虫又包含数据分析的小项目比起单独写几百行爬虫代码在展示综合能力上要实用得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价