ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

天气数据爬虫与可视化实战:从requests到pyecharts

2026/9/28 17:09:30 拓冰建站 浏览量
天气数据爬虫与可视化实战:从requests到pyecharts 1. 项目整体设计与思路拆解1.1 为什么选天气数据做实战项目决定用天气数据作为爬虫和可视化的练手项目不是拍脑袋选出来的而是横向对比了好几个方向之后才定的。很多朋友学爬虫第一反应是去爬电商平台、爬社交媒体结果一上手就碰到登录态验证、滑块验证、字体反爬甚至WAF拦截心态直接崩了。天气数据不一样它在技术难度和项目完成度之间找到了一个很舒服的平衡点。天气数据自带几个天然优势。首先数据源公开且稳定大部分天气网站不需要登录设置一个User-Agent就能正常访问页面。其次数据结构高度规整日期、最高温度、最低温度、天气现象、风向风力都是现成的表格字段解析逻辑非常直接。第三可视化效果极好温度曲线、温差柱状图、天气状况饼图做出来以后信息层次丰富随便挑几张放在作品集里都很有说服力。更重要的一点是这个项目覆盖了一条完整的技术链路发起请求、解析网页、清洗数据、存储数据、可视化展示。你在一个项目里把这五个环节全部跑通以后再接任何爬虫或者数据处理类的需求心里都有一条清晰的路线图。1.2 整体技术架构与数据流转整个项目不依赖任何重量级框架就是一个Python脚本串起几个非常经典的库。数据流转过程是这样的requests发送HTTP请求拿到目标网页的HTML源码BeautifulSoup把HTML解析成结构化数据pandas负责清洗和规范化最后matplotlib和pyecharts把数据转成图表。技术选型上我没有用Selenium或Scrapy原因是天气网站大多不需要执行JavaScript就能把数据渲染出来用requests就足够了。Selenium虽然能解决动态渲染的问题但每次跑任务都要启动一个浏览器实例内存开销大、速度慢。Scrapy本身是一个很强大的爬虫框架但对这个项目来说有点重而且框架的学习曲线会分散练手时的注意力。用requests加BeautifulSoup的组合能把注意力集中在分析页面和处理数据这两件最核心的事上。1.3 数据源选择与合规底线市面上的天气数据渠道大致有四类官方气象接口、商业天气API、天气网站页面、第三方开放平台。官方接口权威但申请流程繁琐商业API返回的是干净的JSON数据但免费额度往往有限不适合频繁请求天气网站页面和开放平台更灵活适合做技术练习。我在项目里主要用的是天气网站公开的历史天气页面。用这类页面之前有几个底线问题必须先想清楚第一是访问频率我一般控制在每秒不超过一次请求做多页面抓取时加上随机延迟第二是只做个人学习研究不以任何形式对外发布或售卖爬取的数据第三是如果网站有明确的robots协议或用户协议禁止爬取就换一个数据源没必要硬碰硬。提示爬虫本身不是坏事但用爬虫的前提是尊重数据提供方的规则。公开数据的个人学习使用没有大问题涉及商业化用途则务必先获得授权。2. 核心工具选型与关键原理解析2.1 requests最简单的HTTP客户端爬虫的本质就是模拟浏览器向服务器发送HTTP请求并接收响应。Python生态里做这件事的库有很多但从易用性来说requests是当之无愧的第一选择。它把复杂的HTTP协议细节封装成了简单的方法调用一个requests.get(url, headersheaders)就能完成GET请求。实际使用中有几个细节非常关键。第一是User-Agent请求头必须设置很多网站虽然没有严格的反爬但会拦截没有浏览器标识的请求头直接返回403第二是timeout参数必须加不加超时时间的话网络一波动程序就会一直挂在那里我用5到10秒既能容忍网络波动又不会卡死第三是编码处理必须留意中文网站的编码可能是utf-8也可能是gbk手动指定响应编码比依赖自动猜测要稳妥得多。import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36, Referer: https://example-weather-site.com/, Accept-Language: zh-CN,zh;q0.9,en;q0.8 } def fetch_page(url): try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.text except requests.RequestException as e: print(f请求失败 [{url}]: {e}) return Noneraise_for_status()是一个容易被忽略的好习惯。它会在状态码不是200的时候主动抛出异常让你及时发现请求失败而不是傻乎乎地继续解析一个错误页面。2.2 BeautifulSoup页面解析的操作台拿到HTML之后接下来的任务就是从一堆标签中把天气数据捞出来。BeautifulSoup是最适合这个场景的解析库它的核心操作就三件事找到目标标签、定位具体内容、提取文本。from bs4 import BeautifulSoup def parse_weather_data(html): if not html: return [] soup BeautifulSoup(html, html.parser) rows soup.select(table tbody tr) weather_list [] for row in rows: cells row.find_all(td) if len(cells) 4: continue weather_list.append({ 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) }) return weather_list有人可能想用正则直接从HTML里匹配数据我试过非常痛苦。正则面对结构化嵌套标签时容易变成一团乱麻而BeautifulSoup把标签树变成Python对象之后查找和提取都是顺理成章的操作。如果网页结构比较复杂还可以换成lxml解析器容错性和性能都比Python内置的html.parser更好。2.3 pandas数据清洗的中转站爬虫得来的数据往往是原生状态——温度挂着单位、日期格式混乱、偶尔混进几行空值。pandas在这个项目里扮演的是数据中台的角色把脏数据整理成干干净净的DataFrame供后续绘图使用。import pandas as pd df pd.DataFrame(raw_data) df[high_temp] df[high_temp].str.replace(℃, , regexFalse).astype(int) df[low_temp] df[low_temp].str.replace(℃, , regexFalse).astype(int) df.dropna(inplaceTrue) df[date] pd.to_datetime(df[date])这里的每一步清洗都有实际意义。去掉温度单位是为了让数据能做算术运算转成整型是为了后面计算温差时不用处理字符串拼接去空值是为了防止画图时在数据中间出现断点日期格式化则是为了在时间轴上正确排序和展示。2.4 可视化库怎么选matplotlib与pyecharts这个项目里我用了两个可视化库很多人会问为什么要弄两套一个行不行答案是可以但两套组合的效率更高。matplotlib是Python可视化的基础库适合生成静态图片灵活度极高几乎能画任何类型的图表。缺点是交互性弱做出来的图是死的读者只能看最终的渲染结果。pyecharts是Apache ECharts的Python封装输出的是交互式HTML页面鼠标悬停能看到具体数值图例可以点击切换还能缩放时间轴在演示和汇报场景下视觉效果更好。实际项目里我这样分工matplotlib负责快速出图看数据质量和变化趋势pyecharts负责出最终成品做汇报展示。两者配合既保证了开发时的调试效率又确保了交付时的观赏性。3. 天气数据爬取实现细节3.1 抓取规划明确URL结构和字段在动手写码之前先花时间规划抓取目标能省掉后面大半天的返工时间。首先要明确需要哪些字段我定的字段是日期、最高温度、最低温度、天气状况、风向风力这五个既覆盖了做分析的核心维度又不会因为字段太多而让解析逻辑变得复杂。然后要人工去目标网站研究URL结构。大部分天气网站的历史页面URL都有规律可循通常包含城市ID和日期参数。开浏览器打开目标页面按下F12进入开发者工具找到承载天气数据的表格元素确认它的选择器路径。这一步花10分钟比后面写代码调试1小时划算得多。3.2 多页面遍历与延迟控制要多抓90天的数据通常需要访问多个页面。这个时候就要写一个循环遍历URL列表并且要做好延迟控制。延迟不是可有可无的访问频率太高一方面可能触发网站的反爬另一方面也给别人服务器造成不必要的负担。import time import random def crawl_weather_data(city_url_map): all_data [] for city, base_url in city_url_map.items(): print(f开始爬取 {city} 的天气数据) for page in range(1, 4): page_url f{base_url}?page{page} html fetch_page(page_url) if html: data parse_weather_data(html) all_data.extend(data) print(f第 {page} 页完成累计 {len(all_data)} 条) time.sleep(random.uniform(1, 3)) print(f{city} 抓取结束) return all_datarandom.uniform(1, 3)这个随机间隔是实战中常用的手段固定间隔的请求模式像机器行为一个随机区间能让请求更像人工浏览同时也能保护目标服务器的稳定性。3.3 解析逻辑的容错设计爬虫代码最容易翻车的地方就是网页结构微调。我在写解析函数的时候会给每一行数据的解析套上异常捕获同时加上对字段数量的校验。某一行的单元格数量不够或者格式异常就跳过这一行继续处理下一行不因为单个脏数据导致整个程序中断。def parse_weather_table(html): soup BeautifulSoup(html, html.parser) rows soup.select(div.weather-table table tr) result [] for row in rows[1:]: # 跳过表头行 cells row.find_all([td, th]) if len(cells) 5: continue try: result.append({ date: cells[0].get_text(stripTrue), weather: cells[1].get_text(stripTrue), high_temp: cells[2].get_text(stripTrue).replace(℃, ), low_temp: cells[3].get_text(stripTrue).replace(℃, ), wind: cells[4].get_text(stripTrue) }) except Exception as e: print(f单行解析失败已跳过: {e}) continue return result这种容错设计本质上是一种局部失败不影响整体的思路。爬虫面对的是不可控的外部网页永远不要假设数据是完美的要做的是让程序在坏数据面前依然能稳定产出好数据。3.4 用CSV保存中间结果数据解析完成后先存成CSV文件这一动作有两个目的一是作为中间产物方便人工打开检查数据质量二是后续做可视化的时候可以直接读文件不需要重新爬一遍网页省时省力。def save_to_csv(data, filenameweather_data.csv): df pd.DataFrame(data) df.to_csv(filename, indexFalse, encodingutf-8-sig) print(f数据已保存至 {filename}共 {len(df)} 条)utf-8-sig这个编码值得单独说一句。CSV文件如果用纯utf-8编码保存用Excel打开时中文很容易乱码而utf-8-sig会在文件头部写入一个BOM标记Excel等工具能够正确识别。这个细节看起来小实际交付数据时经常决定别人对你工作成果的第一印象。4. 数据清洗与预处理4.1 清洗流程的四个标准步骤爬下来的数据一定不能直接拿去画图必须先过一遍清洗流程。我在这个项目里总结出四个标准步骤去重、格式化、缺失值处理、异常值过滤。去重一般用drop_duplicates(subset[date])就行因为同一日期重复出现一两条是非常常见的。格式化包括把日期解析成datetime类型、把带单位或符号的温度转成纯数字。缺失值处理要视情况而定——如果缺的数据量很少直接删除影响不大如果某几天连续缺失可以考虑线性插值。异常值过滤是针对那种超出常识范围的数据比如最高温度在45℃以上或者最低温度在-45℃以下大概率是解析错位导致的需要排查。def clean_weather_data(df): # 1. 去重 df df.drop_duplicates(subset[date], keepfirst) # 2. 日期标准化 df[date] pd.to_datetime(df[date], errorscoerce) # 3. 温度数值化 for col in [high_temp, low_temp]: df[col] ( df[col].astype(str) .str.replace(℃, , regexFalse) .str.replace(°, , regexFalse) .astype(float, errorsignore) ) # 4. 剔除无效行和异常值 df df.dropna(subset[date, high_temp, low_temp]) df df[(df[high_temp] 45) (df[low_temp] -45)] # 5. 按日期排序 df df.sort_values(date).reset_index(dropTrue) return dferrorscoerce是个很实用的参数转换失败时会把异常值变成NaN而不是抛异常配合后面的dropna就能实现自动兜底。4.2 派生字段计算每日温差清洗完原始字段后我习惯再生成一个派生字段——每日温差也就是当天的high_temp - low_temp。这个字段看起来简单分析价值却很大。昼夜温差大的日期往往是晴朗天气人体体感变化明显温差小的日期通常意味着阴天或持续云层覆盖。连续观察温差曲线能对比出不同季节的气候特征。画成柱状图后一眼就能看出哪个时间段气候比较平稳、哪个时间段气温波动剧烈。df[temp_diff] df[high_temp] - df[low_temp]4.3 清洗后的质量确认清洗完成后我会用两行代码做快速确认绝不跳过这一步直接画图print(df.describe()) print(df.head())describe()会给出数据量的统计信息包括最大值最小值平均值温度的最大最小值如果不在合理范围内说明清洗环节有漏洞需要回去检查。head()能看到前几行的格式是否正确。这两行代码跑完数据能不能用心里基本就有数了。5. 数据可视化实现从静态到交互5.1 matplotlib绘制温度趋势带第一个核心图表是温度趋势图。我没有只画两条光秃秃的折线而是用fill_between把最高温和最低温之间的区域填充成半透明色形成温度带的视觉效果。人眼对面积的敏感度远高于对线条的敏感度加了填充之后图表的信息传达效率会显著提升。import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] matplotlib.rcParams[axes.unicode_minus] False def plot_temperature_line(df): fig, ax plt.subplots(figsize(12, 6)) ax.plot(df[date], df[high_temp], color#e74c3c, linewidth2, label最高温度) ax.plot(df[date], df[low_temp], color#3498db, linewidth2, label最低温度) ax.fill_between(df[date], df[low_temp], df[high_temp], color#f39c12, alpha0.2) ax.set_title(近90天温度变化趋势, fontsize16) ax.set_xlabel(日期) ax.set_ylabel(温度 (℃)) ax.legend() ax.grid(True, linestyle--, alpha0.3) plt.tight_layout() plt.savefig(temperature_trend.png, dpi150) plt.show()中文显示处理的这两行rcParams几乎是必写的Python的matplotlib默认字体不支持中文不加这两行图里的标题和标签会变成一行行方框。axes.unicode_minus控制的是坐标轴上负号的显示温度数据在冬季经常出现负数这个配置一定要有。5.2 matplotlib绘制天气分布饼图温度的时序图解决了怎么变化的问题天气分布饼图回答的则是什么天气占了多大比例的问题。统计weather字段的value_counts()再绘制成饼图比例关系一目了然。def plot_weather_pie(df): # 将细分类别合并成大类便于展示 def categorize(w): if 雨 in w: return 雨天 elif 云 in w or 阴 in w: return 云系天气 elif 晴 in w: return 晴天 else: return 其他 df[weather_category] df[weather].apply(categorize) weather_counts df[weather_category].value_counts() plt.figure(figsize(8, 8)) plt.pie(weather_counts, labelsweather_counts.index, autopct%1.1f%%, startangle90, counterclockFalse) plt.title(天气状况分布) plt.axis(equal) plt.savefig(weather_pie.png, dpi150) plt.show()如果直接拿原始天气字段去画饼图很可能会出现七八种细分类别饼图切得像乐高积木一样碎。把小雨中雨雷阵雨合并为雨天把多云阴合并为云系天气分类一下就清晰了。5.3 pyecharts制作交互式温度图表静态图适合嵌进文档和报告但如果你想做汇报演示交互图的体验是静态图比不了的。用pyecharts渲染出来的HTML页面鼠标悬停能显示精确数值点击图例可以隐藏或显示某条曲线这些交互动作会让汇报过程生动很多。from pyecharts.charts import Line from pyecharts import options as opts def plot_weather_echarts(df): dates df[date].dt.strftime(%Y-%m-%d).tolist() high_list df[high_temp].tolist() low_list df[low_temp].tolist() line Line() line.add_xaxis(dates) line.add_yaxis( 最高温度, high_list, is_smoothTrue, linestyle_optsopts.LineStyleOpts(width3, color#e74c3c), itemstyle_optsopts.ItemStyleOpts(color#e74c3c) ) line.add_yaxis( 最低温度, low_list, is_smoothTrue, linestyle_optsopts.LineStyleOpts(width3, color#3498db), itemstyle_optsopts.ItemStyleOpts(color#3498db) ) line.set_global_opts( title_optsopts.TitleOpts(title近90天温度变化趋势交互版), tooltip_optsopts.TooltipOpts(triggeraxis), legend_optsopts.LegendOpts(pos_leftright), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate45)) ) line.set_series_opts( markpoint_optsopts.MarkPointOpts( data[opts.MarkPointItem(type_max, name最高点), opts.MarkPointItem(type_min, name最低点)] ) ) line.render(temperature_echarts.html)set_series_opts里的MarkPointOpts给图表加了最大最小值标记点汇报的时候自动标注峰值和谷值解读图表时省去很多口舌。5.4 多图组合的综合分析展示单独一张图的局限性在于只展示了一个维度的信息。如果手头有一份90天的完整数据集我更建议做一个组合视图把温度趋势、温差柱状图、天气分布放到同一张画布里形成一份完整的数据读报。def plot_combined_analysis(df): fig, axes plt.subplots(3, 1, figsize(12, 15)) # 子图1温度带 axes[0].plot(df[date], df[high_temp], color#e74c3c, label最高温) axes[0].plot(df[date], df[low_temp], color#3498db, label最低温) axes[0].fill_between(df[date], df[low_temp], df[high_temp], color#f39c12, alpha0.2) axes[0].set_title(温度变化趋势) axes[0].legend() # 子图2温差柱状图 axes[1].bar(df[date], df[temp_diff], color#2ecc71, alpha0.6) axes[1].set_title(每日昼夜温差) axes[1].set_ylabel(温差℃) # 子图3天气饼图 df[weather_category] df[weather].apply(lambda w: 雨天 if 雨 in w else (晴天 if 晴 in w else 其他)) weather_counts df[weather_category].value_counts() axes[2].pie(weather_counts, labelsweather_counts.index, autopct%1.1f%%, startangle90) axes[2].set_title(天气状况分布) plt.tight_layout() plt.savefig(weather_analysis.png, dpi150) plt.show()这种组合型大图最适合放到项目总结、作品集或者面试展示材料里它说明你不只是会调用API画个图而是能围绕同一个数据对象从多个角度构建完整的信息表达。6. 常见问题与排查技巧实录6.1 网页结构改版导致解析失败爬虫写完之后最常遇到的问题就是第二天运行时报错或者解析结果全是空值。九成情况是目标网站的页面结构改了CSS类名变了、标签嵌套层级变了、原本放在表格里的数据改成了JavaScript动态渲染。排查思路如下先用浏览器打开页面确认数据还在然后用开发者工具看当前的结构把代码里的选择器和新结构对齐。现象可能原因快速处理解析结果为空列表选择器类名/层级变化用开发者工具重新定位数据顺序错乱表格新增/删除行列检查单行单元格数量个别字段缺失新增了合并单元格增加缺失值兜底逻辑请求返回403User-Agent被识别更新请求头并降低频率6.2 编码乱码的解决思路中文网站最常见的编码是utf-8和gbk。requests默认会依据响应头里的charset解码但很多页面的响应头并不标注或者标注的与实际不符。最简单的调试办法是先打印resp.apparent_encoding看看自动检测的结果再手动指定正确的编码。# 自动检测后手动指定 resp.encoding resp.apparent_encoding如果发现内容还是乱码就打印resp.text的前200个字符看看乱码往往一眼就能判断出是编码问题而不是网络问题。6.3 被反爬后的应对方法天气网站的反爬不算严格但持续高频请求是肯定会触发限制的。我遇到过的情况主要是返回403或509状态码。应对方式很简单降低访问频率、增加随机延迟、补全请求头必要时用requests.Session()保持连接会话。注意应对反爬的正确逻辑是降低频率、模拟人工浏览而不是写个死循环去对抗。暴力破解只会让IP被封得更彻底违背爬虫学习的初衷。6.4 异常温度值怎么处理爬到的数据里偶尔会出现最高温45℃、最低温-20℃这种同一行数据跨度离谱的情况。原因通常是网页表格出现了合并单元格导致解析时数据串行。处理方式就是在清洗环节加合理范围过滤不在范围内的直接剔除。如果某几天的数据连续异常建议人工打开网页核对那几天的情况确认是解析逻辑问题还是原始数据本身的问题。6.5 pyecharts页面打不开pyecharts渲染出的HTML如果打开是空白先不要怀疑代码写错大概率是浏览器的安全策略拦截了本地资源加载。排查时先确认render()里的输出路径是正确的然后换一个浏览器试试。如果引用了外部CDN资源还要检查网络连接情况。本地临时文件我用Chrome或Edge都能正常打开遇到打不开的时候通常都是文件路径含中文或特殊字符导致的。7. 项目扩展与进阶思路7.1 从单城市扩展到多城市对比单一城市的数据视角比较单一。把这个项目扩展成多城市对比其实改动量不大准备一个城市URL映射字典循环遍历调用抓取函数把每个城市的数据分别存文件最后在可视化阶段合并读取到同一张图上。几个城市的温度曲线叠加在一起南北气候差异一目了然。city_url_map { 北京: https://example.com/beijing, 上海: https://example.com/shanghai, 广州: https://example.com/guangzhou }7.2 定时调度打造天气数据积累系统爬虫脚本的潜力在于持续运行。用APScheduler或者系统自带的任务计划程序设置一个每天定时执行的脚本让爬虫自动抓取当天的天气数据并更新CSV文件。跑上一两个月你手里就有了一份真实的个人气象记录库。更进一步爬完数据后自动生成图表并通过邮件发送给自己就演变成了一个小型数据库系统。7.3 存储升级到数据库CSV在数据量少的时候够用数据积累到一定体量后就该考虑升级到SQLite或MySQL。SQLite不需要安装额外的服务Python标准库自带支持是个人项目最顺滑的升级路径。切换过去之后查询和聚合分析都变得灵活很多比如可以直接用SQL按月份聚合温度均值不必先在Python里过滤一遍。完整代码和脚本结构可以按这个目录存放crawler.py负责抓取cleaner.py负责清洗visualizer.py负责画图主入口main.py串联全流程。模块化拆分的好处是每个文件职责单一后期改一个环节不需要动其他代码。最后分享一点个人实操体会。这个项目我前前后后改了五六个版本从最早用正则硬解析HTML到后面换成BeautifulSoup结构化解析从最开始只用matplotlib画静态图到后来加入pyecharts做交互图表。每一次调整的驱动力都是实际使用中的痛点——正则遇到嵌套标签就失灵静态图没法跟观众产生互动。如果你也在做类似的练手项目强烈建议沿着跑通流程、做稳容错、做好呈现这条路走一遍。天气数据只是一个载体你在爬虫和可视化上积累的方法论完全可以迁移到任何一个以数据为主角的实战项目里。