Python爬虫+可视化:手把手实现天气查询桌面应用
简介Python学习者可参考的一份可视化爬虫实战案例围绕天气查询小程序完整演示了从零搭建图形用户界面爬虫工具的过程。文档先介绍tkinter创建主窗口、标签、输入框与按钮的流程再深入get_weather_data函数解析用urllib.request请求天气接口、对城市名进行URL编码、借助gzip解压响应数据、依靠json将数据转为字典的关键步骤并针对无效城市名给出消息框提示方案。随后重点讲解show_data函数如何将未来五日预报逐条取出以LabelFrame框架和Label控件展示日期、风向、风级、最高温、最低温与天气类型同时展示grid网格布局和place绝对定位的不同用法补充窗口大小控制、退出按钮等人机交互细节。压缩包内仅一个PDF文档体积71KB内容覆盖界面搭建、数据获取、天气展示三大模块包含核心代码片段、执行效果截图与常见错误排查说明。读者既可参考完整实现思路也能将请求解析与界面封装方法迁移到其他查询类小工具中。目前已有3745人浏览学习适合需要巩固Python综合编程能力的入门开发者。1. 天气查询界面把爬虫和可视化绑在一起的实用项目你是不是也遇到过这个场景打开天气 App 想看个温度结果迎面是开屏广告、推送弹窗等真正看到数据已经过了十五秒。不如自己写一个 Python 界面输入城市回车温度曲线和天气图标直接铺在眼前。这就是“python可视化爬虫界面之天气查询”这个标题真正在做的事——用爬虫拿数据用可视化组件把数据变成能看的图表再套一个 GUI 让操作变得顺手。这个项目最吸引人的地方在于它的技术栈非常“正”。数据采集你会碰到 Requests、API 授权、HTML 解析可视化你会碰到 PyECharts 或 Matplotlib 的渲染链路界面部分又会涉及 Tkinter 的事件循环、线程通信。它不像纯抓数据的脚本那样一跑就完也不像纯前端图表那样没有数据源而是要你在一个窗口里同时处理好“拿数据”和“展示数据”这两件事。对入门者来说这是一个能完整走通的练手项目对写了几年业务代码的人来说这个项目里的线程模型和缓存设计也值得琢磨一番。2. 天气数据爬取Requests 与 BeautifulSoup 的接口选型2.1 数据源选型开放 API 优先HTML 兜底做天气查询第一步是决定数据从哪儿来。常见做法是优先找不需要注册就可以用的公开 API比如 Open-Meteo它提供全球天气数据支持 JSON 返回没有 API Key 限制适合做教程和本地工具。退而求其次如果目标城市在某个固定网站上有实时天气页面也可以用 Requests 拿 HTML再用 BeautifulSoup 解析标签。两种方式各有适用场景你要做的是通用城市查询API 更稳定你要完全定制特定网站的格式HTML 解析反而更灵活。这里不建议一开始就上 Selenium 或 Playwright 做浏览器渲染那会引入太多无关的复杂度和性能开销。除非目标网站强制要求 JavaScript 渲染后才能看到天气数据否则用 Requests 拿静态 HTML 或直接调 JSON 接口已经覆盖了绝大多数天气查询需求。2.2 用 Requests 请求 Open-Meteo 接口的最小代码Open-Meteo 的调用非常简单只需要经纬度和需要的字段返回 JSON 结构清晰。先写一个独立的抓取函数方便后续在界面层直接调用。import requests def fetch_weather(lat: float, lon: float) - dict: url https://api.open-meteo.com/v1/forecast params { latitude: lat, longitude: lon, current: temperature_2m,relative_humidity_2m,wind_speed_10m, hourly: temperature_2m, forecast_days: 3, timezone: Asia/Shanghai, } resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json()代码说明params里current声明了当前时刻需要返回的字段hourly声明了逐小时温度序列forecast_days3让返回结果包含三天数据。timeout10是必须设置的否则网络异常时程序会无限挂起。raise_for_status()会在返回码不是 200 时抛出异常避免把错误内容当正常数据解析。调用方式就是把城市的经纬度传进去。经纬度可以用一个简单的字典做静态映射也可以接入地理编码 API。Open-Meteo 也提供了 geocoding 接口但这里为了减少请求次数先手动维护几个常用城市。2.3 用 BeautifulSoup 解析网页 HTML 的兼容写法如果某些天气网站没有公开 JSON 接口只能从 HTML 中提取数据那就要用到 BeautifulSoup。下面是一个通用的解析框架假设页面中温度信息存在于classtemp的span标签里。from bs4 import BeautifulSoup def parse_weather_from_html(html: str, temp_class: str) - str | None: soup BeautifulSoup(html, html.parser) tag soup.find(span, class_temp_class) if tag is None: return None return tag.get_text(stripTrue)逻辑说明先构造 BeautifulSoup 对象再通过find定位第一个匹配的标签。get_text(stripTrue)会去掉文字前后的空白。遇到找不到标签的情况返回None而不是抛异常因为极端情况下页面结构可能变化得很快宁可让界面端提示“数据解析失败”也不能直接崩溃。写 HTML 解析时一个务实的技巧是先打开开发者工具找到温度字段实际渲染出来的class名称再在代码里写死。不要依赖class里带有随机后缀的中间层优先找更稳定的特征。如果页面采用动态渲染可以先检查requests返回的文本里是否有温度值如果没有再考虑用渲染工具。2.4 常见参数与反爬限制对照表爬取天气数据时有几个参数直接影响请求的成败和效率整理成表方便你对照调整。参数推荐值作用timeout5~10 秒避免网络异常导致进程卡死headersUser-Agent 填入常见浏览器防止部分服务器拒绝 Python 默认 UAretries2~3 次应对瞬时网络波动cache内存字典或文件缓存相同城市短时间内不重复请求请求频率每小时不要超过几十次降低被限流风险有些网站会检查Referer或Cookie如果遇到 403需要把浏览器里的完整请求头复制出来填进headers。但要注意这属于底线抗性不意味着可以高频率抓取。天气数据更新频率本身很低几分钟才变一次做一个本地查询工具完全不需要频繁打接口后面章节会讲到如何用缓存来保护自己和对方服务器。3. 可视化界面Tkinter 与 PyECharts 的落地3.1 为什么选 TkinterPython 自带的零依赖 GUI可视化界面的方案有很多PyQt 功能更强但安装包体积大、许可证需要额外注意。Tkinter 是 Python 标准库自带的任何环境只要装了 Python 就能跑不用管pip install那一堆依赖。对于天气查询这种单窗口、控件不多的工具Tkinter 完全够用而且入门门槛低一个Label、一个Entry、一个Button就能撑起整个交互。选择 Tkinter 还有一个隐藏的好处它的事件循环天然是单线程的这反倒逼着你去思考怎么把耗时的网络请求放到后台线程里处理。这种思考对理解 GUI 程序的架构至关重要即便以后转到 PyQt 或 Web 前端道理都是相通的。3.2 用 Tkinter 搭建查询窗口的最小代码先做一个能输城市、能点按钮、能显示文字的骨架。import tkinter as tk from tkinter import messagebox class WeatherApp: def __init__(self, root): self.root root root.title(城市天气查询) root.geometry(480x360) self.city_label tk.Label(root, text城市名称:) self.city_label.pack(pady10) self.city_entry tk.Entry(root, width30) self.city_entry.pack(pady5) self.query_btn tk.Button(root, text查询天气, commandself.on_query) self.query_btn.pack(pady10) self.result_text tk.Text(root, height8, width50) self.result_text.pack(pady10) def on_query(self): city self.city_entry.get().strip() if not city: messagebox.showwarning(提示, 请输入城市名) return # 暂时显示占位内容后续接爬虫 self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, f{city} 的天气数据加载中...) app WeatherApp(tk.Tk()) app.root.mainloop()代码说明self.city_entry.get()拿到输入框内容.strip()去掉首尾空格。点击按钮后先校验非空再更新文本框。这里还没有接爬虫先保证界面能正常显示和交互。tk.Text组件用来展示多行文本比Label更适合输出多行结果也可以用来临时显示原始 JSON。3.3 用 Matplotlib 嵌入 Tkinter 绘制温度曲线可视化部分要解决核心问题把爬到的温度数组画成图表并且这个图表能长在 Tkinter 窗口里。有两个成熟方案一是用matplotlib.backends.backend_tkagg里的FigureCanvasTkAgg把图表画在 Tkinter 的 Canvas 上二是用 PyECharts 生成 HTML 文件再通过浏览器组件加载。前者更轻量后者更漂亮我们先用前者保证可复现性。import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg def draw_temperature_curve(self, hours: list, temps: list): # 清空旧的图 for widget in self.root.winfo_children(): if isinstance(widget, FigureCanvasTkAgg): widget.get_tk_widget().destroy() fig, ax plt.subplots(figsize(5, 2.5)) ax.plot(hours, temps, markero, linestyle-, color#1f77b4) ax.set_xlabel(小时) ax.set_ylabel(温度 (°C)) ax.set_title(未来24小时温度变化) ax.grid(True, linestyle--, alpha0.6) ax.xaxis.set_major_locator(plt.MaxNLocator(6)) canvas FigureCanvasTkAgg(fig, masterself.root) canvas.draw() canvas.get_tk_widget().pack(pady5)参数说明figsize(5,2.5)控制图表宽高根据窗口布局可调。MaxNLocator(6)限制横轴最多显示 6 个刻度避免 24 个小时标签挤成一团。grid的linestyle--和alpha0.6让网格线不喧宾夺主。每次画图之前先销毁旧的FigureCanvasTkAgg组件否则重复点击查询会在窗口里叠出多张图。这个函数怎么和爬虫数据对上从 Open-Meteo 返回的 JSON 里hourly.time是时间字符串列表hourly.temperature_2m是对应的温度列表。你需要从时间列表里截取从当前小时开始的 24 个点然后把长度对齐再传给画图函数。细节处容易忽略的是timezone参数你请求 API 时如果不带timezone默认返回 UTC 时间画出来的横轴会和你本地时刻错开 8 个小时。3.4 图表样式参数与交互优化除了画折线天气可视化还可以增加湿度柱状图、风力风向箭头等。但不要把图表塞得过满单窗口里最好一张主图解决核心信息。参数上要注意中文字体Matplotlib 默认字体不包含中文标题里的“温度变化”显示出来是方框。需要在画图前做一次全局配置plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, PingFang SC] plt.rcParams[axes.unicode_minus] False第一行把默认字体换成系统自带的常见中文字体第二行解决负号显示异常的问题。如果你在 Linux 无中文字体环境跑可能还需要apt install fonts-wqy-zenhei装一套开源字体。4. 界面与爬虫的线程整合及缓存参数4.1 同步阻塞问题为什么界面会卡死把爬虫函数直接塞进按钮回调里点查询后整个窗口就卡成“白苹果”。原因在于 Tkinter 是单线程事件循环on_query里如果执行了requests.get()这个网络请求会阻塞事件循环期间窗口无法重绘也不能响应任何点击。即使网络很快本地缓存未命中时也会有几百毫秒的卡顿感一旦网络慢或超时程序就像死掉一样用户会忍不住再点几次。正确的做法是把耗时操作放到工作线程里执行通过queue.Queue把结果传回主线程。Tkinter 本身不允许子线程操作界面组件所以主线程必须负责更新 UI工作线程只负责拿数据。4.2 用 threading 和 Queue 实现异步刷新下面这段代码把爬虫、线程、界面串起来是项目里最关键的骨架。import threading import queue class WeatherApp: def __init__(self, root): # 省略之前的控件初始化 self.result_queue queue.Queue() self.root.after(100, self._poll_queue) def on_query(self): city self.city_entry.get().strip() if not city: messagebox.showwarning(提示, 请输入城市名) return thread threading.Thread(targetself._fetch_worker, args(city,), daemonTrue) thread.start() self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, f正在查询 {city} ...) def _fetch_worker(self, city): try: data fetch_weather_by_city(city) # 复用之前的爬虫函数 self.result_queue.put((success, city, data)) except Exception as exc: self.result_queue.put((error, city, str(exc))) def _poll_queue(self): try: status, city, payload self.result_queue.get_nowait() if status success: self._update_ui(city, payload) else: messagebox.showerror(查询失败, payload) except queue.Empty: pass self.root.after(100, self._poll_queue)逻辑说明_fetch_worker在子线程中执行网络请求耗时操作不会阻塞主线程。_poll_queue每 100 毫秒用after触发一次检查队列里是否有新结果。queue.Empty是等待处理的常态直接忽略。daemonTrue保证程序退出时子线程自动销毁不会成为僵尸线程。这里用到了root.after的固定轮询模式它是 Tkinter 多线程界面开发里最稳妥的方式比在子线程里直接用root.after调用 UI 更新要安全得多。4.3 缓存与超时参数减少重复请求天气数据的时效性要求不高同一个城市一分钟内多次查询完全可以用缓存直接返回。在类里加一个字典做内存缓存记录抓取时间戳。import time class WeatherApp: def __init__(self, root): # ... self._cache {} def _get_cached_or_fetch(self, city: str, coords: tuple, ttl: int 600): now time.time() cached self._cache.get(city) if cached and now - cached[ts] ttl: return cached[data] data fetch_weather(coords[0], coords[1]) self._cache[city] {ts: now, data: data} return data参数说明ttl600表示缓存 10 分钟超过这个时间才重新请求。因为 Open-Meteo 本身数据更新频率是小时级10 分钟已经足够。_cache字典的 key 是城市名value 是包含时间戳和数据的字典。这个缓存要放在_fetch_worker里调用还是放在工作线程里其实应该放在_fetch_worker里因为爬虫请求本身就是在工作线程里执行缓存命中时可以直接返回数据连网络请求都不用发。4.4 异常处理与日志记录网络爬虫最忌讳每次失败都弹一个模态框打断用户。更合理的做法是第一次失败时弹出提示然后更快地记录日志。建议在项目根目录加一个log目录用logging模块输出运行信息。import logging logging.basicConfig( filenameweather_app.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, ) def _fetch_worker(self, city): try: data fetch_weather_by_city(city) self.result_queue.put((success, city, data)) except Exception as exc: logging.warning(query city%s failed: %s, city, exc) self.result_queue.put((error, city, str(exc)))日志里记录城市名和失败原因后续排查是否被反爬、DNS 解析失败、接口字段变化都有迹可循。注意不要把 API Key 之类的敏感信息写入日志本项目的 Open-Meteo 不需要 Key这一点天然安全。5. 进阶与验证打包成可执行文件与单元测试5.1 用 PyInstaller 打包一个不依赖 Python 环境的天气工具自己电脑上跑通以后想让同事双击就能用最直接的办法是用 PyInstaller 打包成单个 exe 文件。在项目根目录执行pip install pyinstaller pyinstaller --onefile --windowed --name weather_app weather_app.py参数说明--onefile把所有依赖打进单个 exe--windowed在 Windows 下运行时不会弹出黑色命令窗口。--name自定义产物名称。打包出来的文件体积会在 3060 MB 之间因为会把 Python 解释器和 Matplotlib 等依赖包含进去。如果体积太大可以改用--exclude-moduletkinter之外的库做减法但要注意别把实际用到的库剪掉了。打包后有一个常见坑程序用 Matplotlib 画图时在--windowed模式下中文字体可能出现问题。因为 PyInstaller 不会自动包含系统字体目录。解决方法是把字体文件复制到项目目录在代码里用matplotlib.font_manager.FontProperties显式指定字体路径。5.2 用 unittest 模拟爬虫返回数据验证界面逻辑写测试不是开发完以后才做的事而是为了验证“爬虫返回异常数据时界面怎么表现”。用unittest.mock模拟爬虫函数的返回结果可以不依赖网络测试界面逻辑。import unittest from unittest.mock import patch from weather_app import WeatherApp class TestWeatherApp(unittest.TestCase): patch(weather_app.fetch_weather_by_city) def test_success(self, mock_fetch): mock_fetch.return_value { current: {temperature_2m: 20.3}, hourly: {time: [2025-01-01T00:00], temperature_2m: [20.1]}, } app WeatherApp.__new__(WeatherApp) app._cache {} app._fetch_worker(北京) status, city, data app.result_queue.get_nowait() self.assertEqual(status, success) self.assertEqual(data[current][temperature_2m], 20.3)测试说明patch装饰器把fetch_weather_by_city替换成假函数测试时不会发出真实网络请求。这里用WeatherApp.__new__绕过了__init__对 Tkinter 界面的依赖只测试工作线程和队列传值逻辑。如果要在真实窗口里跑测试可以用root.withdraw()隐藏窗口。5.3 一个实战技巧城市名自动补全与历史记录界面体验上值得花功夫的是输入框的自动补全。把常见城市列表放在tk.Listbox里用户敲一个字就过滤一次。这个功能用 Tkinter 实现并不复杂核心是绑定键盘事件def on_key_release(self, event): typed self.city_entry.get().strip().lower() self.city_listbox.delete(0, tk.END) for city in self.city_names: if typed in city.lower(): self.city_listbox.insert(tk.END, city)然后再把历史查询记录存到json文件里启动时恢复最近十条记录方便快速点选。这个细节看似小事却让工具真正做到“查询界面”的完整闭环。做完这些你手里的项目已经不只是一个玩具而是一个带缓存、异步、日志、打包、测试的完整桌面应用。本文还有配套的精品资源点击获取