Python爬虫实战:从零采集全年天气数据并实现可视化分析

发布时间:2026/9/18 8:31:15
Python爬虫实战:从零采集全年天气数据并实现可视化分析 我这两年带过不少人入门Python发现一个特别有意思的现象很多人学了一堆爬虫语法requests会用了BeautifulSoup也会用了但一到自己独立做点东西就卡壳——不知道爬什么、数据存成什么样、拿到数据之后又该怎么办。所以今天想分享一个非常适合练手、又不涉及高危敏感爬取目标的完整案例用Python爬取全年天气数据并做可视化分析。这个项目的好处在于它几乎覆盖了Python爬虫数据分析的核心闭环网页结构分析、动态请求处理、数据解析、存储、清洗、可视化一整套下来你能把之前零散学的知识串成一条线。而且天气数据是公开数据爬取压力小、合规风险低非常适合作为个人作品集项目。我选择的目标是某天气网站的全年历史天气页面以北京为例逐月爬取温度、天气状况、风力风向等字段最终生成一份包含全年数据的CSV文件再用matplotlib和pyecharts做几组可视化图表比如全年最高/最低温走势、降水天数分布、各月份平均气温变化等。整个过程如果你已经掌握了Python基础语法跟着走一遍大概两个晚上能全部啃完。1. 项目整体设计与方案选型1.1 为什么选天气数据做爬虫项目很多人第一个爬虫项目会选择爬豆瓣电影Top250、爬电商商品、爬招聘网站等等这些当然也是经典案例。但天气数据有一个非常独特的优势数据结构高度规律化。一个城市的全年天气其实就是30到12张表格每张表格30到31行每行代表一天。字段固定日期、最高温、最低温、天气、风力风向没有复杂的网页布局变化不涉及登录鉴权、验证码也没有太强的反爬机制。从学习角度来说它让你能把精力集中在“如何高效获取数据”上而不是和研究反爬机制较劲。更关键的是天气数据天然适合可视化。温度变化是典型的时间序列数据你可以做折线图、热力图、柱状图、玫瑰图每一种图都能讲出有价值的信息。这就能让项目自然延伸到数据分析层面而不是“爬完就扔”。1.2 技术选型requests还是scrapy确定了目标之后下一个问题是框架选型。很多人一上来就上scrapy觉得这才算“正经爬虫”。我并不反对但其实对这个体量的项目来说scrapy反而有点杀鸡用牛刀。我用的是requests BeautifulSoup pandas pyecharts的组合理由有三个第一全年12张页面每页大小几十KB总共数据量不到1MBrequests直接串行请求完全没压力不需要scrapy的并发调度能力。第二这个项目的核心学习点是数据解析和可视化requests配合BeautifulSoup足够简洁代码量少、调试方便新手不容易被框架的概念淹没。第三scrapy的Selector语法、Item Pipeline、Middleware这些概念,对于第一次完整做项目的人来说认知负担比较重容易劝退。如果你未来要爬几十万级数据量的站点再学scrapy完全来得及。第一版项目把链路跑通优先级远高于技术栈的“天花板”。1.3 数据存储方案为什么用CSV而不是数据库数据存储我也是特意选了最简单的CSV方案。你可能会想为什么不用MySQL或者SQLite原因在于这个项目的核心目标是数据分析和可视化而pandas读取CSV只需要一行代码处理完还能直接再写回CSV全程不需要安装数据库服务、不需要写建表语句也不需要考虑连接管理。当然如果你后续想把这个项目扩展成“定时抓取天气并做年度报告”的自动化服务那时候再考虑SQLite也不迟——SQLite也是SQLite数据库文件比MySQL部署轻量得多。这里我想强调一个理念工具是服务的不是拿来炫耀的。技术选型一定要匹配项目规模一上来就上重型工具除了增加学习成本没有任何实际收益。2. 网页分析与接口定位2.1 确定目标网站与URL规律我这次用的是国内比较常见的天气历史数据站点以example-weather.com代称你在实际操作中可以用类似“天气后报”或各大气象数据站替代。这类网站的特点是有固定的城市代码和历史天气查询接口URL很有规律。以北京为例首页的URL结构大体是https://www.example-weather.com/beijing.html点进某个月的历史天气URL则是https://www.example-weather.com/beijing/202412.html这里202412就是2024年12月。也就是说我们只要把URL里的年份和月份动态拼接出来就能依次爬取全年12个月的数据。在做任何爬虫之前第一件事不是打开编辑器写代码而是先打开浏览器手动访问目标页面右键选择“检查”在Network面板里观察请求的规律。这一步很多人会忽略但它恰恰是整个爬虫项目里最关键的环节——先手动再自动。2.2 分析响应内容是HTML还是接口JSON这里有一个重要的判断点目标页面返回的是直接带数据的HTML还是通过异步接口加载JSON数据在Network面板里刷新页面找到document类型的请求点击预览Preview看看返回内容里有没有具体的气温数据。如果返回的HTML里直接包含了表格数据那我们只需要用BeautifulSoup去解析就行如果发现页面表格区域是空白的而是有一个XHR请求单独返回了JSON数据那就需要直接去模拟那个XHR接口。我这边的目标站点属于前者HTML里直接有完整的表格所以我走的是传统方案requests请求页面 - BeautifulSoup解析表格 - 提取数据。但我也建议你养成检查Network的习惯因为现在越来越多的现代网站采用Vue或React前端框架数据大概率走的是JSON接口。如果你只会解析HTML遇到这类站点就会束手无策。2.3 分析HTML结构定位数据行接下来看一下页面的HTML结构。目标页面的数据表格结构非常典型table classhistory-table tr td2024-12-01/td td晴/td td6℃/td td-4℃/td td西北风 3-4级/td /tr !-- 更多行 -- /table每个tr代表一天第一个td是日期第二个是天气状况第三个是最高温度包含单位符号第四个是最低温度第五个是风力风向。我们只需要在浏览器里定位到这个表格后面的解析代码就水到渠成了。很多新手在解析HTML时容易犯一个错误把结构记得死死的然后把选择器写死。但实际运行时发现字段对不上比如有的行td数量不对第一个td里的日期格式有变化等。这里提前打个预防针真实网页数据远没有示例里那么规整写解析代码的时候一定要做容错处理下面我会详细展开。3. 爬虫代码实现与数据采集3.1 基础环境准备开始写代码之前先把环境准备好。我假设你已经完成了Python的安装建议Python 3.9及以上版本接下来创建项目目录和虚拟环境mkdir weather_spider cd weather_spider python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate然后安装依赖库pip install requests beautifulsoup4 pandas matplotlib pyecharts我把pyecharts也一并装上了后面做交互式图表用。如果你只想要最基础的静态图matplotlib就够了但pyecharts生成的HTML图表在展示时更好看适合做作品集。如果你想在Jupyter环境里跑也可以直接pip install jupyter不过我自己的习惯是写一个独立的.py脚本执行输出CSV之后再开一个脚本做可视化分析这样职责分开调试起来也更省心。3.2 完整爬虫代码单月页面为例单月页面的爬取代码我拆成几个片段来讲方便你理解每部分的作用。首先是请求头设置和请求逻辑import requests from bs4 import BeautifulSoup import time HEADERS { 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, Accept-Language: zh-CN,zh;q0.9, Accept-Encoding: gzip, deflate, br, } def fetch_page(url: str) - str | None: 通用的请求函数负责发请求并返回HTML文本 try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding # 防止中文乱码 return resp.text except requests.RequestException as e: print(f[错误] 请求失败: {url}, 原因: {e}) return None这里有几个细节值得注意timeout10是对每个请求设置超时时间避免某个页面卡住导致整个脚本停在那里。resp.encoding resp.apparent_encoding很关键很多天气站点的页面编码是GBK或GB2312如果你直接用resp.text中文大概率乱码这是因为requests会猜编码但猜得不一定准。用apparent_encoding基于页面内容做推断能避免这个问题。不过apparent_encoding也有代价它需要额外读取一部分内容来推断编码稍微慢一点点。在这个体量的项目里完全无感。接下来是页面解析逻辑def parse_month_page(html: str, year: int, month: int) - list[dict]: 解析单月天气页面返回当月的天气数据列表 soup BeautifulSoup(html, html.parser) table soup.find(table, class_history-table) if table is None: print(f[警告] {year}年{month}月页面中没有找到历史数据表) return [] rows table.find_all(tr) data [] for row in rows: cells row.find_all(td) # 有些tr可能是表头td数量不对就跳过 if len(cells) 5: continue try: date_text cells[0].get_text(stripTrue) weather cells[1].get_text(stripTrue) high_temp_raw cells[2].get_text(stripTrue) low_temp_raw cells[3].get_text(stripTrue) wind cells[4].get_text(stripTrue) # 清洗温度字段去掉℃符号 high_temp high_temp_raw.replace(℃, ).strip() low_temp low_temp_raw.replace(℃, ).strip() data.append({ date: date_text, weather: weather, high_temp: high_temp, low_temp: low_temp, wind: wind, }) except Exception as e: # 单独一行解析失败不应影响整体 print(f[警告] 解析某行数据时出错: {e}, 原始内容: {cells}) continue return data解析逻辑的核心是find_all(td)按位置索引取值。这里我顺手处理了两种情况一是表头行通常没有5个td通过长度判断直接跳过二是温度字段里的“℃”符号先替换掉方便后面转成数值类型做分析。3.3 爬取全年数据的主逻辑单月搞定之后全年就简单了循环12次就行。这里我要特别强调一下请求间隔def crawl_year(city_code: str, year: int) - list[dict]: all_data [] base_url fhttps://www.example-weather.com/{city_code}/{year} for month in range(1, 13): url f{base_url}{month:02d}.html print(f[信息] 正在抓取 {year}年{month}月 ...) html fetch_page(url) if html: month_data parse_month_page(html, year, month) all_data.extend(month_data) print(f[信息] {year}年{month}月 获取到 {len(month_data)} 条数据) # 每两个请求之间休眠1到2秒降低对目标站点的压力 time.sleep(1 (month % 2)) return all_datamonth:02d的作用是把月份格式化为两位数比如1变成01。这样拼出来的URL就是202401.html非常规律。time.sleep(1 (month % 2))这个写法可能有人觉得奇怪其实我这是为了让请求间隔有一点随机性避免每两个请求之间的间隔完全相同从而降低被识别为脚本的概率。虽然天气网站的反爬并不严格但养成带节奏请求的习惯是好的以后做其他项目会感谢这个习惯。3.4 数据存储到CSV把数据CSV落地用Python标准库csv就能搞定也可以直接用pandasimport pandas as pd def save_to_csv(data: list[dict], filename: str beijing_weather_2024.csv): df pd.DataFrame(data) df.to_csv(filename, indexFalse, encodingutf-8-sig) print(f数据已保存至 {filename}共 {len(df)} 条记录)这里有两个点要提encodingutf-8-sig比单纯的utf-8更适合CSV因为Excel打开UTF-8编码的CSV文件时会出现中文乱码而带BOM头utf-8-sig的格式能被Excel正确识别。如果你后面想用Excel预览数据这个细节能帮你省去不少麻烦。至于用pandas还是标准库csv我个人建议直接上pandas因为后面清洗、统计分析、可视化全都依赖pandas数据集在这种规模下内存开销完全不是问题提前熟悉DataFrame操作对后续的学习路径也更顺畅。4. 数据清洗与全年温度的可视化4.1 数据质量分析与清洗数据爬下来只是第一步接下来才是体现“分析”价值的部分。首先读取CSV看看数据的基本情况import pandas as pd df pd.read_csv(beijing_weather_2024.csv, encodingutf-8-sig) print(df.head()) print(df.info()) print(df.describe())如果你跟着上面的代码走到了这一步大概率会发现几个问题温度字段是字符串类型而且最小值可能出现负值比如“-4℃”被清洗后变成了“-4”还是字符串需要转成数值。极少数日期的天气、风力字段可能为空可能是页面本身缺数据也可能是解析时那行被跳过了。可能有重复行因为万一你跑了两遍爬虫且没有清空旧数据CSV里就存在重复记录。针对这些典型问题我写一个通用的清洗流程# 1. 去掉完全重复的行 df df.drop_duplicates(subsetdate) # 2. 温度字段转为数值类型出错时强制设为NaN df[high_temp] pd.to_numeric(df[high_temp], errorscoerce) df[low_temp] pd.to_numeric(df[low_temp], errorscoerce) # 3. 删除温度缺失的行 df df.dropna(subset[high_temp, low_temp]) # 4. 日期列转成标准datetime类型 df[date] pd.to_datetime(df[date]) # 5. 新增月份、星期等衍生列后面做统计时直接用 df[month] df[date].dt.month df[weekday] df[date].dt.day_name() print(df.info()) print(df.head())pd.to_numeric适配了“-4”和“6”这类纯数字字符串向数值的转换errorscoerce意味着一旦遇到“N/A”或“未知”这类无法转换的内容直接变成NaN不会导致整个程序崩溃。这一步做完你的DataFrame就是一个干净、可分析的标准化数据表了。4.2 全年温度走势折线图matplotlib先来最经典的一张图全年每天的最高气温和最低气温走势。用matplotlib实现非常简单import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 让中文正常显示 plt.rcParams[axes.unicode_minus] False # 让负号正常显示 fig, ax plt.subplots(figsize(16, 6)) ax.plot(df[date], df[high_temp], label最高气温, linewidth1) ax.plot(df[date], df[low_temp], label最低气温, linewidth1) ax.set_title(2024年北京每日最高/最低气温走势) ax.set_xlabel(日期) ax.set_ylabel(温度 (°C)) ax.legend() ax.grid(True, alpha0.3) plt.tight_layout() plt.savefig(temp_trend.png, dpi150) plt.show()这里有一个非常容易踩的坑matplotlib默认字体不支持中文。如果你不设置rcParams[font.sans-serif]画出来的中文标题全部是方块。我用的SimHei是Windows自带的黑体如果你在macOS上跑可以改成Arial Unicode MS或者PingFang SC。Linux服务器上可能需要先安装中文字体比如fonts-wqy-zenhei。4.3 各月份平均温度柱状图折线图适合看趋势柱状图适合看对比。接下来统计每个月的平均最高温度和平均最低温度monthly_stats df.groupby(month).agg( avg_high(high_temp, mean), avg_low(low_temp, mean), ).round(1) print(monthly_stats) fig, ax plt.subplots(figsize(12, 5)) months range(1, 13) width 0.4 ax.bar([m - width/2 for m in months], monthly_stats[avg_high], width, label平均最高气温) ax.bar([m width/2 for m in months], monthly_stats[avg_low], width, label平均最低气温) ax.set_xticks(list(months)) ax.set_xticklabels([f{m}月 for m in months]) ax.set_title(2024年北京各月平均气温) ax.set_ylabel(温度 (°C)) ax.legend() ax.grid(axisy, alpha0.3) plt.tight_layout() plt.savefig(monthly_avg_temp.png, dpi150) plt.show()这组图做出来之后你能非常直观地看到一年内的温度变化节奏冬季12月到次年2月平均气温在零度上下夏季6月到8月平均最高温超过30度春秋两季过渡明显。这张图其实特别适合用来“讲故事”比如结合我们前面爬到的天气数据可以发现北京冬季昼夜温差大、夏季温差相对小这就是典型的温带季风气候特征。分析的价值就在这种细节里。4.4 天气现象分布与风玫瑰图pyecharts接下来是天气状况的统计。用value_counts就能统计出全年出现最多的天气现象weather_counts df[weather].value_counts() print(weather_counts.head(10))在真实数据里你会发现“晴”“多云”“阴”“小雨”这类天气占主导偶尔有“雪”“雾”“霾”等极端天气。用pyecharts做一个饼图展示各天气现象的占比from pyecharts.charts import Pie from pyecharts import options as opts weather_data [list(z) for z in zip(weather_counts.index, weather_counts.values)] pie ( Pie() .add(, weather_data, radius[35%, 65%]) .set_global_opts( title_optsopts.TitleOpts(title2024年北京天气现象分布), legend_optsopts.LegendOpts(orientvertical, pos_top15%, pos_left2%), ) .set_series_opts(label_optsopts.LabelOpts(formatter{b}: {c}天 ({d}%))) ) pie.render(weather_pie.html)pyecharts的语法是链式调用初看有点陌生但其实逻辑很清晰add()添加数据set_global_opts()设置全局配置set_series_opts()设置系列标签格式。生成的weather_pie.html用浏览器打开就是一张可交互的图表鼠标悬停能看到每一项的具体数值和百分比。这个交互式HTML在作品集展示的时候很有优势直接点开就能看比贴一张静态图显得用心很多。4.5 风力风向与极端天气的分析如果页面数据里包含风力风向你还可以做一个更有意思的图表统计各风向出现的频率画风频玫瑰图。虽然pyecharts没有直接提供风玫瑰图的组件但可以用极坐标图Polar实现思路是把风向分成16个方位统计每个方位的出现天数然后映射到极坐标上。我这里因为数据字段是“西北风 3-4级”这种带等级和方位的文本需要先拆分出风向import re def extract_wind_direction(wind_text: str): 从 西北风 3-4级 中提取 西北风 match re.match(r([东南西北]风), wind_text) return match.group(1) if match else None df[wind_direction] df[wind].apply(extract_wind_direction) wind_dir_counts df[wind_direction].value_counts() print(wind_dir_counts)这一步的意义在于你能发现某个城市全年主导风向是什么。以北方的城市为例大概率“北风”和“西北风”出现频率会很高这其实就反映了季风气候下冬季偏北风的特点。如果你对气象学感兴趣还可以把风向和温度进一步交叉分析比如看看“东南风”出现的月份是不是集中在夏季、温度是否偏高这就是数据挖掘的雏形了。5. 常见问题与排查技巧实录5.1 请求头缺失导致403或503很多新手第一步就卡在请求上requests一访问目标网站就返回403或503。绝大多数原因就是缺少User-Agent或User-Agent太老。我之前见过有人直接用requests.get(url)而不设置headers然后被网站拦截还以为是网站反爬太强。其实大部分普通网站只是校验了User-Agent是否来自真实浏览器。解决办法就是像前面示例代码那样把浏览器的User-Agent复制到请求头里。如果你不确定自己浏览器的User-Agent是什么直接在浏览器地址栏输入about:version或者在Network面板任选一个请求查看。到了这个阶段普通静态页面基本都能搞定。如果遇到更复杂的反爬策略再去研究Cookie和Session的维持策略。5.2 中文乱码的排查思路爬下来HTML之后用BeautifulSoup解析发现中文全是“锟斤拷”类的乱码。这个问题的根源是响应内容的编码和requests默认猜测的编码不一致。排查时先用代码看看实际编码resp requests.get(url, headersHEADERS, timeout10) print(resp.encoding) # requests猜的编码 print(resp.apparent_encoding) # 基于内容推断的编码 print(resp.headers.get(Content-Type))一般来说resp.apparent_encoding比resp.encoding更可靠。如果发现页面声明是charsetgbk而resp.encoding显示的是ISO-8859-1那就手动指定resp.encoding gbk在天气网站这个案例里resp.apparent_encoding已经能解决问题。但如果以后遇到反爬站点故意在响应头里提供错误的Content-Type来迷惑爬虫就需要自己去检测编码了。5.3 解析数据为空或字段对不上解析时拿不到数据、或者find_all(td)返回空列表这种情况要先确认你登录页面的身份和脚本可能不一样。有些网站在浏览器里看到的是完整数据但requests请求过去之后返回的是“请开启JavaScript”或者“访问频繁”的提示页。这时候有两个排查方向。第一把请求到的HTML保存到本地文件用文本编辑器打开看看内容到底是什么html fetch_page(url) with open(debug.html, w, encodingutf-8) as f: f.write(html)然后直接搜索“表格”或“history-table”这些关键词确认页面里是否真的有你要的表格。如果页面已经明确写了“需要开启JavaScript”那就说明数据是动态加载的你得换方案。第二如果页面里有表格但结构和预期不一致比如字段顺序变了、增加了列那就用BeautifulSoup先把表格里的所有行打印出来观察soup BeautifulSoup(html, html.parser) table soup.find(table, class_history-table) for row in table.find_all(tr)[:3]: cells [c.get_text(stripTrue) for c in row.find_all(td)] print(cells)看到真实结构之后再调整索引位置基本上5分钟就能定位问题。5.4 请求过程中被限速或封IP虽然天气网站相对友好但如果你循环请求速度过快依然可能触发简单的限流机制。症状是前几个页面正常后面开始返回429状态码或直接超时。我的经验是三个层次来应对。第一层是礼貌爬取主动降低请求频率像前面代码里那样在每两个请求之间加time.sleep()。第二个层次是加入重试机制遇到429或5xx的情况退避重试可以用简单的循环实现也可以直接用tenacity库或者requests的Retry机制。第三个层次是使用代理池但这个项目完全不需要就不多展开了。在这里我特别想提醒一句设置合理的请求间隔不是怂是专业。我们写爬虫是为了学习和分析数据不是为了让某个网站的服务挂掉。一个合格的爬虫工程师首先考虑的是对目标站点的负载影响。5.5 爬虫合规与道德边界问题最后聊一个非常重要的话题因为这关系到这个技能能走多远。我在搜索热词里看到“因爬虫入狱”这个词上了热门。确实爬虫不小心踩到红线后果是非常严重的。但这个话题不能一概而论地说“爬虫会坐牢”吓退新手而是要搞清楚边界在哪里。我的理解是爬虫本身的工具属性是中性的关键看我爬取什么数据、用来做什么。这三点是我给自己定的铁律公开的、非敏感的数据优先。企业单位内部的、需要登录才能看到的、涉及个人隐私的、明确有版权声明禁止抓取的内容一律不碰。如果网站有robots.txt先去看它允许什么不允许什么。虽然robots.txt是君子协议技术上可以不遵守但这体现了基本的网络礼仪。爬取下来的数据只用于个人学习、分析和研究不用于商业变现更不用于二次打包贩卖。项目里如果有涉及登录后才能访问的数据比如个人订单、通讯录、私信等果断放弃。这类数据一旦爬取性质就完全不同了哪怕只是测试也绝不能碰——轻则违法重则坐牢这个说法不是我危言耸听而是有真实案例的。回到我们的天气数据项目它安全的原因在于数据公开、匿名可访问、更新频率低、存储压力小、无版权争议完全符合上面的所有原则。这也是我推荐新手做这个项目的重要原因之一——既能练手又干净安全。6. 项目扩展方向与后续优化思路如果你顺利跑完了上面所有代码并且成功生成了全年温度趋势图和交互式图表你已经完成了第一个“爬虫分析”的完整闭环。但如果只是做到这一步就收工我觉得有点可惜这个项目还有很大的扩展空间。一个简单的方向是做一个简单的Streamlit或Flask网页应用把CSV读取、图表展示集成到一个页面上选择不同城市、年份就能看到对应图表。这样你的项目就不只是脚本而是一个可以给别人演示的小应用。另一个方向是换成某个区域内的多城市横向对比比如同时爬北京、上海、广州、成都、哈尔滨五个城市分析不同城市的气候差异。这需要你抽象出一套通用的“城市配置”把城市代码和名称做成参数传进去代码复杂度上升一点但项目层次立刻就不同了。如果你想把爬虫本身做深可以研究一下把requests换成httpx或aiohttp做异步并发同时请求多个城市的数据体会一下并发带来的性能提升。或者给爬虫加上定时调度用APScheduler或GitHub Actions每个月自动更新一次数据形成持续积累的长期时间序列。这些方向我都试过铺开说每一项都可以再写一篇长文。我个人的建议是如果你时间有限优先做多城市对比因为它的投入产出比最高——代码改动不大但数据分析的价值明显提升“从单点催生网络”这才是数据分析应该有的思维。根据我自己的实操经验这个项目最适合的定位就是你学习路上的“第一个完整闭环”。它把请求、解析、存储、清洗、可视化五个环节串在了一起技术上不复杂但每一步都踩在真实项目的节奏上。做完一遍之后你可能会有一种“哦原来爬虫数据分析是这么回事”的感觉。那就对了。后面再遇到任何爬虫需求本质上都是“找URL规律、分析响应结构、写解析逻辑、清洗入库、做可视化”这个固定流程的不同变体罢了。如果你在实操中遇到问题卡在某个报错上欢迎带着具体的报错信息来交流我看到会回复。实战项目就是这样一个人闷头写容易钻牛角尖有人点拨一下往往立刻豁然开朗。