资讯动态

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

发布时间:2026/10/14 6:27:32 来源:尧图企业网站定制
年初我搭了一个专门整理全年天气数据的小项目就是标题里这个“python爬虫——爬取全年天气数据并做可视化分析”。项目本身不复杂但把爬虫、数据清洗和可视化这条链路完整走了一遍练完以后最大的感受是爬虫只占一半工作量后面数据规整反而是大头。这篇直接把我的实现方式、选型理由、踩过的坑全部记录下来完整还原一套能从零跑通的全流程方案。适合三类人刚看完requests和BeautifulSoup教程想找个综合练习的需要做天气数据相关课程设计或个人项目的以及做运营、旅游、活动策划时需要拿历史天气数据做参考的人。1. 拆解需求这项目本质上是一个数据全链路小工程先说清楚“全年天气数据”到底长什么样。我们说的爬取不是只拿一个“今天晴”这种结果而是要拿到一整年每一天的结构化记录包括日期、最高气温、最低气温、平均气温、天气现象晴/阴/雨/雪、降水量、相对湿度、风向、风力甚至空气质量等字段。有了这些每天一条的数据才能往下做可视化分析。1.1 确定数据模型先想清楚要什么字段我最终定的数据表结构大概是这样字段名类型示例用途date日期2024-07-15时间维度high_temp数值35.2当天的最高气温low_temp数值24.1当天最低气温avg_temp数值29.6日平均气温weather_desc文本晴转多云天气现象描述precipitation数值0.0当日降雨量毫米humidity数值63相对湿度百分比wind_dir文本东南风主导风向描述wind_level文本3级风力等级描述这个字段集合不是拍脑袋定的它直接决定了后面能画什么图。如果收集阶段漏了湿度后面想分析“梅雨季的闷热程度”就只能干瞪眼。所以在写第一行代码之前建议把所有想做的分析维度列出来再反推需要哪些字段。用我自己的话讲先想好要画什么图再去定要存什么数据。1.2 技术栈选型为什么用这套组合整套项目用到的核心库不多但每一个都是“同类里最适合干这事的”requests发送HTTP请求的标配简单直接适合面向静态页面或者普通JSON接口。BeautifulSoup解析HTML的小能手尤其是面对那种“表格型”天气页面比正则表达式的可读性好太多。pandas数据清洗和聚合的核心处理缺失值、转换日期、按月份groupby清一色用它可以省掉大量手写循环。pyecharts可视化这块我推荐它因为生成的是交互式HTML图表能够缩放、悬停显示数值比matplotlib出的静态PNG更适合做演示和复盘。sqlite3Python内置数据落盘我选它而不是CSV原因后面会详细讲。这个选型组合没有冷门库也不吃显卡一台普通笔记本就能跑对入门者非常友好。但也有一个需要提醒的点真实的天气数据源有些是把数据直接放在了HTML表格里有些则是通过接口动态返回JSON。两者的处理方式很不一样。HTML表格可以直接用BeautifulSoup找table标签JSON接口则更适合直接转字典取键值。所以在写爬虫之前一定要先用浏览器打开目标页面按F12看下网络请求到底返回了什么。这个“先观察再动手”的习惯能帮你避免后面推倒重来。2. 环境准备与数据源分析这个环节看起来水但省掉它的人基本都会在半路卡住。我见过不少朋友一上来就写爬虫代码结果页面分析错了目标接口和解析规则全不对折腾半天还以为是自己代码问题。2.1 环境搭建与依赖安装我建议新建一个独立项目目录方便管理后续的爬虫脚本、清洗脚本和可视化脚本。在Python虚拟环境中安装依赖即可我使用的版本是Python 3.11mkdir weather_project cd weather_project python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate pip install requests beautifulsoup4 pandas pyecharts lxml这里有一个小细节解析HTML时建议安装lxml它比Python内置的html.parser解析速度快得多在处理整年365天数据时感觉不明显但如果你把范围扩大到多城市多年份没有lxml你会等到怀疑人生。pyecharts装完之后默认会在当前目录生成HTML文件不需要额外装任何浏览器组件比较省心。2.2 数据源两种形态的识别方式天气数据页面的形式我总结下来主要就两类服务端渲染的静态HTML表格打开页面能直接看到表格数据右键查看源代码也能找到。这种页面只需要requests抓回HTML再用BeautifulSoup解析。前端AJAX动态加载的JSON接口页面上能看到数据但HTML源代码里没有按F12切换到Network面板刷新页面找到返回数据的XHR请求里面的JSON才是真正的数据源。怎么判断是哪种最简单的办法是在浏览器页面里按CtrlS保存完整HTML再用文本编辑器打开搜一下“日期”或者“最高气温”。如果能在保存的HTML里直接找到数据就是第一种如果找不到就是第二种。面对第二种情况要直接抓取那个XHR请求的URL和请求参数解析JSON而不是去解析HTML。我那次实现就是因为目标站点刚好提供了接口返回处理起来反而比解析HTML更稳定。2.3 请求头配置与超时设置无论面对哪种数据源请求头都要尽量伪装得像正常浏览器。我用的基础请求头模板是import requests from fake_useragent import UserAgent # 平时写死一个也行但用库随机生成能定期换一换见下文注意点 # ua UserAgent() headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36, Referer: https://target-site.example.com/weather, Accept: application/json, text/html, */*, Accept-Language: zh-CN,zh;q0.9, } resp requests.get(https://target-site.example.com/api/weather, headersheaders, timeout10) resp.raise_for_status()关于headers有一点个人经验Referer指的是你从哪个页面跳过来的。很多接口会校验这个字段如果你发现不带Referer时返回403或者鉴权失败补上这个字段往往就好了。timeout一定要设不设的话某个请求挂起时你的循环会一直卡在那里越是批量爬取越要设置。fake_useragent这个库可以随机生成UA头但有些接口会对UA做白名单匹配随机UA反而可能触发拦截。实测下来我共用了一个Chrome浏览器的固定UA配合稳定的请求间隔并没有遇到什么问题。所以这个库并不是必须的写死一个看起来合理的UA就够用。3. 爬虫环节从请求到落库的完整实现这一节是整个项目的核心。我会用一个简化但完整可跑的示例来演示从拿数据到存库最后讲讲反爬应对策略。3.1 抓取目标接口并转换为Python字典假设目标数据源是JSON接口返回的数据结构类似{ data: { city: A城, date: 2024-07-15, high_temp: 35.2, low_temp: 24.1, avg_temp: 29.6, weather_desc: 晴转多云, precipitation: 0.0, humidity: 63, wind_dir: 东南风, wind_level: 3级 } }对应爬取脚本import requests import json import time url https://target-site.example.com/api/weather params { city: A城, date: 2024-07-15 } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0, Referer: https://target-site.example.com/weather } resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() data resp.json()[data] print(data)requests.get的params参数是一个被很多人忽略的好东西它会把字典自动拼成?cityA城date2024-07-15的查询字符串还会帮你处理URL编码比手动拼字符串优雅也不容易出错。这里需要强调一个细节如果接口返回的实际是JSONP或者text格式resp.json()会抛异常这时先打印前200个字符看看返回内容再决定下一步。3.2 用循环获取一整年数据思路很简单用Pandas生成2024-01-01到2024-12-31的所有日期然后逐个请求。这里有两个必须要注意的点请求间隔和失败重试。import pandas as pd import requests import time from datetime import datetime def fetch_weather(date_str): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0, Referer: https://target-site.example.com/weather } url https://target-site.example.com/api/weather params {city: A城, date: date_str} resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json()[data] date_list pd.date_range(2024-01-01, 2024-12-31).strftime(%Y-%m-%d).tolist() results [] for idx, date_str in enumerate(date_list): try: item fetch_weather(date_str) item[date] date_str results.append(item) print(f已获取 {date_str}) except Exception as e: print(f失败 {date_str}: {e}) # 关键点每次请求之间间隔0.5秒 time.sleep(0.5)这个代码里体现的其实是最朴素的“君子爬虫”做法每天请求一次加上0.5秒延时对目标站点的影响很小。我个人建议即便你有能力并发请求也在初版脚本里老老实实做单线程串行采集。全年365天串行加上延时也就不到10分钟完全没有必要为了追求速度去增加被封的风险。3.3 数据落盘CSV与SQLite怎么选采集到的results是一个列表每个元素是一个字典。落盘方式我试过两种方式一存CSVdf pd.DataFrame(results) df.to_csv(weather_A_city_2024.csv, indexFalse, encodingutf-8-sig)CSV的好处是方便用Excel打开人工查看适合数据量小、后续不打算做复杂关联查询的场景。注意编码一定要用utf-8-sig否则你用Excel打开时中文会变成乱码。方式二存SQLiteimport sqlite3 conn sqlite3.connect(weather.db) df pd.DataFrame(results) df.to_sql(daily_weather, conn, if_existsreplace, indexFalse) conn.close()我实际项目里用的是SQLite。原因很简单第一采集过程中可能会中断如果只存CSV中断一次就得重新爬而SQLite可以逐条INSERT爬一条存一条天然支持断点续传。第二后面做统计时SQL语句直接SELECT date, high_temp FROM daily_weather WHERE month7比反复读CSV再过滤要舒服。当然最简单的稳妥做法是采集过程中每条写入SQLite全部采集完了再从SQLite导出CSV做备份。这样既有断点续传能力也能拿到方便查看的文件。要注意pandas的df.to_sql()写出的表日期字段是字符串后面清洗时记得再转一次。3.4 反爬应对策略从频率控制到重试机制天气数据站点虽然一般不会对低频爬虫采取极端措施但基本的反爬意识还是要有的。我总结了一套从轻到重的策略频率控制最基础每次请求间隔不低于0.3秒这个间隔既不影响效率又能大幅降低被识别为机器行为的概率。失败重试有讲究遇到超时、5xx等临时性错误不要立刻重新请求而是等几秒钟再试。常见的写法是循环三次每次等待时间递增比如等待2秒、5秒、10秒。请求头务必完整UA、Referer、Accept-Language这些字段尽量都带上。有些接口会校验这些字段的组合是否像一个真实浏览器。遇到验证码直接停如果你发现某次请求返回的是验证码页面基本说明IP已经被标记了。这个时候不要硬刚把当天数据标记为缺失休息几小时再继续。硬爬只会让IP被标记更久。很值得提醒的是爬虫不是越强悍越好目标只是“把数据拿回来”不是“证明自己技术多厉害”。这套策略的前三条我基本上每个爬虫项目都会用第四条则是我在大规模采集时的纪律底线。4. 数据清洗与聚合统计让原始数据变成可用数据很多初学者爬完数据就急着画图结果画出来的图要么日期轴乱序要么缺一个月的数据要么出现湿度99%、温度-15℃这种异常值。数据清洗虽然枯燥但它决定了可视化的质量。这一节我把清洗过程分成三步讲。4.1 日期字段处理与缺失值填充从SQLite读回DataFrame后第一步永远是看一下基本信息import sqlite3 import pandas as pd conn sqlite3.connect(weather.db) df pd.read_sql(SELECT * FROM daily_weather, conn) conn.close() print(df.info()) print(df.describe())df.info()能看出每列有没有空值和类型对不对。我实测的结果是date列是object类型字符串需要转成datetime类型high_temp列是int或者float取决于当时怎么存的需要统一转成floatweather_desc列有少数空值通常是接口当天返回异常导致的。日期转类型的代码很简单df[date] pd.to_datetime(df[date]) df[high_temp] pd.to_numeric(df[high_temp], errorscoerce) df[low_temp] pd.to_numeric(df[low_temp], errorscoerce)errorscoerce是个好设计它会将无法转换的值变成NaN而不是直接报错终止。如果某些天的天气现象缺失评估一下缺失比例如果只有几天可以根据前后天的天气做简单填充如果缺失了整月最好重新补采因为插值堆出来的数据画图没意义。4.2 异常值筛查不要放过任何一个离谱数字天气数据里常见的异常有四类气温倒挂最高气温小于最低气温绝大多数情况是数据解析错误。数值越界相对湿度大于100%或者降水为负数基本都是脏数据。突变异常前一天高温35℃后一天突然变成零下10℃这在非寒潮场景下就很可疑。类型污染比如温度字段里混入了中文备注导致转成NaN。我习惯写一个简单的过滤器# 气温倒挂检查 invalid_order df[df[high_temp] df[low_temp]] print(f气温倒挂记录数: {len(invalid_order)}) # 湿度越界检查 invalid_humidity df[(df[humidity] 100) | (df[humidity] 0)] print(f湿度异常记录数: {len(invalid_humidity)})对于气温倒挂如果只是少量记录最稳妥的处理方式是直接删除那一天的数据或者参考前后两天的均值修正。不建议闭着眼睛把high_temp和low_temp对调因为对调后可能依然不符合事实比如数据本来就是解析错位。4.3 按月聚合统计从每天到每月清洗干净后接下来就是可视化前的聚合。我们按月份算均值顺便统计每个月的降雨。df[month] df[date].dt.month df[year] df[date].dt.year monthly_stats df.groupby(month).agg( avg_high(high_temp, mean), avg_low(low_temp, mean), avg_temp_mean(avg_temp, mean), total_precip(precipitation, sum), avg_humidity(humidity, mean) ).round(1) monthly_stats.to_csv(monthly_weather_stats.csv, encodingutf-8-sig)这一步输出的monthly_weather_stats.csv后面所有月维度图表都会用到。聚合时千万记得在groupby后用.round(1)保留一位小数否则生成图表时坐标轴上会出现一长串烦人的小数位。5. 可视化实现四张图看透一年天气可视化选pyecharts核心原因前文提过交互式图表在复盘时特别好用。以下四张是我认为最能说明问题的图。5.1 全年气温趋势折线图第一张图看整体温度变化。用日度数据画横轴是日期纵轴是温度三条线分别是最高气温、平均气温、最低气温。from pyecharts.charts import Line from pyecharts.options import TitleOpts, TooltipOpts, LegendOpts, DataZoomOpts line ( Line() .add_xaxis(df[date].dt.strftime(%m-%d).tolist()) .add_yaxis(最高气温, df[high_temp].round(1).tolist(), is_smoothTrue) .add_yaxis(平均气温, df[avg_temp].round(1).tolist(), is_smoothTrue) .add_yaxis(最低气温, df[low_temp].round(1).tolist(), is_smoothTrue) .set_global_opts( title_optsTitleOpts(titleA城2024年气温变化趋势), legend_optsLegendOpts(pos_top5%), tooltip_optsTooltipOpts(triggeraxis), datazoom_opts[DataZoomOpts(range_start0, range_end100)], ) ) line.render(temperature_trend.html)如果不加DataZoomOpts365个点全部挤在一张图里根本看不清春夏秋冬的细节。加了滚动条后可以任意缩放查看某一段区间交互体验完全不一样。这是pyecharts相比matplotlib最有优势的地方。5.2 月度降雨量与湿度组合图第二张图聚焦水分维度。月份作为横轴降雨量用柱状图湿度用折线图组合在同一个坐标系里。from pyecharts.charts import Bar, Line as Line2 bar ( Bar() .add_xaxis([f{m}月 for m in monthly_stats.index.tolist()]) .add_yaxis(降雨量(mm), monthly_stats[total_precip].tolist()) ) line2 ( Line2() .add_xaxis([f{m}月 for m in monthly_stats.index.tolist()]) .add_yaxis(湿度(%), monthly_stats[avg_humidity].tolist()) ) bar.overlap(line2) bar.set_global_opts( title_optsTitleOpts(titleA城2024年月度降雨与湿度对比), yaxis_optsNone ) bar.render(precip_humidity.html)这张图能直观看出哪几个月是雨季雨季的湿度是否和降雨同步上升。我自己跑下来最有感触的是往往7、8月降雨量最高但湿度曲线在6月就已经爬到了顶峰说明“体感最闷”和“下最多雨”并不完全同步。这种发现不做数据可视化永远看不到。5.3 风向频率极坐标玫瑰图第三张图分析风向。它把一年时间里不同风向出现的次数画在极坐标系里。做成玫瑰图的好处是面积大小一目了然能直接看出这座城市的主导风向是什么。from pyecharts.charts import Pie from pyecharts import options as opts wind_count df[wind_dir].value_counts().reset_index() wind_count.columns [wind_dir, count] pie ( Pie() .add( , [list(z) for z in zip(wind_count[wind_dir], wind_count[count])], radius[20%, 60%], center[50%, 50%], rosetyperadius, label_optsopts.LabelOpts(formatter{b}: {c}天), ) .set_global_opts(title_optsopts.TitleOpts(titleA城2024年风向分布)) ) pie.render(wind_rose.html)这里有个经验之谈rosetyperadius是玫瑰图模式效果是扇形半径不一样能凸显占比差异如果不设置就是普通饼图。风向字段在清洗时必须做归一化处理比如把“东南风”“东风”都单独统计但要把“东南风3级”这种带风力后缀的文本先去掉。我们数据源正好是纯风向所以不需要额外处理。5.4 高温日分布热力日历图第四张图是我最喜欢的它用日历热力图的形式展示一年里温度超过30℃的天数分布。严格来说pyecharts的Calendar组件可以实现from pyecharts.charts import Calendar from pyecharts import options as opts heat_data [ [df[date].dt.strftime(%Y-%m-%d).tolist()[i], int(df[high_temp].tolist()[i])] for i in range(len(df)) ] cal ( Calendar() .add(, heat_data, calendar_optsopts.CalendarOpts(range_2024, cell_sizeauto)) .set_global_opts( title_optsopts.TitleOpts(titleA城2024年每日最高气温日历), visualmap_optsopts.VisualMapOpts( max_40, min_0, orienthorizontal, is_customTrue, ), ) ) cal.render(high_temp_calendar.html)日历图查看分布特别直观一眼扫过去就能认出哪几周热浪连片哪几天骤冷比折线图更适合做“全年温度记忆”的回顾。唯一要提醒的是Calendar组件对日期格式要求严格必须是“YYYY-MM-DD”的字符串所以传入前先把date列格式化好。6. 踩坑记录与排查速查表这部分是全文最有“烟雾弹”价值的内容。我在实现过程中确实遇到过下面这些问题有些花了不少时间才找到原因。6.1 中文乱码接口返回正常但Python打印乱码这个问题绝大多数出在编码处理上。解决方案是拿到resp之后不要直接.json()先看响应头里的charset# 如果response没有正确识别编码 resp.encoding resp.apparent_encodingapparent_encoding会根据响应内容自动猜测编码对中文站点准确率很高。还有存CSV时的utf-8-sig编码问题我在前文说过了重提一次因为真的太常见了。6.2 请求返回403但页面能在浏览器打开403十有八九是请求头不够“像浏览器”。排查思路从轻到重先补全User-Agent再补Referer再补Accept和Accept-Language最后如果还不行检查是不是需要带Cookie。很多接口只要你先在浏览器里访问一次目标页面把Cookie复制过来塞进requests里就行。6.3 解析HTML表格时行数对不上用BeautifulSoup解析静态表格时有人会遇到“某一行解析丢了”的情况原因通常是单元格里嵌套了子标签。解决办法是拿每一行中所有的td或th并且对每个单元格使用get_text(stripTrue)不要直接用.text。如果个别行有合并单元格colspan解析逻辑要额外处理否则字段对不齐。这类边角问题很磨人建议直接拿一行的HTML原始内容做对比排错效率高。6.4 图表中文标签变成方框pyecharts生成的HTML图表理论上中文会正常显示。但如果你的系统缺少中文字体又用了非HTML渲染方式就会变成方框。排查顺序先确认系统有没有需要的字体再检查HTML文件本身是不是UTF-8编码保存。实在不行就在pyecharts的全局配置里指定一个确定存在的中文字体名称。6.5 常见问题速查表现象可能原因排查优先级resp.json()抛异常返回的不是JSON可能是HTML错误页先print前200字符所有请求都403UA/Referer被拦截补全请求头带Cookie单个日期失败后全集停循环缺少try/except加异常捕获标记失败日期SQLite表里日期是字符串pandas默认推断类型读出后pd.to_datetime重新转换折线图x轴密集看不清缺少datazoom加DataZoomOpts开启缩放Excel打开CSV中文乱码编码用了utf-8改用utf-8-sig这张表是我根据自己排查过程整理的不一定覆盖所有数据源情况但思路可以复用先看原始返回内容再判断解析环节是否出错最后才怀疑可视化库的问题。最后分享一个实践经验项目收尾后我最大的体会是天气数据爬虫的技术难度其实不高真正花时间的地方在数据规整和图表解读。数据规整做不好图表再华丽也是误导人的图表解读做不好数据清洗得再干净也体现不出价值。所以建议你在动手之前先花几分钟把“我到底想通过数据分析出什么结论”写下来不要等到图全画完了再临时想解读思路。另外如果你后续想扩展这个项目我推荐两个方向一个是在现有数据库上增加历史多年数据做“今年和往年同期对比”另一个是引入多城市对比研究不同气候区的差异。基础架构我已经替你验证过——requests采集、SQLite存储、pandas清洗、pyecharts可视化这一套足够支撑你往任何方向扩展。希望你也能做出几张让自己眼前一亮的天气图表。

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

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

免费获取报价 →
↑