ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python天气数据爬取与可视化课设全解析:从requests到Tkinter

Python天气数据爬取与可视化课设全解析:从requests到Tkinter 简介基于Python构建的天气数据爬取与可视化项目附带详细注释与界面演示适合Python初学者、计算机相关专业学生及教师作为课程设计、毕业设计或数据分析练习的参考。资源包以zip格式提供共20个文件核心为3个Python脚本负责数据采集和数据分析配套2个CSV天气数据集、1张PNG界面演示图、1份MD说明文档以及用于IDE配置的XML文件和辅助文件整体仅393KB。项目完整覆盖天气数据采集、清洗、分析与可视化全流程代码注释详细结构清晰便于二次修改与功能扩展同时包含真实天气样例数据可直接查看运行效果也可作为人工智能、大数据方向的基础实践。目前已有107人学习下载该资源源于高分课程设计代码均通过运行测试适合需要快速搭建天气数据演示或完成课设、毕设任务的开发者参考。整体轻量易用配合详细注释可帮助读者快速熟悉从数据采集到展示的完整开发路径。1. 一个天气爬虫课设的完整拆解从 requests 到可视化很多人拿到 Python 天气数据爬取与可视化的课程设计第一反应是找个免费 API 调一下把 JSON 打印出来就算交差。但 weather-china-master 走的是另一条路用 requests 抓中国天气网的城市页面用 BeautifulSoup 解析 HTML 表格落成 weather14.csv 和 weather1.csv再用 pandas 做聚合统计用 matplotlib 把结果画成图表最后用 Tkinter 做一个能点按钮、能切城市、能看图的演示界面。整个链路不依赖付费 API也不需要浏览器驱动适合作为 Python 数据分析与可视化类课程的完整课设也适合后续要进入模型训练的同学先补上数据采集这一环。代码带详细注释单文件结构配好 Python 3.8 环境就能直接运行下载后先打开 README.md 看运行顺序和依赖版本再对照下面的拆解会更容易上手。接下来按实际调试顺序走一遍请求怎么写、CSV 怎么设计、分析脚本看什么指标、图表怎么选以及界面封装时的路径坑。2. weather.py 的请求构造与解析requests 加 BeautifulSoup 的选型边界2.1 为什么不用 Selenium 也不用 Scrapy抓天气页面这种低频、小规模的数据requests 加 BeautifulSoup 是成本最低的组合。判断标准很简单只要目标页面是服务端渲染、数据不依赖 XHR 动态加载就不需要 Selenium。中国天气网的 7 天预报在查看源代码时就能看到完整标签属于典型的静态 HTML。用 Selenium 虽然也能跑但要多维护一个浏览器驱动答辩环境里没有对应驱动时会在启动阶段直接报错。Scrapy 的并发调度和 Item Pipeline 更适合大规模采集针对 14 个城市、每天抓一次的频率Scrapy 会多出 items、pipelines、middlewares 等一堆文件代码量翻倍答辩时反而不容易讲清楚每一行代码的职责。我整理这版代码的时候只保留了 requests 和 BeautifulSoup 两个库。requests 负责把页面以字符串形式拿回来BeautifulSoup 负责把字符串解析成节点树边界很清晰网络层归 requests解析层归 BeautifulSoup。后续想换数据源时只需要替换 fetch 部分解析函数可以原封不动复用。这一点在项目演示时是很好的切入点如果评委问“网站改版了怎么办”可以直接指到 parse 函数说明。2.2 请求头、超时与编码设置爬虫的第一道门槛是请求头。裸的 Python requests 会把python-requests/x.x.x作为 User-Agent 发出去一部分反爬策略看到这个标识会直接拒绝。最稳的做法是带一个浏览器 User-Agent再加上 Referer 模拟从首页跳转。import time import requests from bs4 import BeautifulSoup def fetch_weather(city_code: str, timeout: int 10) - str: 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, Referer: http://www.weather.com.cn/ # 模拟从首页进入 } url fhttp://www.weather.com.cn/weather/{city_code}.shtml resp requests.get(url, headersheaders, timeouttimeout) resp.encoding utf-8 # 强制指定编码避免中文乱码 return resp.text逻辑说明city_code是城市页面的编号北京是 101010100上海是 101020100这个编号在天气网的站点导航里能直接查到。timeout同时约束连接和读取时间校园网偶发丢包不设超时的话一个卡死的请求会让 14 个城市的循环停在原地。resp.encoding utf-8是关键一行天气网部分节点响应头里没有声明 charsetrequests 会按 ISO-8859-1 猜测编码中文直接变乱码所以拿到响应后立刻指定编码。参数说明Referer模拟来源页面但有些代理环境下这个字段反而会成为拦截特征。如果校内网络抓不到数据先去调 Referer 再试同时确认timeout是不是设得过小。2.3 用 select 而不是逐层 find_all解析时我习惯用soup.select(ul.t.clearfix li)这是 7 天预报块的特征类名select 把所有 li 节点取出来再逐个提取日期、天气、温度和风力。def parse_forecast(html: str) - list[dict]: soup BeautifulSoup(html, html.parser) items soup.select(ul.t.clearfix li) # 7 天预报的每个 li 卡片 rows [] for li in items: date li.select_one(h1).get_text(stripTrue) weather li.select_one(p.wea).get_text(stripTrue) # 高低温在源码里各占一行这里合并成 23℃/12℃ temp li.select_one(p.tem).get_text(stripTrue).replace(\n, ) wind li.select_one(p.win span).get_text(stripTrue) # 只取风向文字 rows.append({ date: date, weather: weather, temperature: temp, wind: wind, }) return rows逻辑说明select_one只取第一个匹配节点get_text(stripTrue)删除标签首尾空白。温度节点里的高低温各占一行直接取文本会得到23℃\n12℃用replace(\n, )拼成23℃/12℃写进 CSV 单列可读性最好。风力的p.win里还带一个箭头图标直接取整段会得到箭头字符在不同操作系统上显示不一致所以只取span里的文字。这种写法的容错性比find_all(ul)再往下钻更好。页面如果在目标 ul 前面插入了广告 lifind_all 很容易把广告误算进去select 用类名定位则不受影响。运行中如果发现items为空优先检查类名是不是变了而不是怀疑网络。2.4 失败重试与失败城市收集天气网不是每一秒都稳定偶尔会返回空页面或缺标签。课程设计里直接中断整个流程太浪费加一层轻量重试就行。def fetch_with_retry(city_code: str, retries: int 3, delay: int 2) - list[dict]: for attempt in range(1, retries 1): try: html fetch_weather(city_code) rows parse_forecast(html) if rows: # 解析结果为空也视为失败 return rows except requests.RequestException as exc: print(f[{attempt}/{retries}] {city_code} 失败: {exc}) time.sleep(delay) # 只有失败才等待不拖慢成功路径 raise RuntimeError(f{city_code} 连续 {retries} 次抓取失败)逻辑说明retries是最大尝试次数delay是失败后的等待秒数。这里把“解析结果为空”也当作失败处理因为页面结构变化时 select 不会报错只会返回空列表不处理的话空数据会被写进 weather14.csv。time.sleep放在异常分支里只有请求失败才等待成功路径不会被拖慢。我一般还会在外层维护一个failed_cities列表except RuntimeError时把城市编号 append 进去。等 14 个城市全部跑完再对失败列表统一补爬这样即使有两个城市临时挂了其余城市的 CSV 也完整可用不用从头跑一遍。提示failed_cities里只存 city_code 就够补爬时直接复用fetch_with_retry不需要重新解析整个页面的数据。3. CSV 落盘与多城市数据合并编码和字段设计决定分析体验3.1 存储方案对比项目里最终产出了 weather14.csv 和 weather1.csv。weather14 是 14 个城市的横向对比weather1 是单城市的时间序列。CSV 对这类规模的数据是最合适的格式pandas 一行代码能读Excel 双击能看git 能追踪变化。存成 xlsx 需要额外维护 openpyxl存 SQLite 又要求答辩现场装 DB Browser这些成本在课设场景里都没必要。存储方案读取成本答辩现场打开适用规模CSVpandas 直接 read_csvExcel 双击可看10 万行以内xlsx需要 openpyxlExcel 可看但依赖库5 万行左右SQLite标准库 sqlite3需要装 DB Browser100 万行以上对这个项目一天爬一次、总共 14 个城市的数据量CSV 是明确的选择。真正决定分析体验的不是格式而是编码和字段设计。3.2 utf-8-sig 与 newline 参数很多课设代码用的是标准库 csv写文件时最容易踩两个坑中文乱码和行之间多出空行。import csv def write_csv(path: str, rows: list[dict]) - None: # utf-8-sig 写入 BOMExcel 打开中文不乱码newline 避免 Windows 空行 with open(path, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter( f, fieldnames[city, date, weather, temperature, wind] ) writer.writeheader() writer.writerows(rows)逻辑说明utf-8-sig会在文件开头写入 BOM 头Excel 用本地 ANSI 编码打开无 BOM 的 UTF-8 文件时中文列名会整体乱码加了 BOM 后 Excel 能自动识别。newline是 csv 模块在 Windows 平台上的标准要求不加的话每两行之间会多出一个空行。fieldnames的顺序就是表头的顺序字典里多余键不影响写入但缺键会抛ValueError。需要注意如果直接用 pandas 的to_csv写 DataFrame中文乱码问题同样存在方法是传encodingutf-8-sig原理和这里完全相同。3.3 多城市数据的收集与合并多城市爬取时最忌讳每爬一个城市就打开文件追加一行。中间崩溃会在 CSV 末尾留下半条记录重新补爬时还要先做去重。常见做法是先在内存里把all_rows合并完最后一次写盘。cities { 北京: 101010100, 上海: 101020100, 广州: 101280101, 成都: 101270101, } all_rows [] failed [] for name, code in cities.items(): try: rows fetch_with_retry(code) for r in rows: r[city] name all_rows.extend(rows) print(f{name}: {len(rows)} 条累计 {len(all_rows)} 条) except RuntimeError: failed.append(name) write_csv(weather14.csv, all_rows) if failed: print(失败城市:, failed) else: print(全部写入完成)逻辑说明先爬完所有城市再统一调用write_csv能保证 weather14.csv 要么完整生成、要么不生成不会出现半截文件。r[city] name是在内存里给每条记录补上城市字段这个字段要在写入前确定fieldnames里才包含 city 这一列。failed列表记录完全失败的城市方便补爬而不是在 13 个成功城市的数据里重新跑一遍。3.4 温度字段规范化早晚要拆成数字p.tem解析出来的23℃/12℃对人类可读对 pandas 聚合就是一场灾难。分析前先拆出temp_high和temp_low两列更稳妥。import re def split_temp(value: str) - tuple[int, int]: nums re.findall(r-?\d, value) # -? 兼容零下温度 if len(nums) 2: return int(nums[1]), int(nums[0]) # 返回 (低温, 高温) if len(nums) 1: return int(nums[0]), int(nums[0]) return 0, 0逻辑说明页面源码里温度块的显示顺序是“高温在前、低温在后”但这个顺序在页面改版时并不稳定。这里用正则把所有数字抽出来固定返回(低温, 高温)。-?\d的-?兼容零下温度北方城市冬天气温是负值不加这个符号-5℃ 会被解析成 5℃。两个数字都取不到时返回(0, 0)后续分析里按缺失值过滤掉。如果split_temp拆出来的数字和实际天气不符先看原始 temperature 字符串确认是不是箭头图标被带进了p.tem的文本里。4. data14_analysis.py 的数据清洗与聚合pandas 的类型转换和分组统计4.1 read_csv 阶段就做类型转换有些同学读 CSV 之后用 for 循环逐行清洗数据量小的时候看不出问题但不是 pandas 的用法。正确做法是从read_csv开始把类型定好后续聚合才不会踩 dtype 的坑。import pandas as pd df pd.read_csv(weather14.csv) # 拆出高低温数字原始 temperature 列保留用于核对 df[temp_high] df[temperature].apply(lambda x: split_temp(x)[1]) df[temp_low] df[temperature].apply(lambda x: split_temp(x)[0]) df[date] pd.to_datetime(df[date], format%d日)逻辑说明temperature列保留原始字符串temp_high和temp_low由split_temp拆出原始数据不丢分析字段和文本字段可以对照检查。date列用%d日解析因为天气网预报显示的是“今天”“明天”这类相对日期。如果解析遇到无法识别的值会变成NaT后续用dropna过滤掉即可。参数说明format里的%d是两位数日期页面如果给的是“04月20日”要改成%m月%d日。pandas 2.x 之后对时间格式更严格格式不对会直接抛异常这其实是好事至少比静默解析错误更容易发现。4.2 14 个城市的横向对比课设里最有展示价值的分析是把 14 个城市未来三天的最高温和最低温拉成同一张表再按最高温均值排序。df[day_index] df.groupby(city).cumcount() # 每个城市按日期编号 0,1,2... next3 df[df[day_index] 3] # 取每个城市的前三天 summary ( next3.groupby(city) .agg( avg_high(temp_high, mean), avg_low(temp_low, mean), max_high(temp_high, max), ) .sort_values(avg_high, ascendingFalse) .reset_index() )逻辑说明groupby(city).cumcount()给每个城市按日期顺序编号相当于给每天打上“第一天、第二天、第三天”的标签。agg里的命名聚合一次性算平均最高温、平均最低温和最高温极值生成一张汇总表。sort_values(avg_high, ascendingFalse)把最热的城市排在最前面答辩演示时直接看表就能讲出城市温差。参数说明mean求平均想换中位数就写median想换极差就分别取max和min。某个城市某天数据缺失时mean默认跳过 NaN不需要提前填零。4.3 单城市时间序列与 data1_analysis.py 的分工同一个项目里的 data1_analysis.py 和 data14_analysis.py 分工不同。weather14 做城市间横向对比weather1 做单城市纵向趋势。单城市分析需要把多天数据按时间排序再算温度变化率。city_df ( df[df[city] 北京] .sort_values(date) .reset_index(dropTrue) ) city_df[temp_diff] city_df[temp_high].diff() # 相邻两天最高温差值逻辑说明先按城市筛选再按日期排序保证时间序列的顺序正确。temp_diff用diff()计算相邻两天最高温的差值正数表示升温、负数表示降温。这个字段一般作为图上的文字标注不需要写回 CSV。注意sort_values(date)只有在 date 被to_datetime正确解析后才可靠。如果 date 还是字符串按字母序“10日”会排在“9日”前面画出来的温度曲线会前后颠倒。遇到这种情况先检查 4.1 里的格式参数不要急着改绘图代码。5. matplotlib 天气可视化折线图、柱状图与保存参数5.1 中文字体与负号配置matplotlib 默认字体不含中文字符直接用中文当标签会出现方框。最常见的全局配置是import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] # 中文字体 plt.rcParams[axes.unicode_minus] False # 负号正常显示逻辑说明font.sans-serif把默认字体族换成黑体SimHei 在 Windows 上自带。axes.unicode_minus必须设为 False否则坐标轴上的负号会显示成方块。如果你在 macOS 上运行SimHei 不存在改成[Arial Unicode MS]或[PingFang SC]。5.2 单城市温度折线图单城市趋势适合折线图横轴日期纵轴温度高低温各一条线。fig, ax plt.subplots(figsize(10, 5)) ax.plot(city_df[date], city_df[temp_high], markero, label最高温) ax.plot(city_df[date], city_df[temp_low], markers, label最低温) ax.set_title(北京七日温度趋势) ax.set_ylabel(温度 (℃)) ax.legend() ax.grid(True, linestyle--, alpha0.5) plt.xticks(rotation45) # 横坐标太密集时旋转标签避免重叠 plt.tight_layout() plt.savefig(beijing_temp.png, dpi150, bbox_inchestight)逻辑说明figsize控制画布大小日期较多时横向拉长到 10比默认 6.4 的显示效果好。markero是圆形点markers是方形点用来区分高低温两条线。plt.xticks(rotation45)解决横坐标日期太密集导致标签互相覆盖的问题这是 python 画图横坐标太密集时最常见的修复方式。grid画虚线网格alpha 控制透明度。tight_layout保证标题和坐标轴标签不被裁切。5.3 多城市横向柱状图横向柱状图适合展示 14 个城市的温度排序。城市名长度不等横向排列能避免斜字重叠。fig, ax plt.subplots(figsize(8, 7)) ax.barh(summary[city], summary[avg_high], color#4C72B0) ax.set_xlabel(未来三天平均最高温 (℃)) ax.set_title(14 城市温度对比) for idx, value in enumerate(summary[avg_high]): ax.text(value 0.2, idx, f{value:.1f}, vacenter) # 柱子右侧标数值 plt.tight_layout() plt.savefig(city_compare.png, dpi150, bbox_inchestight)逻辑说明barh是横向柱状图城市名在 y 轴排列最长也不会和相邻标签重叠。ax.text在柱子右侧写数值value 0.2防止标签贴在柱子上vacenter让文字在垂直方向居中。颜色用十六进制值直接替换成项目主题色即可。如果数据可视化里还有省份温度分布的需求那就不是 matplotlib 的长项要换 pyecharts 的 Map 类型。但课设场景里柱状图已经能把城市对比讲清楚不必强上地图。5.4 保存图片的 dpi 和 bbox_inches课设提交要把图片插进报告dpi直接决定清晰度。150 适合屏幕展示300 适合打印。bbox_inchestight会裁掉图片四周空白避免插进 Word 后留白过大。如果图片导入 Word 后模糊先检查是不是dpi太低而不是把figsize无限调大文件过大在答辩电脑上打开反而更卡。6. Tkinter 界面演示与打包爬虫可视化界面的落地6.1 用子线程跑爬虫避免界面卡死Tkinter 的按钮回调如果直接做网络请求窗口会假死直到请求完成演示时点完按钮界面完全无响应。常见做法是启动一个子线程去爬通过队列把结果传回主线程。import threading import queue task_queue queue.Queue() def start_crawl(): def worker(): rows fetch_with_retry(101010100) # 北京 task_queue.put(rows) threading.Thread(targetworker, daemonTrue).start() def poll_queue(): try: rows task_queue.get_nowait() update_chart(rows) except queue.Empty: pass root.after(200, poll_queue) # 每 200ms 检查一次队列逻辑说明start_crawl里用threading.Thread把网络请求放到后台daemonTrue保证主窗口关闭时线程能跟着结束。poll_queue通过root.after循环检查队列拿到数据再刷新图表。这样界面不会在爬取期间卡住评委点按钮时能看到窗口立即响应。6.2 打包成 exe 的路径坑用 PyInstaller 打包时最常遇到的是图表字体和 CSV 模板找不到。常见处理是按资源文件所在目录动态拼路径import sys from pathlib import Path BASE_DIR Path(sys._MEIPASS) if hasattr(sys, _MEIPASS) else Path(__file__).parent csv_path BASE_DIR / weather14.csv逻辑说明PyInstaller 会把资源放在临时解包目录路径存在sys._MEIPASS里直接从源码运行时用__file__所在目录。用这个逻辑取路径打包前后行为一致。字体方面如果目标机器没有 SimHei可以把字体文件用--add-data一起打进去运行时再用FontProperties指定字体文件路径。6.3 动态刷新图表Tkinter 里显示 matplotlib 图表用FigureCanvasTkAgg把 Canvas 放进窗口即可。每次爬取结束后要清空旧图、重新绘制再调用canvas.draw()。注意canvas.get_tk_widget().pack()之后不要重复创建 Figure否则窗口会叠加多个绘图区域。窗口标题用当前城市名动态更新比固定写“天气查询”更有演示效果。本文还有配套的精品资源点击获取
返回列表