简介:这份资源是一个用 Python 编写的天气查询小程序,面向刚接触网络请求与 API 交互的编程初学者,以及想通过小项目巩固 Python 基础的开发者,可用于查询指定地区的实时天气信息。压缩包共 2 个文件,以 1 个 py 脚本和 1 个 db 数据库文件为主,脚本承载天气查询的核心逻辑,数据库文件用于存放城市索引等基础数据,整体约 31KB,体量轻便、便于直接运行与阅读。目前已有 463 人学习下载,具备一定的参考热度。程序围绕 requests 与 json 两个核心库展开,演示了如何向天气 API 发送 GET 请求、解析返回的 JSON 数据并提取温度、湿度、风速等信息,同时涉及 sys.argv 命令行参数处理、API 密钥传递以及输入校验与请求异常处理等细节,读者可据此理解 Python 与在线服务集成的完整链路,并在此基础上扩展天气图标显示等功能。
1. 一个命令行天气查询小工具,为什么值得拆
上周同事在群里问「明天要不要带伞」,我顺手在终端敲了条命令,三秒后气温、湿度、未来两天降水概率全出来了。他以为我装了什么插件,其实就是一个用 Python 写的天气查询小程序——输入城市名,调公开天气接口,把 JSON 解析成人类能读的几行字。这类项目看着简单,但麻雀虽小五脏俱全:HTTP 请求、异常处理、数据解析、命令行参数、缓存策略,一个完整的小型工程该有的它都有。适合刚学完 Python 语法想找个练手项目的新手,也适合想快速搭个内部小工具的老手。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这个项目的骨架和关键实现拆开讲。
2. 项目定位与技术选型:为什么是 requests + argparse 这套组合
2.1 这个项目到底解决什么问题
天气查询是个典型的「输入极简、输出结构化」场景。用户只需要给一个城市名,程序负责拼请求、发 HTTP、解析响应、格式化输出。它不涉及数据库、不涉及前端渲染、不涉及用户系统,所以技术栈可以压到最薄。项目正文没给具体依赖清单,但按这个场景的常见做法,核心依赖就两个:requests负责 HTTP 通信,argparse负责命令行参数解析。前者比urllib少写一半样板代码,后者是标准库自带、零安装成本。如果你想让输出带颜色,可以加colorama;想缓存查询结果,标准库的json+ 文件读写就够了,不必上 Redis。
这个项目的价值不在于「查天气」本身——手机上一划就有——而在于它是一个可复现的最小工程模板。你把它跑通之后,换掉接口地址和解析逻辑,就能变成汇率查询、快递追踪、股票行情,骨架完全复用。
2.2 技术栈拆解与选型理由
| 层次 | 选型 | 理由 |
|---|---|---|
| 网络请求 | requests | 同步阻塞模型足够,API 简单,异常类型清晰 |
| 参数解析 | argparse | 标准库,支持位置参数和可选参数,自带 help |
| 数据解析 | json(标准库) | 天气接口返回 JSON,无需额外依赖 |
| 输出格式化 | 字符串格式化 + 可选 colorama | 终端可读性优先 |
| 缓存 | 本地 JSON 文件 + 时间戳 | 避免频繁请求触发接口限流 |
选requests而不是aiohttp,是因为天气查询是低频操作,一个人一天查不了几次,异步带来的复杂度远大于收益。选argparse而不是click,是因为这个项目的参数就一个城市名加几个开关,标准库完全够用,少一个第三方依赖就少一个安装失败的坑。这种「够用就好」的选型思路,在小工具类项目里比堆技术栈更值得学。
2.3 目录结构长什么样
项目正文没给目录树,但按这类小程序的常见组织方式,我一般会拆成下面这样,既不过度设计,也不把所有逻辑塞一个文件:
weather-cli/ ├── main.py # 入口,参数解析与流程编排 ├── weather/ │ ├── __init__.py │ ├── api.py # 接口请求与响应处理 │ ├── parser.py # JSON 字段提取与格式化 │ └── cache.py # 本地缓存读写 ├── config.py # 接口地址、超时、缓存过期时间 └── requirements.txtmain.py只做三件事:解析参数、调api拿数据、调parser输出。业务逻辑全在weather/包里,这样你以后想换接口,只动api.py;想改输出样式,只动parser.py。新手容易把所有代码写进一个main.py,跑是能跑,但改起来就是牵一发动全身。
3. 跑起来:从零到第一条天气输出的完整步骤
3.1 环境准备与依赖安装
先确认 Python 版本。这个项目用到的语法在 3.7 以上都没问题,建议直接用 3.8+。虚拟环境是血泪经验——我见过太多人把依赖装进系统 Python,后来另一个项目要不同版本的requests直接翻车。所以第一步永远是建虚拟环境:
# 创建并激活虚拟环境(Windows 用 venv\Scripts\activate) python -m venv venv source venv/bin/activate # 安装依赖 pip install requests如果你要用颜色输出,再加一句pip install colorama。requirements.txt里就写这两行,版本号可以不锁死,这类小工具对版本不敏感。装完之后用pip list确认requests在列表里,不在的话检查是不是虚拟环境没激活——这是新手最高频的翻车点,终端前面没有(venv)前缀就说明没进去。
3.2 接口配置与第一个请求
天气数据来自公开接口,项目里通常把地址和密钥放在config.py。不同接口的字段名不一样,下面用常见的「当前天气」结构举例,你拿到自己的接口文档后对照改字段名即可:
# config.py API_URL = "https://api.example-weather.com/v1/current" API_KEY = "your_api_key_here" # 替换成自己申请的 key TIMEOUT = 8 # 请求超时秒数,别设太长 CACHE_TTL = 600 # 缓存有效期,单位秒# weather/api.py import requests from config import API_URL, API_KEY, TIMEOUT def fetch_weather(city: str) -> dict: """请求天气接口,返回原始 JSON 字典。""" params = { "city": city, "key": API_KEY, "lang": "zh", # 中文返回 } try: resp = requests.get(API_URL, params=params, timeout=TIMEOUT) resp.raise_for_status() # 4xx/5xx 直接抛异常 return resp.json() except requests.Timeout: raise RuntimeError(f"请求超时({TIMEOUT}s),检查网络或调大 TIMEOUT") except requests.HTTPError as e: raise RuntimeError(f"接口返回错误:{e.response.status_code}")这段代码有三个关键点。第一,params字典交给requests拼查询串,不要自己手拼 URL,中文城市名手拼容易漏编码。第二,timeout必须显式设置,不设的话请求可能挂几十秒,用户体验极差。第三,raise_for_status()把 HTTP 错误转成异常,配合try/except给出人话提示,而不是让用户看到一坨 traceback。参数说明:city是位置参数,传中文或拼音看接口支持;lang控制返回语言,有些接口用language字段名,以文档为准。
3.3 解析响应并格式化输出
拿到 JSON 之后不能直接print,那出来的是一坨嵌套字典。parser.py的职责是把字段提出来,拼成终端里一眼能读的几行:
# weather/parser.py def format_weather(raw: dict) -> str: """把接口原始 JSON 转成可读文本。字段名按实际接口调整。""" try: cur = raw["data"]["current"] city = raw["data"]["city"] temp = cur["temp"] humidity = cur["humidity"] desc = cur["weather"] wind = cur.get("wind_speed", "未知") except KeyError as e: raise RuntimeError(f"接口字段缺失:{e},检查接口版本是否变更") lines = [ f"城市:{city}", f"天气:{desc}", f"气温:{temp}°C", f"湿度:{humidity}%", f"风速:{wind} m/s", ] return "\n".join(lines)try/except KeyError是这里的保险丝。天气接口偶尔会改字段名或某些城市缺字段,没有这层保护,用户看到的就是KeyError: 'humidity'这种黑匣子报错。cur.get("wind_speed", "未知")用get给可选字段兜底,必填字段才用[]直接取。输出用"\n".join()而不是多次print,方便以后加颜色或写日志时整体处理。
3.4 入口编排与命令行参数
main.py把上面几块串起来,argparse负责接住用户输入:
# main.py import argparse from weather.api import fetch_weather from weather.parser import format_weather from weather.cache import load_cache, save_cache def main(): parser = argparse.ArgumentParser(description="命令行天气查询") parser.add_argument("city", help="城市名,如 北京") parser.add_argument("--no-cache", action="store_true", help="跳过缓存强制请求") args = parser.parse_args() if not args.no_cache: cached = load_cache(args.city) if cached: print(format_weather(cached)) return raw = fetch_weather(args.city) save_cache(args.city, raw) print(format_weather(raw)) if __name__ == "__main__": main()跑起来就是python main.py 北京,加--no-cache强制走网络。argparse的action="store_true"表示这个开关出现即为真,不出现为假,适合布尔型选项。if __name__ == "__main__"这层判断别省,否则这个文件被别的模块 import 时会直接执行查询逻辑。到这里,一个能跑的最小闭环就完成了:参数进、请求发、JSON 解析、文本出。
4. 核心模块拆解:缓存、异常与请求链路的关键实现
4.1 缓存模块:省的不只是流量
天气数据十分钟内基本不变,每次查询都打接口既慢又容易触发限流。缓存模块用本地 JSON 文件存「城市 → 数据 + 时间戳」,读的时候比对时间差:
# weather/cache.py import json, os, time from config import CACHE_TTL CACHE_DIR = os.path.join(os.path.expanduser("~"), ".weather_cache") def _path(city: str) -> str: os.makedirs(CACHE_DIR, exist_ok=True) return os.path.join(CACHE_DIR, f"{city}.json") def load_cache(city: str): """命中且未过期返回数据,否则返回 None。""" p = _path(city) if not os.path.exists(p): return None try: with open(p, "r", encoding="utf-8") as f: obj = json.load(f) if time.time() - obj["ts"] < CACHE_TTL: return obj["data"] except (json.JSONDecodeError, KeyError): pass # 缓存损坏就当没有,不阻断主流程 return None def save_cache(city: str, data: dict): with open(_path(city), "w", encoding="utf-8") as f: json.dump({"ts": time.time(), "data": data}, f, ensure_ascii=False)几个细节值得说。缓存目录放在用户主目录下的隐藏文件夹,而不是项目目录,这样换工作目录也能命中。ensure_ascii=False让中文城市名在文件里正常显示,方便调试时直接打开看。load_cache里捕获JSONDecodeError和KeyError后静默返回None——缓存是加速手段,坏了就退化成直接请求,绝不能因为缓存文件损坏让整个程序崩掉。CACHE_TTL设 600 秒是个折中,太短等于没缓存,太长天气变了你还看旧数据。
4.2 异常分层:让报错说人话
这个项目最容易写得糙的地方就是异常处理。接口超时、城市不存在、密钥失效、返回字段变更,四种情况用户看到的提示应该完全不同。我的做法是在api.py里把底层异常统一转成RuntimeError并带上人话,main.py只兜一层:
# main.py 中的调用段 try: raw = fetch_weather(args.city) except RuntimeError as e: print(f"查询失败:{e}") return这样终端里出现的是「查询失败:请求超时(8s),检查网络或调大 TIMEOUT」,而不是二十行 traceback。注意不要用裸except:把所有异常吞掉,那样连代码 bug 都被掩盖了,调试时你会怀疑人生。分层原则是:底层知道「发生了什么」,上层决定「怎么告诉用户」,中间用自定义异常或RuntimeError传递。
4.3 请求链路全景:一次查询经过哪些环节
把一次python main.py 上海的完整链路串起来看:argparse解析出city="上海"和no_cache=False→load_cache查本地文件,未命中 →fetch_weather拼参数、发 GET、8 秒超时、raise_for_status校验 →resp.json()转字典 →save_cache写文件带时间戳 →format_weather提取字段拼文本 →print输出。整条链路里任何一个环节失败,都有对应的异常出口,不会让程序卡死或输出乱码。这个「入口薄、模块清、异常有出口」的结构,就是这类小工具能长期维护的关键。
5. 避坑与排查:五个真实会遇到的坑
5.1 中文城市名请求返回空结果
现象:传「北京」接口返回空数组或 400。原因:部分接口要求 URL 编码或拼音,requests的params会自动编码,但如果接口本身只认拼音,中文就查不到。解决:先看接口文档确认参数格式,需要拼音就加一层映射字典,或者用urllib.parse.quote手动处理后再拼。排查时把resp.url打出来,看实际请求地址长什么样,比猜快得多。
5.2 密钥写死在代码里被提交
现象:代码推到公开仓库后密钥泄露,接口被刷爆。原因:API_KEY直接写在config.py里跟着代码走。解决:改成读环境变量os.environ.get("WEATHER_KEY"),本地用.env文件加载,.env加进.gitignore。这个坑我踩过一次,从那以后凡是带 key 的项目,第一步就是先写.gitignore。
5.3 缓存导致「天气不更新」
现象:明明外面下雨,程序还显示晴。原因:CACHE_TTL设太大,或者save_cache的时间戳用了错误的时间源。解决:先手动删掉~/.weather_cache下的文件验证是不是缓存问题,是的话调小CACHE_TTL,并确认time.time()和文件里的ts是同一时区同一单位(都是秒级 Unix 时间戳)。加--no-cache开关就是给这种情况留的后悔药。
5.4 接口字段变更导致 KeyError
现象:昨天还能跑,今天报KeyError: 'temp'。原因:接口升级换了字段名,或者某些城市返回结构不一致。解决:parser.py里对必填字段的KeyError捕获后打印原始 JSON 的前几百字符,对照接口文档改映射。长期做法是把字段映射抽成一个常量字典,改的时候只动一处。
5.5 虚拟环境没激活导致模块找不到
现象:ModuleNotFoundError: No module named 'requests',但明明pip install过。原因:装进了系统 Python 或另一个虚拟环境。解决:看终端提示符有没有(venv),没有就重新source venv/bin/activate;再用which python和which pip确认两者指向同一环境。这个坑新手一天能踩三次,养成「先看前缀再敲命令」的习惯就好。
6. 二次开发:换接口、加功能与验证方法
拿到这个骨架之后,最值钱的改造方向是「换数据源」。把config.py的API_URL和api.py的params换掉,再把parser.py的字段映射对着新接口文档改一遍,一个汇率查询或快递追踪工具就出来了,main.py和cache.py一行不用动。这就是前面强调「入口薄、模块清」的回报。
第二个方向是加「多城市对比」。argparse的city改成nargs="+"接收多个城市,main.py里循环调用fetch_weather,输出用表格对齐。注意循环里每个城市单独 try,一个失败不能拖垮整批:
parser.add_argument("cities", nargs="+", help="一个或多个城市名") # 调用侧 for city in args.cities: try: print(format_weather(fetch_weather(city))) except RuntimeError as e: print(f"{city} 查询失败:{e}")第三个方向是输出结构化。加一个--json开关,命中时直接json.dumps(raw, ensure_ascii=False, indent=2),方便管道给其他脚本消费。验证方法很简单:改完先跑python main.py 北京 --no-cache确认网络链路通,再跑python main.py 北京确认缓存命中,最后故意传个不存在的城市看报错是否友好。三步都过,改造就算稳了。
我自己的习惯是每次改完parser.py都拿一份保存下来的真实响应 JSON 做离线测试,不依赖网络,这样字段映射改错了立刻能发现,不用等接口抽风。从那以后我每次动解析逻辑都强制走一遍离线样本,省了大量「线上才发现字段对不上」的时间。希望这个骨架能帮你少走几步弯路,把它改成自己顺手的小工具。
本文还有配套的精品资源,点击获取