简介这款Python天气查询小程序是面向初学者的API调用与数据解析实践参考核心演示如何用requests库向天气服务发送GET请求并通过json库把返回数据解析为Python字典从而提取温度、湿度、风速等信息。程序包含命令行交互支持通过sys.argv接收城市参数也可预设查询北京天气方便快速体验不同区域的实时天气。资源包为RAR压缩格式共两个文件包含一个Python主程序脚本和一个城市索引数据库文件整体仅31KB轻量易部署。该资源已有四百六十三人学习下载属于小而精的入门项目。读者拿到后可直接运行程序查看天气也可修改参数扩展城市结合城市索引数据库还能理解城市代码与天气数据索引的配合方式同时可参考描述中提到的错误处理思路学习如何校验输入地点、判断API请求是否成功并优化天气信息的友好展示。1. 天气查询小程序从入门项目到能日常用的完整链路用 python 写一个天气查询小程序听起来只是进阶练习调一个公开接口打印一行温度就完事。真把它放进每天要跑的晨会脚本里你必须面对接口限流、城市名匹配、编码报错、时区偏移这些细节——任何一个都能让程序在最该说话的时候沉默。这份资源是一个可运行的天气查询小程序包含命令行与 GUI 两个入口内置超时重试、本地缓存和数据校验适合刚学完基础、准备做真实 HTTP 请求的新手也适合想把每日天气塞进内部报表的从业者。拿到手改一个 API Key 和城市参数就能跑不是只能演示的玩具代码。整套程序麻雀虽小五脏俱全足够当模板反复复用。2. 核心请求模块接口选型、参数设计与超时重试2.1 为什么选 HTTP JSON 接口三个现实约束做天气小程序先想清楚一个问题自己采气象数据不现实。大部分个人和内部工具都直接用现成的天气数据服务商注册后拿一个 API Key请求一个 URL 就返回 JSON。好处是省去采集和清洗环节坏处是字段命名、单位约定、限流策略各家都不一样代码写得再漂亮接口细节对不上一样翻车。我一般选接口只看三样免费额度够不够个人用、支不支持中文描述字段、单位能不能由请求参数指定。个人程序一天最多请求几十次免费套餐每秒个位数请求绰绰有余。反倒是单位坑很大有的服务商默认返回开尔文不传 unit 参数就拿回来一个 300 左右的数直接显示成 300 度非常离谱。下面这张参数表是我常用的通用结构具体服务商字段名可能略有出入但参数语义基本一致。参数示例说明city目标城市拼音或ID服务商识别城市的字段拼音和城市ID不保证同时支持keyabcd1234req控制台申请到的密钥放配置文件别写死在代码里unitmetricmetric 返回摄氏度imperial 返回华氏度不传可能是开尔文langzh_cn让天气描述返回中文减少一次本地翻译接口是否通先用 curl 探路比直接写 Python 调试快得多。请求地址我这里统一用占位域名接入时换成你申请到的真实接口地址即可。curl https://api.example-weather.com/v3/weather?city目标城市拼音key你的密钥unitmetriclangzh_cn返回 200 且 JSON 里有温度字段再进代码层返回 401 先检查密钥返回 404 大概率是城市拼写问题。这个自测动作能省掉后面至少半小时的定位时间。2.2 请求函数写法超时、重试与错误分级接口可能慢可能临时挂掉免费服务商还可能在你并发高时拒绝请求。所以第一版请求函数就必须包含三样东西超时时间、重试次数、错误分级。超时推荐用元组形式区分连接超时和读取超时比单独一个数字更能定位问题。import time import requests def fetch_weather(city: str, api_key: str) - dict | None: url https://api.example-weather.com/v3/weather params { city: city, # 城市拼音requests 会自动做 URL 编码 key: api_key, unit: metric, # 强制摄氏度拒绝玄学温度 lang: zh_cn, # 让描述字段直接返回中文 } # 连接超时 3 秒、读取超时 5 秒避免 DNS 假死拖住整个界面 for attempt in range(3): try: resp requests.get(url, paramsparams, timeout(3, 5)) if resp.status_code 200: return resp.json() if resp.status_code in (401, 403): # 密钥或权限问题重试没有意义直接抛出 raise PermissionError(fAPI 鉴权失败: {resp.status_code}) if resp.status_code 429: # 限流等一会儿再试 time.sleep(2 ** attempt) continue except requests.Timeout: time.sleep(2 ** attempt) continue except requests.ConnectionError: time.sleep(2 ** attempt) continue return None代码里用 requests.get 把 params 直接交给库处理requests 会按标准对中文城市名做百分号编码比手动拼 URL 再转义少一类编码翻车。重试次数 range(3)指数退避就是失败后等 2 的 0 次方秒、1 次方秒、2 次方秒。对 429 和网络异常做重试对 401/403 直接抛出因为改密钥才能解决重试一百次都白搭。有同学问要不要固定等 1 秒再试。我一般不用固定值服务商限流往往持续几十秒固定间隔显得有礼貌但没用指数退避至少能撞上恢复窗口。这个函数如果跑在无界面服务器上整个 fetch 外面再包一层 try/except返回 None 表示本次不可用千万别让异常直接崩掉任务。2.3 数据清洗把接口乱字段归一化接口返回的 JSON 通常是一大堆嵌套字段直接塞给界面层等于把黑匣子放大。我习惯在请求层下面加一个解析函数把服务商字段统一成自己的结构体以后换服务商只需要改这一个函数。def parse_weather(raw: dict) - dict | None: if not raw or data not in raw: return None node raw[data][0] # 多数接口把当前天气放在 data 列表里 try: temp float(node.get(temp_c, node.get(temperature, -999))) # 部分接口即使在 metric 下也返回开尔文做一次数量级兜底 if temp 200: temp temp - 273.15 humidity float(node.get(humidity, -1)) wind_kph float(node.get(wind_kph, node.get(wind_speed, -1))) return { city: node.get(city_name, ), text: node.get(text, node.get(condition, 未知)), temp_c: temp, feelslike_c: temp, # 缺省时先用当前温度顶上 humidity: humidity, wind_kph: wind_kph, } except (TypeError, ValueError): return None这里把温度、湿度、风速统一成 float。数量级兜底那行是经验有服务商对 unit 参数视而不见拿回来 301.9 这种值减 273.15 才变成人话。缺字段时用 -1 或 None 兜住界面层拿到 -1 就显示“暂缺”而不是显示一个负数湿度。字段归一化讲究一个原则接口叫 wind_kph界面叫 wind_speed那就在这一层完成改名别在 UI 层写一堆 if 判断。这个函数的返回值是整个程序内部传递的数据契约后面缓存、GUI 都基于这个结构改起来才安全。3. 把 JSON 变成人话解析、本地缓存与极端值兜底3.1 描述字段本地映射别让界面出现英文状态服务商的 langzh_cn 不一定完全可靠有的接口对“阴天”返回 overcast对“小到中雨”返回 light_moderate_rain。可以在解析函数里加一个映射字典把这些英文键翻译成中文展示层才能统一。DESC_MAP { clear: 晴, partly_cloudy: 多云, overcast: 阴, light_rain: 小雨, moderate_rain: 中雨, heavy_rain: 大雨, } def trans_text(text: str) - str: if not text: return 未知 return DESC_MAP.get(text, text)映射表放在独立词典里因为它纯属展示逻辑跟计算逻辑分开改起来不用翻整个文件。注意顺序先取接口自带的中文描述取不到再查映射表两者都没有用“未知”兜底。词表是有限的几十个 key 足够覆盖日常场景真正容易漏的是“零星小雨”这类复合词把 drizzle 之类的变体也加进去即可。3.2 本地 TTL 缓存把 5 分钟内的重复请求挡在门外命令行版本如果每次敲击都实时请求连续敲几次很容易撞上限流而且启动速度会慢得让人怀疑程序坏了。所以加了一个非常轻的 JSON 文件缓存默认 600 秒。判别逻辑很简单缓存不存在、缓存过期、缓存文件损坏三种情况都重新请求。import json import os import time CACHE_PATH os.path.join(os.path.expanduser(~), .weather_forecast_cache.json) CACHE_TTL 600 def load_cache(city: str) - dict | None: try: with open(CACHE_PATH, r, encodingutf-8) as f: payload json.load(f) except (FileNotFoundError, json.JSONDecodeError): return None if payload.get(city) ! city: return None if time.time() - payload.get(ts, 0) CACHE_TTL: return None return payload.get(data) def save_cache(city: str, data: dict) - None: payload {city: city, ts: time.time(), data: data} # 先写临时文件再替换防止写入一半进程被杀导致 JSON 损坏 tmp_path CACHE_PATH .tmp with open(tmp_path, w, encodingutf-8) as f: json.dump(payload, f, ensure_asciiFalse) os.replace(tmp_path, CACHE_PATH)CACHE_PATH 放在用户目录而不是程序目录是为了打包成 exe 后还能正常读写。缓存文件损坏要静默处理很多人直接把 json.load 放在主流程里文件坏一次程序就崩一次。这里捕获两个异常并返回 None让上层走正常的网络请求等于自动修复缓存。TTL 设 600 秒意思是连续 10 分钟内查同一个城市只有第一次走网络后五次直接读本地文件。如果部署在服务器上做定时任务建议把 TTL 压到 300 秒配合第 6 章的定时播报播报前强制刷新缓存。3.3 数量级校验把数据源异常挡在 UI 外面前面提到开尔文误判这只是数据源异常的一种。更常见的是某天湿度字段突然返回 -1 或 999风速变成 500 km/h这种值一旦显示出来用户只会觉得程序有问题而不是数据源有问题。我习惯在解析函数最后加一轮校验。def validate_weather(data: dict) - bool: if data[humidity] 0 or data[humidity] 100: return False if data[temp_c] -80 or data[temp_c] 60: return False if data[wind_kph] 0 or data[wind_kph] 300: return False return True这个边界按地球气候最大范围设计温度 -80 到 60湿度 0 到 100风速 0 到 300。不在范围内的数据直接判为无效程序显示“数据异常”而不是把 999% 湿度打在屏幕上。注意这只是兜底不是严谨的数据质检真正的气象数据还有时空连续性判断那种算法对个人小程序来说太重了。4. 交互与打包从控制台到 GUI再到多端可用的 exe4.1 命令行入口参数、退出码与可脚本化命令行版本的核心不是打印字符串而是退出码。同一个工具既要人敲也可能被别的脚本调用退出码 0/2/3 就能告诉调用方“成功、参数错、接口失败”。用 argparse 接收城市和刷新参数这样就能让定时任务直接调。import argparse import sys def main() - int: parser argparse.ArgumentParser(description天气查询命令行版) parser.add_argument(--city, requiredTrue, help目标城市拼音) parser.add_argument(--refresh, actionstore_true, help忽略缓存强制刷新) args parser.parse_args() data load_cache(args.city) if args.refresh or data is None: raw fetch_weather(args.city, API_KEY) data parse_weather(raw) if data is None: return 3 if not validate_weather(data): return 3 save_cache(args.city, data) print(f{data[text]} {data[temp_c]:.1f}°C 湿度{data[humidity]:.0f}%) if __name__ __main__: sys.exit(main())这里把请求、解析、校验、缓存串成一条链路。参数校验失败由 argparse 直接抛 SystemExit返回码是 2网络或数据异常统一返回 3正常路径返回 0。这样计划任务里写python cli.py --city xxx || 写日志就能拿到明确的成功与否信号。refresh 参数是个后悔药开关缓存有 bug 或者想立刻看最新温度时不用删缓存文件加一个 --refresh 强制走网络。4.2 GUI 版tkinter 天气卡片与线程分离很多人第一次写 GUI 会卡在“点查询按钮后窗口假死”上原因就是网络请求在 UI 线程里同步执行界面只能干等。解决方法是把请求丢进子线程完成后用 after 回调更新控件而不是在子线程里直接改控件。import tkinter as tk from threading import Thread class WeatherApp: def __init__(self, root: tk.Tk) - None: self.root root self.city_var tk.StringVar(value目标城市) self.info_var tk.StringVar(value输入城市后点查询) entry tk.Entry(self.root, textvariableself.city_var) entry.pack() btn tk.Button(self.root, text查询, commandself.query) btn.pack() tk.Label(self.root, textvariableself.info_var).pack() def query(self) - None: city self.city_var.get().strip() if not city: return self.info_var.set(查询中...) Thread(targetself._fetch_async, args(city,), daemonTrue).start() def _fetch_async(self, city: str) - None: raw fetch_weather(city, API_KEY) data parse_weather(raw) self.root.after(0, self._update_result, data) def _update_result(self, data: dict | None) - None: if data is None: self.info_var.set(查询失败请检查网络或城市名) else: self.info_var.set( f{data[text]} {data[temp_c]:.1f}°C 湿度{data[humidity]:.0f}% f风速{data[wind_kph]:.0f}km/h )tkinter 是内置库不需要额外装包打包体积也小。Thread 设置成 daemon程序退出时子线程不会阻塞。容易忽略的是 after 的线程安全性子线程执行完用 root.after(0, ...) 把更新动作交回主循环避免界面控件被并发修改。如果直接在子线程里调用 set偶发崩溃会让你以为程序有随机 bug。嫌 tkinter 难看可以换 PySide6但线程分离的思路不变。4.3 打包成单文件PyInstaller 的依赖与路径坑控制台和 GUI 都调通后最终给同事用的通常是一个 exe。PyInstaller 打包 tkinter 程序比较容易因为内置库识别率高。pyinstaller --noconfirm --onefile --windowed --name weather_query main_gui.py--windowed 表示去掉黑色控制台窗口运行纯 GUI 版如果留一个控制台版调试去掉这个参数就行。打包时注意 main_gui.py 里不要用相对路径找配置文件。前面缓存路径已经放在用户目录配置文件和 API Key 建议从环境变量或 config.yaml 读取避免把密钥打进 exe 后泄露。有些机器上运行 exe 报缺 MSVCP140.dll那是 VC 运行库缺失不是代码问题补装运行库即可。打包产物 30 到 50 MB 是正常现象。还有一条血泪经验exe 放到别人电脑上可能被杀毒软件隔离常见做法是加开发者签名或者提交给杀毒厂商报备。个人用不想折腾就先加白名单。5. 避坑专题编码、限流、时区与城市查不到的四个翻车现场5.1 中文 Windows 下 print 天气描述报编码错误现象控制台版在中文系统里一运行print 天气描述时就抛UnicodeEncodeError: gbk codec cant encode character前面几行输出正常遇到生僻字就崩。原因Windows 控制台默认编码是 GBK部分汉字或符号映射不到。天气接口返回的中文经过网络解析后是 Unicodeprint 到终端时尝试编码成 GBK编码失败就抛异常。解决程序启动后立刻对标准输出重设编码为 UTF-8import sys if hasattr(sys.stdout, reconfigure): sys.stdout.reconfigure(encodingutf-8)这行代码放在 main 入口最前面。GUI 版不涉及这个问题因为显示控件用的是文本变量不是控制台。注意在 cmd 里跑还需要先执行chcp 65001切换代码页否则输出可能乱码PowerShell 通常没这个问题。5.2 连续点查询被限流429 与 403 交替出现现象手动调试时连续刷新几十次接口开始返回 429 Too Many Requests偶尔还变成 403过十几分钟自己恢复期间拿不到任何有效数据。原因免费套餐通常有 QPS 限制。手动点击按钮的间隔可能不到 1 秒瞬时并发一超过阈值服务端就把后续请求挡掉403 有时是限流策略的另一种表现。解决第 3 章的 600 秒缓存是第一步把同一个城市 10 分钟内的重复请求挡住。第二步是改请求节奏GUI 版把按钮在请求期间禁用连续点击只发一次。第三步是退避重试429 时等待 2 秒、4 秒、8 秒后再试而不是立刻狂点。fetch_weather 里已经对 429 做最多三次退避整个程序最多等待 14 秒不会无限卡死。提示调试限流时不要一看到 403 就怀疑密钥泄露先看响应头里的限流余量字段再决定是换密钥还是降频率。5.3 凌晨显示昨天温度时间戳时区问题现象早上 8 点查到的“最近更新时间”显示昨天或者实况温度与外面明显差了 8 个小时。原因接口返回的 update_time 或 ts 字段是 UTC 时间戳程序直接datetime.fromtimestamp(ts)会生成当前时区的时间如果服务商文档没写明 UTC很容易把它当成当地时间。实况字段本身不直接带时间段但做“今日/昨日”分类时时区歧义就会暴露。解决优先让服务商返回字符串时间比如update_time: 2024-05-20 14:00:00它已经带时区格式。若只能拿到 UTC 时间戳就把服务商返回的 timezone_offset 悄悄算进去from datetime import datetime, timedelta utc_ts 1716199200 local_dt datetime.utcfromtimestamp(utc_ts) timedelta(seconds8 * 3600)写这种转换时最好在代码里注释一句“服务商返回 UTC”不然三个月后自己都看不懂为什么加 8 小时。更省事的是请求参数里加 tz 字段能少做一步换算就少做。5.4 城市拼音查不到县级市直接报未知城市现象输入常见市区拼音能查到换某个县级市名就报 Unknown City反复确认拼写仍然失败。原因很多免费接口的城市库只收录市区或要求用城市 ID 匹配。拼音搜索是按名称匹配的县级市名称没入库自然查不到。这不是代码问题是服务商数据覆盖范围的问题。解决优先用免费服务商的城市搜索接口换取 city_id再拿 city_id 请求天气。例如先请求https://api.example-weather.com/v3/search?keyword目标城市返回数据里取 id 字段缓存下来。城市 ID 通常是纯数字或短字符串比拼音稳定也避免同名城市歧义。查过一次以后把 id 存进缓存文件下次直接使用不用再走搜索。如果连搜索接口都没有就用上级城市名代替同时在显示文案里加一个“离你最近的收录城市”标注避免误导。6. 进阶玩法自动定位、定时播报和一周温度曲线6.1 用 IP 定位拿城市再查天气不想手敲城市名的场景下IP 定位是最省事的方案。请求一个 IP 定位接口返回城市名再交给天气接口。def locate_by_ip() - str | None: try: resp requests.get(https://ip-api.example.net/json, timeout3) data resp.json() return data.get(city) except (requests.RequestException, ValueError): return NoneIP 定位只能精确到城市级别对天气查询够用。链路变成定位城市 → 查天气 → 填缓存全程不需要人工输入。6.2 定时播报轻量调度器挂晨间任务用 sched 模块做一个脚本内循环每天早上播报一次适合放在服务器上常驻。import sched import time def morning_job(): city load_config(city) raw fetch_weather(city, API_KEY) data parse_weather(raw) save_cache(city, data) print(f{time.strftime(%Y-%m-%d %H:%M)} 今日{data[text]} {data[temp_c]:.1f}°C)sched 适合脚本自身的循环要长期稳定运行建议直接用系统计划任务调 cli.py系统级的重启和日志管理都比脚本内循环可靠。6.3 把一周预报画成折线晨会报表场景里一张图比一段文字直观得多。把接口返回的 weekly 字段解析成日期、最高温、最低温列表用 matplotlib 画折线import matplotlib.pyplot as plt dates [周一, 周二, 周三, 周四, 周五] highs [28, 30, 29, 26, 24] lows [18, 20, 19, 17, 15] plt.plot(dates, highs, markero, label最高温) plt.plot(dates, lows, markero, label最低温) plt.legend() plt.savefig(weekly.png, dpi150)画图不是天气小程序的核心但放在晨会报表里很好用。这套资源里通常还会包含一个 draw_weekly.pyCSV 落盘字段直接对齐解析层的 dict 结构生成图片后自动存到用户目录。我最初做这套小程序时把 API Key 直接写死在代码里发给同事后来对方更换服务商整个程序静默失效排查了半天才发现是解析函数只认旧字段。从那以后我每次都强制走一遍Key 从配置文件读、超时重试默认开、缓存路径放用户目录、打印出来的温度先过数量级校验。这套流程看起来琐碎但能把一个“能跑”的玩具变成一个“每天敢用”的工具。希望帮到你。本文还有配套的精品资源点击获取