
如果你刚把Python基础语法过完一遍正愁不知道拿什么项目练手那“爬取全年天气数据再做可视化分析”这个方向我强烈建议你试一次。我自己当初就是靠这个项目把requests、页面解析、pandas清洗和matplotlib画图这一整套流程彻底跑通的前前后后花了一个周末最后看到那张一整年的温度曲线图时成就感比干看十遍教程都实在。这个项目本质上是把“爬虫”和“数据分析”串成一条完整链路先写程序从公开天气站点抓取某座城市一整年的历史天气记录再把数据整理成结构化表格最后用图表把温度、降水、天气现象这些维度的规律呈现出来。它能解决的问题很简单网站上一页一页翻太麻烦而用脚本批量抓取加自动分析十分钟就能获得一张“年度天气画像”。适合的人群也很明确——Python刚入门想找个真实项目练手的人、准备转数据分析但缺实战经验的人以及单纯对自己所在城市的气候规律好奇的普通人。1. 项目整体设计与思路拆解1.1 为什么拿天气数据练手爬虫练手项目有很多电商、招聘、社交平台都是常见目标但天气数据是我见过最适合新手跑通全流程的方向原因有几个。第一数据结构高度规整。天气页面几乎都是表格或列表日期、最高气温、最低气温、天气现象、风力风向字段固定且含义清晰。你不需要处理评论区那种乱七八糟的文本也不需要和复杂的JSON嵌套结构搏斗解析难度对新手足够友好。第二数据量适中。一座城市全年365条记录既不会少到让你觉得“没意思”也不会多到需要分布式爬虫。这个量级足够练习“批量抓取”的思路又不会让单机脚本跑半小时都跑不完。第三可视化维度丰富。温度能画折线图和箱线图天气现象能画柱状图和饼图降水能按月聚合甚至能做每年同期对比。数据花样多图表自然好看出成果的反馈感很强。第四反爬压力适中。公开的天气历史数据基本属于半开放信息绝大多数站点不设严格的反爬门槛顶多限制请求频率。你能在这里练到UA伪装、请求间隔、异常重试这些实战技巧又不会被复杂的滑块验证码劝退对新手是恰到好处的难度。1.2 技术栈选型与核心链路这个项目的技术选型我遵循“够用就好”的原则不盲目上重型框架。整体链路是四段式数据采集用requests发送HTTP请求带上浏览器UA伪装身份拿到HTML源码。数据解析用BeautifulSouplxml解析HTML表格提取目标字段。这两者配合使用最顺手前者写选择器简洁后者解析速度更快。数据存储与清洗用pandas做格式转换、缺失值处理、类型转换最后保存为CSV文件。如果数据量到了几十万条级别再考虑sqlalchemy入库也不迟。数据可视化统计分析用matplotlib画静态图需要交互效果时用pyecharts生成HTML图表。两者各司其职不冲突。为什么不直接调天气API原因很现实各大天气API基本都要注册Key免费版有每日调用上限数据粒度也受限。而爬虫方式能让你完整经历“拿HTML→解析→清洗→分析”的全过程练的是基本功。等这套流程熟练了以后换任何数据源都只是改选择器的事。1.3 数据源选择的关键判断标准选数据源是这个项目最容易踩坑的地方因为很多新手上来就挑一个页面花里胡哨的网站结果选择器写半天还是解析失败。我建议用下面几个标准来筛选页面结构稳定历史天气页面最好是老式的HTML表格而不是JS动态渲染的图表。判断方法很简单——用浏览器“查看源代码”搜索日期字符串能找到就说明是静态内容可以直接爬。URL规律明显按月分页的URL最好带年份和月份参数比如类似/xxx/2024-01.html这种结构遍历时只需要改数字就行。有明确的robots规则虽然公开数据抓取一般问题不大但看一眼目标站点的robots协议、并在脚本里控制请求频率是专业习惯也是对对方服务器的尊重。我最终选择的是一个按月份展示历史天气的公开站点表格结构非常规整每行就是一天的数据。下面所有代码都围绕这个数据源展开。2. 环境准备与单页面抓取实现2.1 开发环境与依赖库安装这个项目不需要多强的机器一个普通笔记本就够。我用的是Windows系统 Python 3.10但整套代码在macOS和Linux上也没区别。Python环境安装我就不展开说了官网下载安装包后记得在安装界面勾选“Add Python to PATH”这是很多新手被卡住的第一道门槛。装好之后打开终端先建一个虚拟环境避免把全局环境搞乱python -m venv weather_envWindows下激活命令是weather_env\Scripts\activatemacOS/Linux下是source weather_env/bin/activate。激活后你会看到命令行前缀变成了(weather_env)说明已经进入独立环境。接下来安装项目依赖一条命令搞定pip install requests beautifulsoup4 lxml pandas matplotlib如果还需要交互式图表补一个pip install pyecharts就这么几个库没了。不需要Scrapy不需要Selenium这个量级的项目用Scrapy属于杀鸡用牛刀Selenium更是完全没必要——静态页面用requests就够了。2.2 构造请求UA伪装与超时重试写爬虫第一步不是解析页面而是学会“像个正常人一样去访问网站”。如果直接用默认的Python requests头很多站点会直接返回403因为你暴露了自己的身份。解决方式很简单把浏览器会发送的请求头带上去。import requests import time import random headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.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.9,en;q0.8, } def fetch_html(url, max_retries3): for attempt in range(max_retries): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: resp.encoding utf-8 return resp.text else: print(fHTTP {resp.status_code}第{attempt 1}次重试) except requests.RequestException as e: print(f请求异常{e}第{attempt 1}次重试) time.sleep(random.uniform(1, 2)) return None这段代码里有几个细节值得注意。resp.encoding utf-8这一行别省。很多网页虽然声明是UTF-8但requests默认的编码推断偶尔会出错直接导致中文乱码。手动指定编码是成本最低的解决方案。timeout10是给请求一个最大等待时间。如果不设置遇到网络波动时程序可能卡死在那里几分钟不吭声非常让人抓狂。设置了超时后请求异常会走except分支配合重试机制就能自愈。random.uniform(1, 2)让重试间隔在一个小范围内随机浮动而不是固定写死。原因是固定间隔容易被识别为脚本行为随机间隔更接近人类操作。后面做批量爬取时这个随机策略更重要。2.3 页面解析用BeautifulSoup提取表格字段拿到HTML之后需要从里面把天气数据一行行拎出来。我用的数据源页面结构非常典型整个月的数据放在一个table标签里每个tr是一天各个字段分散在td单元格中。先写一个解析函数把表格切成行再切单元格from bs4 import BeautifulSoup def parse_page(html): soup BeautifulSoup(html, lxml) # 找到包含天气数据的表格因为页面里可能有其他表格 table soup.select_one(table) rows table.select(tr) data [] for row in rows: cells row.find_all(td) if len(cells) 5: continue # 跳过表头行或空行 # 按顺序解析各列注意去掉多余空白 date cells[0].get_text(stripTrue) high_temp cells[1].get_text(stripTrue) low_temp cells[2].get_text(stripTrue) weather cells[3].get_text(stripTrue) wind cells[4].get_text(stripTrue) data.append([date, high_temp, low_temp, weather, wind]) return data那段if len(cells) 5: continue是必须的。因为表格第一行往往是表头单元格数量和正文行不一样如果不跳过后面pandas处理时会混入脏数据。这也是我实际调试时踩过坑才补上的。get_text(stripTrue)会自动把单元格里的前后空格、换行符清掉拿到干净的原始文本。比如气温单元格里可能存的是 32℃ 经过strip后变成32℃。解析完单页后先打印前几行验证一下html fetch_html(url) data parse_page(html) for row in data[:5]: print(row)看到输出正常单页抓取就算打通了。这一步是整个项目的基石后面所有批量操作都建立在这两个函数的可靠性之上。3. 全年数据抓取的工程化处理3.1 摸清URL规律把单页变成批量单月数据打通后下一步就是把12个月甚至365天都抓下来。关键动作是分析URL结构找到动态变化的参数。我使用的数据源URL规律是https://weather.example.com/history/2024-01.html https://weather.example.com/history/2024-02.html ...每次只有月份数字变化。既然找到了规律生成URL列表就是一次循环的事def build_urls(year): urls [] for month in range(1, 13): url fhttps://weather.example.com/history/{year}-{month:02d}.html urls.append(url) return urls这里的{month:02d}是Python格式化语法作用是让月份不足两位时补零保证2024-01而不是2024-1。别小看这个细节有些网站的URL对格式非常敏感写错了直接返回404。有了URL列表再配合前面写好的fetch_html和parse_page批量采集主函数就出来了。但这里千万别写成“一口气把12个页面全抓完”的粗暴循环必须加节奏控制。3.2 请求频率控制与断点续采批量爬虫最大的忌讳是“快”最快的脚本往往是死得最快的脚本。每次请求之间不休息和DDoS攻击没有区别目标服务器一旦发现异常流量轻则封你IP重则拉黑整个网段。我采用的策略是随机间隔2到4秒def crawl_year(year): urls build_urls(year) all_data [] for url in urls: print(f正在抓取{url}) html fetch_html(url) if html: month_data parse_page(html) all_data.extend(month_data) print(f成功解析 {len(month_data)} 条记录) else: print(f失败{url}) # 每次请求之间随机休息2-4秒避免给对方服务器造成压力 time.sleep(random.uniform(2, 4)) return all_data另外还要考虑断点续采。假设程序跑到第7个月时突然断网了如果所有数据都在内存里那就全丢了得从头来。更稳妥的做法是每爬完一个页面就把数据增量写入文件import csv import os def save_checkpoint(data, filename): file_exists os.path.exists(filename) with open(filename, a, newline, encodingutf-8-sig) as f: writer csv.writer(f) if not file_exists: writer.writerow([日期, 最高温度, 最低温度, 天气, 风向风力]) writer.writerows(data)这样即使中途挂了重启后只需要从断掉的那一个月继续不用从头跑。encodingutf-8-sig有个好处用Excel直接打开CSV文件时中文不会乱码因为带上了BOM头。这个小细节很多教程不会提但实际工作中特别实用。3.3 数据清洗把字符串变成能分析的数值抓下来的原始数据是字符串比如气温是32℃这种格式没法做数学运算。清洗环节用pandas处理效率最高import pandas as pd df pd.read_csv(weather_data.csv) def clean_temperature(temp_str): # 去掉℃符号和空格转为整数 return int(temp_str.replace(℃, ).strip()) df[最高温度] df[最高温度].apply(clean_temperature) df[最低温度] df[最低温度].apply(clean_temperature)这一步踩过最大的坑是有些日期的气温字段不是正常的25℃而是空值或null。如果直接int()转换程序会直接抛异常。我后来用了更健壮的写法def safe_int(value): try: return int(value.replace(℃, ).strip()) except (AttributeError, ValueError): return None df[最高温度] df[最高温度].apply(safe_int)遇到解析不了的值先给None等后面分析时决定是填充还是丢弃。千万别在清洗阶段就把流程打断一把梭的运行方式在真实项目中是很危险的。清洗完顺手检查一下数据质量print(df.isnull().sum()) # 每列缺失值数量 print(df.describe()) # 数值列的统计描述一般看这俩输出就能判断数据靠不靠谱。比如describe()里最高温度最小值如果出现-10℃说明至少有几天数据没大问题如果出现一个离谱的99℃那多半是爬到了乱码或网站本身的脏数据需要回去核对原始页面。4. 可视化分析与数据洞察4.1 全年温度变化曲线一眼看清季节轮廓数据清洗完毕第一个可视化我建议做全年最高温、最低温的折线图。这是最直观、也最容易出效果的图import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei] plt.rcParams[axes.unicode_minus] False plt.figure(figsize(14, 6)) plt.plot(df[日期], df[最高温度], label最高温度, color#d9534f, linewidth0.8) plt.plot(df[日期], df[最低温度], label最低温度, color#428bca, linewidth0.8) plt.fill_between(df[日期], df[最高温度], df[最低温度], alpha0.1, color#5bc0de) plt.title(2024年全年温度变化曲线) plt.xlabel(日期) plt.ylabel(温度℃) plt.legend() plt.xticks(range(0, 365, 15)) plt.grid(alpha0.3) plt.tight_layout() plt.savefig(temp_curve.png, dpi150)这里有两个中文显示的关键设置必须记住plt.rcParams[font.sans-serif]指定中文字体否则图上所有中文会变成方块plt.axes.unicode_minus设置为False才能正常显示负号否则坐标轴上的负温度会显示成乱码。这两个坑几乎每个新手都得踩一次。fill_between给两条曲线之间的区域填充了半透明色视觉上能看到每天都处于一个“温度区间”。这种巧妙的填充可以让冷热交替的季节变化一眼看出来——冬天两条线贴得近而且位置低夏天位置高且拉开幅度大。4.2 月度箱线图温度分布比平均值更真实单纯看温度曲线能感受到趋势但看不出每个月内部的温度波动。这时候箱线图是更好的工具它能展示每个月最高温的中位数、四分位数、异常值一眼看出哪个月最“稳”、哪个月温差最大monthly_data [df[最高温度][df[日期].dt.month m] for m in range(1, 13)] plt.figure(figsize(12, 6)) plt.boxplot(monthly_data, labels[f{m}月 for m in range(1, 13)]) plt.title(各月最高温度分布箱线图) plt.ylabel(温度℃) plt.grid(alpha0.3) plt.tight_layout() plt.savefig(monthly_box.png, dpi150)箱线图有个非常实用的洞察价值看1月到3月那些箱子之间的距离。如果箱子长说明这一个月之内气温波动大“倒春寒”这类极端天气的事后分析就是靠这种图看出来的。4.3 天气现象统计按压降和晴天频率做日历热力图温度维度做完了再看天气现象。把“天气”列按月份聚合统计每个月的雨天数量、晴天数量是个很有意思的角度# 简单分类包含雨字归为雨天包含晴字归为晴天 df[天气大类] df[天气].apply(lambda x: 雨 if 雨 in str(x) else (晴 if 晴 in str(x) else 其他)) pivot pd.crosstab(df[日期].dt.month, df[天气大类]) pivot.plot(kindbar, stackedTrue, figsize(12, 6), colormapSet3) plt.title(各月天气类型分布) plt.xlabel(月份) plt.ylabel(天数) plt.legend(title天气类型) plt.xticks(rotation0) plt.tight_layout() plt.savefig(weather_type.png, dpi150)堆叠柱状图在这里表达力极强每个月三块颜色分别代表雨天、晴天和其他天气的天数一眼就能看出梅雨季在哪几月、秋季的晴天比例有多高。如果觉得还不够过瘾可以再做一个全年逐日天气日历热力图——横轴是月份纵轴是日期格子颜色代表当天天气类型那种图做出来非常惊艳适合发朋友圈展示项目成果。4.4 图表选型心得别让好看牺牲了信息量可视化环节做到后面我最大的感悟是图表不是越炫越好而是越“能说明问题”越好。整个项目里我只做这几类图每类都有明确的目的折线图展示连续时间维度上的变化趋势适合温度曲线。箱线图展示数据分布和异常值适合做月度对比。堆叠柱状图展示构成比例随时间的变化适合统计天气类型。散点图如果数据覆盖多年可以画温度与湿度的散点去探索相关性。如果项目后续想展示给非技术朋友看可以用pyecharts把关键图表转成交互式HTML鼠标悬停能看到具体数值效果比静态图好得多。保存成HTML文件后不需要开任何服务双击就能在浏览器里看分享给同事也很方便。5. 常见问题排查与合规安全心得5.1 反爬限制的典型表现和对策爬天气数据虽然压力不大但批量抓取时还是可能遇到反爬机制。我总结了三类最典型的问题和对应解法。第一类是“状态码403”。表现为前两个页面正常第三个页面突然拒绝访问。原因几乎都是请求频率太快。对策把请求间隔从2秒提到4秒并打乱URL访问顺序而不是按照月份顺序老老实实爬。随机化可以显著降低被识别为脚本的概率。第二类是“页面内容被替换”。正常月份返回的是表格HTML某个月返回的却是一堆JS代码。这通常是站方对频繁IP做了降级处理返回一个提示页面。对策在parse_page里加一个前置校验判断返回的HTML里是否包含预期特征比如if 最高温度 not in html: return []不匹配就直接跳过避免把垃圾数据混进结果集。第三类是“IP暂时无法访问”。如果请求频率太激进对方可能做短时间封禁。这里最实在的建议不是教你绕过去而是停下来改天再跑。毕竟我们是爬公开的历史数据不是争分夺秒抢票放慢节奏对双方都友好。5.2 数据缺失与异常值的修复思路全年数据偶尔会有缺失行比如某一天网站本身就没记录或者解析时那一行被跳过了。处理起来有两个方向如果只缺一两天用前后两天均值填充即可比如df[最高温度].interpolate()在温度这类连续型数据上效果很好。如果缺了整一个月用均值填充会严重失真建议直接把该月标注为缺失并在可视化时跳过或者去别的公开数据源补一份。异常值方面我遇到过一次某天最高温爬出来是“99℃”显然不对。这类问题要靠区间校验来抓提前设定合理范围比如最高温不高于45℃、不低于-30℃超范围的直接置为缺失值。5.3 项目合规使用的边界意识聊爬虫必须聊合规这是行业共识。这个项目里我的原则是只抓取公开的、非商业敏感的数据明确避开了需要登录、需要验证码的页面抓取频率控制在人类浏览的正常节奏不追求速度爬下来的数据仅用做个人学习与统计分析不批量转发、不做商业用途、不尝试绕过任何访问限制。在这个前提下爬虫是一个非常好的技术学习工具。它训练的是数据获取、清洗和分析的完整思路这套能力不会过时——哪怕以后天气数据源换了结构、换了技术栈核心方法论依然成立。6. 写在最后的个人体会这个项目做完之后我自己留了两个习惯一是把爬虫脚本的sleep间隔参数抽成配置项方便随时调整节奏而不是每次修改都要钻进代码里找二是每次抓完数据都顺手生成一份checkpoint.csv保留现场宁可多存文件也不要一锤子买卖。真要给后来人一个建议的话我会说别指望一次就能跑通全流程你大概率会在编码、中文显示、结构变化这些环节卡住几次但每卡一次学到的东西都比顺利时多。如果这个项目你已经能不看教程独立完成下一阶段可以考虑把时间跨度拉长到三年或者同时爬三五个城市做横向对比分析——同样的链路换个数据维度就又是个新项目了。