☰
Python天气查询小程序:100行代码实现终端查天气
2026/10/18 19:19:11 网站建设 项目流程

简介:这是一份由Python编写的天气查询小程序源码包,主要面向刚开始接触网络请求与数据解析的Python初学者,目的是演示如何通过公开天气接口获取实时气象信息。压缩包内共有两个文件,其中一个是以.py结尾的主程序脚本,负责发起网络请求、读取接口返回的数据,并在命令行中显示温度、湿度等信息;另一个是.db格式的城市代码数据库,用来把地点名称转换成接口需要的参数。整个资源包只有31KB大小,体积很小,结构清楚,适合直接阅读和修改;目前已有四百六十三人学习过这份资源。通过完整代码,可以学到使用网络请求库发送请求、用解析工具处理返回数据、获取命令行参数等常见技巧,也包含地点是否有效、请求是否成功等错误的处理办法;虽然代码不多,却覆盖了网络编程与第三方服务对接的主要环节,可以作为课程练习或入门项目的参考。

1. 天气查询小程序:100多行Python代码,把查当天天气变成一条终端命令

每天出门前打开天气App确认温度和风力,再决定穿什么,很多人都有这个习惯。某开发者当时为了省掉这一步,用Python写了个天气查询小程序——在终端敲一句python weather.py 目标城市,当天天气、实时温度、体感温度、风力等级和出行提示立刻就能打印出来。这个小程序不依赖浏览器,不需要图形界面,核心代码只有100多行,装好Python环境就能跑。它适合有Python基础、想练习HTTP请求和JSON解析的入门开发者,也适合任何想把天气信息接进自动化脚本的人。项目虽然小,但把命令行参数解析、第三方接口对接、数据格式化输出这条完整链路都串了起来。

2. 项目全貌与技术选型:单文件加一个第三方接口,凑出一个最小可用的天气工具

2.1 功能边界:这是一个刻意做减法的个人工具

打开一个天气App,通常看到的是未来一周预报、空气质量指数、降雨雷达图、穿衣指数、紫外线强度,甚至还有广告信息流。但天气查询小程序把功能压到了最简:输入城市名,返回当天天气、实时温度、体感温度、风力等级和一条出行提示。换句话说,它不是一个完整的天气客户端,而是一个面向终端和脚本的天气数据获取工具。

这种取舍在个人工具场景里是合理的。每天通勤前需要的信息往往就那么几项:今天天气怎么样、温度多少、风大不大、需不需要多穿一件。与其在App里翻几屏才能看到结论,不如在终端里敲一行命令直接拿到结果。对于有自动化需求的开发者,命令行工具还能被系统定时任务调度,实现每天早上自动拉取天气并推送通知,这是图形界面App很难做到的。

功能被刻意约束之后,代码复杂度也跟着降了下来。项目没有数据库、没有缓存、没有前端页面、没有后台服务,唯一的“后端”是第三方天气API。这也决定了它的适用人群:想快速入门网络请求和JSON处理的Python初学者,或者想给已有脚本补一个天气查询能力的开发者。它对运行环境几乎没有要求,只要是Python 3.6以上、能联网的机器就能跑。

技术选型有一个原则:能不用框架就不用框架。很多人一看到“小程序”就联想到Web框架或桌面GUI,但这个项目完全没有往那个方向走。它保持命令行的形态,恰恰是因为命令行接口最容易跟其他程序组合——输出可以被重定向到文件,返回值可以被shell脚本读取,甚至连Python自身的subprocess都能调用它。相比之下,GUI程序在自动化场景里就成了一个负担。

2.2 技术栈选择:requests加标准库就够了

再具体看技术栈。这个项目的外部依赖只有一个:requests库,负责以HTTP协议跟天气数据服务商通信。其余功能全部由Python标准库承担:argparse处理命令行参数,json解析接口返回的JSON文本,datetime把查询时间格式化成可读字符串,os从环境变量读取API Key。标准库加一个第三方包的组合,是这类工具最稳妥的选型,也是大多数个人开发者的默认习惯。

跟requests相比,也可以直接用Python标准库里的urllib.request发请求,区别主要在代码量和出错处理成本。用urllib写一个带查询参数和超时控制的GET请求,往往要写模板块代码,处理超时还要手动捕捉异常;用requests的话,一行requests.get(url, params=params, timeout=10)就完成同样的事。个人开发不是技术竞赛,选更省心的库是明智的。尤其是在中文编码处理上,requests对响应内容的解码比urllib更省心,中文城市名和中文返回内容都不容易变成乱码。

关于数据获取,这个项目走的是通用做法:申请一个第三方天气数据服务商的接口密钥,再按服务商提供的文档构造请求URL。这样做的优势有三点:不用自己搭建气象采集,接口返回结构化JSON易于解析,接口稳定不受页面改版影响。这类免费接口通常有调用频率限制,个人脚本每天查询几十次完全不会触达上限。如果后续需要更高频的调用,换成付费套餐也只需要改几个参数,核心代码不需要动。

一个经常被忽略的细节是中文城市名在URL里的编码问题。requests的params参数在内部会对中文做URL编码,比如城市名会被编码成百分号加十六进制的形式;如果自己手动拼URL就很容易在这里翻车,返回结果要么是乱码城市,要么直接报“城市不存在”。所以即使只是把城市名拼到URL后面,我也建议统一用params传参而不是f-string拼接,能省掉一整类编码报错的排查时间。

2.3 目录结构与模块边界:单文件的背后是一条完整数据链路

在很多入门项目里,“模块化”是被过度强调的概念,两三行逻辑也必须拆成五个文件才算标准。但天气查询小程序采用另一种风格:单文件优先。项目的物理结构非常简单,看起来像下面这样:

weather-project/ ├── weather.py # 主程序:参数解析、网络请求、数据处理、输出 ├── requirements.txt # 依赖声明:requests └── README.md # 使用说明与接口申请指引

weather.py内部依然按职责拆成了函数,而不是把所有逻辑塞进main里。parse_args()负责读取用户输入,get_weather()负责请求接口并拿回字典,format_weather()负责把字典转换成多行文本,main()负责把它们串起来。虽然同处一个文件,但每个函数只做一件事,阅读时沿着函数名往下走,就能把“输入城市名”到“打印天气”的链路看完整。

选择单文件的深层原因是降低认知负担。对一个用于解决具体问题的小工具来说,文件数量越少,打开项目时的心理成本越低;函数边界清晰,遇到问题也能快速定位。这也是我特别想强调的一点:不要急着上工程脚手架,先把核心逻辑跑通。等后续真的需要扩展成定时推送、多城市监控甚至Web服务时,再把文件拆成request_handler.py、formatter.py、config.py也来得及,改动成本远低于一开始就铺一堆空目录。

3. 核心模块拆解:从输入城市名到打印天气详情,一条完整的请求链路

3.1 入口层:argparse如何把用户输入变成参数对象

命令行工具的入口通常从参数解析开始。这个项目选argparse而不手写sys.argv,是因为参数解析这件事比看起来要复杂:要区分位置参数和可选参数,要校验参数类型,要为用户输出帮助信息。argparse把这些都打包好了,而且只要几行配置就能生成清晰的使用提示。

def parse_args(): parser = argparse.ArgumentParser( description="天气查询小程序:输入城市名,输出实时天气" ) parser.add_argument( "city", help="目标城市名称,例如:A市" ) parser.add_argument( "--days", type=int, default=1, choices=[1, 3], help="预报天数,默认1天,可选1天或3天" ) parser.add_argument( "--verbose", action="store_true", help="打印接口原始返回的JSON,便于排查问题" ) return parser.parse_args()

这段代码有几个关键点。city是位置参数,用户必须提供,顺序也不能乱;--days是可选参数,用default=1保证不传时走默认值,用choices=[1, 3]限制取值范围,用户传2会被argparse直接拒绝并报错;--verbose是布尔开关,action="store_true"表示这个参数一旦出现就为True,不出现就是False。这样设计的好处是,参数校验的逻辑交给argparse,业务代码里不需要再写一堆if判断。

一个使用细节值得注意:可选参数和位置参数的顺序在大多数情况下可以互换,python weather.py A市 --days 3和python weather.py --days 3 A市效果一样,但建议把位置参数放在前面,读起来更自然,也避免某些老版本环境下解析异常。

3.2 请求层:用params传参避免中文城市名编码翻车

参数解析完成之后,核心逻辑进入请求层。这里做的事情就是向天气API发起HTTP GET请求,把返回的JSON数据转成Python字典。代码量不多,但每个细节都会影响成败。

def get_weather(city, days=1): """请求第三方天气接口,返回解析后的字典。""" url = "https://api.weather-service.example.com/v1/weather" params = { "key": API_KEY, "city": city, "days": days, "unit": "metric", "lang": "zh_cn", } resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() # 非200状态码直接抛异常 return resp.json() # 请求体转成Python字典

第一行url是服务商提供的接口地址,通常以/v1/weather结尾。params字典里的key是服务商分配的密钥,city是目标城市名,days是预报天数,unit="metric"表示用摄氏温度,lang="zh_cn"要求返回中文天气描述。这些参数名是不同天气服务商最常见的约定,实际使用时要对照自己申请的接口文档做调整。

requests.get(url, params=params, timeout=10)这行代码里,timeout=10是连超时时间都考虑进去了,单位是秒。加timeout是特别值得养成习惯的写法,否则网络异常时请求可能挂很久才报错,命令行工具给人的感觉就是“卡死了”。resp.raise_for_status()会在HTTP状态码不是200时直接抛出异常,比自己去判断resp.status_code == 200更简洁,也避免拿到错误页面后继续往下解析。resp.json()把JSON字符串解析成Python字典,后续代码直接按字典取数据。

这里不建议手拼URL的另一个原因是url的查询参数需要记住顺序,而params字典天然是键值对,少一个参数、多一个参数一眼就能看出来。把城市名放在params里,requests会自动处理编码,中文城市名不需要手动quote。

3.3 数据层:从JSON字典到格式化文本

请求返回的JSON结构通常是多层的,大致长这样:

{ "code": "200", "message": "ok", "data": { "city": "A市", "update_time": "2025-01-08 09:00", "today": { "weather": "晴", "temperature_min": 8, "temperature_max": 18, "wind": "东北风2级", "humidity": 40 } } }

格式化函数要做的事情就是把这棵JSON树里需要的字段抽出来,拼装成适合直接展示的文本:

def format_weather(data): if data.get("code") != "200": return f"查询失败:{data.get('message', '未知错误')}" info = data["data"] today = info["today"] text = ( f"城市:{info['city']}\n" f"更新:{info['update_time']}\n" f"天气:{today['weather']}\n" f"温度:{today['temperature_min']} ~ {today['temperature_max']}°C\n" f"风力:{today['wind']}\n" f"湿度:{today['humidity']}%\n" ) return text

data.get("code")用了字典的get方法而不是data["code"],区别在code字段缺失或异常时get返回None而不是抛KeyError,程序可以继续往下走。第一行做的是业务层面的判断:接口返回的code不是200,就把错误信息拼出来给用户看。这个判断很重要,因为网络请求能成功不代表数据查询成功,城市名输错时HTTP状态码依然可能是200,但业务code会提示查询失败。

接下来的解析是典型的字典嵌套取值。data["data"]拿到业务数据层,再data["today"]拿到当天的天气详情。f-string里直接嵌变量并做简单运算,比如把温度最小值最大值拼成“8 ~ 18°C”,比先拼接再替换变量的写法更直观。格式化部分只负责展示,不修改原始数据,这样verbose模式还能用原始JSON做调试。

说到这里,verbose开关的价值就体现出来了。格式化函数把它拼错了,对着终端输出很难定位是接口字段变了还是解析逻辑错了。加上--verbose直接打印原始JSON,一眼就能看出接口到底返回了什么字段,这是我在调试接口类小工具时最常用的手段。

3.4 流程编排:main函数为什么不写业务逻辑

最后是main函数和程序入口的写法:

def main(): args = parse_args() data = get_weather(args.city, args.days) if args.verbose: print(json.dumps(data, ensure_ascii=False, indent=2)) else: print(format_weather(data)) if __name__ == "__main__": main()

main函数的职责是编排而不是实现。它先把parse_args的结果存进args,然后调用get_weather拿数据,最后根据verbose开关决定打印原始JSON还是格式化文本。所有具体逻辑都已经下沉到前面三个函数里,main函数里没有任何一个if分支在做数据处理,这是模块边界清晰的标准姿势。

json.dumps(data, ensure_ascii=False, indent=2)这行值得单独解释。dumps把字典再转回JSON字符串,ensure_ascii=False保证中文原样输出而不是显示成\uXXXX的转义序列,indent=2让嵌套结构有缩进,调试时一眼能看清层级关系。很多人调试JSON习惯用print(data),打印出来的是一行长串,缩进一开立刻舒服很多。

最底部的if __name__ == "__main__"是Python的通用惯例,只有直接运行这个文件时才执行main(),被其他文件import时不会触发。这个写法让weather.py既可以当命令行工具跑,也可以被别的脚本import后调用get_weather()或format_weather()函数。对二次开发来说,这个入口写法直接决定了能不能轻松复用代码,算是为后续扩展留的后门。

4. 本地运行与部署细节:从pip安装到第一次查天气

4.1 环境准备与依赖安装

项目对Python版本的要求不高,Python 3.6以上基本都够用,因为代码里用到的argparse、json、datetime都是老牌标准库,requests也支持到很老的版本。如果你用的是系统自带Python,先检查一下版本:

python --version

确认版本没问题后,进入项目目录创建虚拟环境并安装依赖。这一步看似多余,但能避免requests装到系统全局环境里污染其他项目,是我每次拿到一个Python项目都会做的第一件事。

cd weather-project python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install -r requirements.txt

requirements.txt内容很简单,就一行requests。不写死版本号的好处是安装时拿最新稳定版,对这个小工具来说requests的API非常稳定,新版不会破坏现有代码。如果你的网络环境比较慢,可以在pip命令后面加-i参数切换到国内镜像源,安装速度会明显提升。

依赖装完后可以用pip show requests确认安装成功,看到版本和安装路径就说明环境OK了。

4.2 申请密钥并按环境变量配置

下一步是准备API Key。做法是先在第三方天气数据服务商的平台上注册账号,创建一个应用,拿到一串字符串形式的密钥。创建应用时通常会让你选订阅套餐,免费档对个人查天气完全够用。

拿到密钥后不要在weather.py里直接写死。代码里读取的是系统环境变量WEATHER_API_KEY,这样做的原因有两个:一是代码可能被分享出去,写死密钥等于把钥匙交给别人;二是环境变量可以按机器分别配置,换一台电脑不需要改代码。配置方式在Linux和macOS下是:

export WEATHER_API_KEY="你的密钥"

Windows下用的是set WEATHER_API_KEY=你的密钥。这个环境变量只在当前终端窗口生效,关掉终端就失效了,所以每次跑之前都要设置。如果嫌麻烦,也可以把export命令写进shell的配置文件里,让每次开终端自动生效。

配置完后运行python weather.py A市,如果输出里出现温度数据,说明密钥和环境变量都通了。如果报401或403错误,优先检查环境变量有没有在当前终端里设置成功,可以用echo $WEATHER_API_KEY验证。

4.3 三种调用方式与输出解读

环境都通了之后,日常使用有以下三种调用方式。

第一种是单城市实时查询,最简单:

python weather.py A市

输出大概长这样:

城市:A市 更新:2025-01-08 09:00 天气:晴 温度:8 ~ 18°C 风力:东北风2级 湿度:40%

这里有一个输出习惯可以直接抄走:把更新时间也打出来。天气数据的时效性很强,看到更新时间能判断手里的数据是刚拉的还是缓存了半天的。格式化函数里专门留了update_time字段就是为了这个。

第二种是3天预报,加一个参数就行:

python weather.py A市 --days 3

如果接口支持3天预报,返回的数据会变成一个数组,格式化函数也会相应变化。加这个参数的用处是给“周末去哪儿”这种场景留余地,后文二次开发部分还会用到。

第三种是调试模式,适合排查问题的时候用:

python weather.py A市 --verbose

这个模式把接口返回的原始JSON整个打印出来,字段名、数据类型、嵌套结构一目了然。当格式化输出跟预期不符时,先开verbose看原始数据,定位是接口字段变了还是解析代码写错了,十次有九次能直接找到原因。

首次运行如果所有输出都正常,建议把这几条命令按顺序过一遍作为验证模板。环境变量设置、参数传递、数据解析这三层各通一次,后续改动代码时再跑一遍同样的验证流程,改动是否引入问题就清楚了。

5. 避坑排查:天气小程序跑不通?这四类问题最常见

5.1 现象一:运行报ModuleNotFoundError,明明装了requests

症状是运行python weather.py A市时报错ModuleNotFoundError: No module named 'requests',但自己明明执行过pip install requests。这种情况在两种场景下最容易出现:一是系统里有多个Python版本,pip装到了Python 3.10,命令行却用的Python 3.8;二是用户没有激活虚拟环境,pip装进了venv,终端却还在用全局解释器。

排查思路是先确认两边的Python和pip是不是同一个。在终端里分别执行which python和which pip,看它们是否指向同一个解释器路径。更直接的办法是运行python -c "import requests; print(requests.__file__)",能打印出路径说明当前解释器能导入;如果报错,说明当前环境里确实没装。

解决办法很简单:确保当前终端激活了虚拟环境再执行pip install,或者用python -m pip install requests代替裸pip命令。python -m pip能保证安装的目标就是你当前这个python对应的环境,不会装错地方。从那以后我每次遇到ModuleNotFoundError,第一步都是跑python -m pip而不是裸pip。

5.2 现象二:HTTP 401或403,密钥看起来没问题

症状是请求返回401 Unauthorized或403 Forbidden,但自己确认密钥复制得很完整。最常见的原因有三个:环境变量没有真正传进代码进程,密钥本身已经过期或被服务商重置,以及请求参数里key的字段名跟接口文档不一致。

第一类原因最隐蔽。代码里用os.getenv("WEATHER_API_KEY")读取环境变量,如果代码是把它作为默认值写在函数签名里,比如def get_weather(city, days=1, api_key=os.getenv("WEATHER_API_KEY")),那函数定义时环境变量就已经被读走了,即使后面在终端export也不会变。解决方式是把读取环境变量的动作放进函数体内部,每次调用都重新读取。

第二类原因需要登录服务商控制台检查密钥状态,密钥有时会因为长时间未使用被自动回收。第三类原因对照接口文档就能发现,有些服务商用api_key,有些用key,还有的用Authorization请求头。排查时打开verbose模式看原始返回错误信息,里面通常会直接提示是鉴权失败还是参数错误。

5.3 现象三:中文全部变成乱码或问号

症状是城市名、天气描述在终端里显示成????????或者乱码。这个问题的根源在终端编码,不在代码逻辑。macOS和Linux的终端默认UTF-8编码,基本不会出问题;Windows的命令行窗口默认GBK编码,Python的print输出UTF-8字符时就容易显示异常。

解决方式有三个层级。最临时的是在运行命令前执行set PYTHONIOENCODING=utf-8,让Python强制以UTF-8输出;最一劳永逸的是把Windows终端的代码页切到UTF-8,在cmd里执行chcp 65001后再重启终端;如果要让代码适配到底,可以在代码入口处加上sys.stdout.reconfigure(encoding='utf-8'),从Python层面强制标准输出用UTF-8。

有一个容易误导人的点要提醒一下:请求返回的JSON里中文显示正常,不代表终端就一定是UTF-8。JSON解析得到的字符串在内存里一直是Unicode,跟终端编码没有关系,乱码只会在print真正把字符送到终端时才出现。所以看到乱码后,先检查print环节,不要再回头改请求层。

5.4 现象四:城市名查不到,提示city not found

症状是请求成功,HTTP状态码也是200,但返回的业务code提示城市不存在。这类问题在天气查询里非常典型。原因是第三方天气API的城市匹配规则各不相同,有的要求城市全名,有的只认城市代码,还有的要求带省份前缀才能区分同名城市。

排查方法先确认城市名写对了没有,比如县市级城市是否带了地级市前缀。然后测试英文名或城市代码是否能查到,如果英文名能查而中文不能,多半是接口的城市库对中文支持不全。最快的解法是换用服务商提供的城市ID,在它的官网或文档里搜索城市列表,拿到ID后把city参数改成数字ID。

这个问题的深层教训是:接口文档里的“支持中文城市名”往往只是一个宣传口径,实际覆盖的城市范围要看它的城市库更新情况。遇到查不到的冷门城市,我一般会先查一查城市列表,而不是反复试各种中文写法浪费时间。

6. 二次开发:把天气小程序扩展成定时推送与多城市监控工具

6.1 三个值得优先尝试的改造方向

天气小程序跑通之后,最值得动手的改造有三个方向。

第一个方向是定时查询。用Python标准库的time.sleep或操作系统的定时任务,让脚本每隔几小时自动查一次天气,输出追加到日志文件。这适合想要“早上自动收到天气提醒”的人,改动量最小,却能让工具从“被动查询”变成“主动推送”。

第二个方向是缓存优化。免费API通常有调用频率限制,连续查同一个城市的天气毫无意义,因为几小时内天气不会剧烈变化。可以在本地存一个cache.json,记录上次查询的城市和时间,两小时内查到相同城市直接返回缓存文本,不再发HTTP请求。

第三个方向是多城市对比。把要关注的城市写进一个列表,循环调用get_weather,输出一个多行的对比表格。这个功能对经常出差的人非常实用,一条命令就能知道多个城市的冷暖差异。

技术选型上不需要引入框架,优先用标准库解决。定时等待用time.sleep就够,缓存用json文件就可以,多城市就是一层for循环,这符合项目一贯的轻量风格。不要一上来就上celery、Redis,对个人工具来说都是过度设计。

6.2 落地示例:两小时自动查询并追加日志

以定时查询为例,改造思路是:在原项目里新增一个运行入口,循环里调用已有的get_weather和format_weather,把结果追加写入文本文件,然后sleep两小时。核心改动就是下面这一段:

import time from datetime import datetime def auto_report(city, interval=7200): """每隔interval秒查询一次天气,结果追加到日志文件。""" while True: data = get_weather(city, days=1) text = format_weather(data) stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open("weather.log", "a", encoding="utf-8") as f: f.write(f"\n[{stamp}]\n{text}\n") print(text) time.sleep(interval)

这段代码的关键点是循环里先做业务再sleep,保证第一次查询立刻发生,而不是先干等两小时。写入日志用"a"模式追加而不是"w"覆盖,这样多次查询会累积成一个完整的记录文件。time.sleep(interval)的单位是秒,7200秒正好两小时。

调用方式也很简单,在主程序入口加一个参数分支:

parser.add_argument("--auto", action="store_true", help="开启自动定时查询模式")

main函数里判断如果用户传了--auto,就进入auto_report循环而不是执行一次查询。这种改造让原项目完全保持向后兼容,不加--auto时行为跟原来一模一样,加上之后才启用新功能。

验证改动是否可靠有一个很实际的方法:把interval临时改成5秒,跑两分钟看日志文件里有没有按预期追加多条记录。验证通过后再把间隔改回7200秒,对外表现就是每两小时新增一条天气记录。

6.3 验证方法与工程习惯

二次开发最容易翻车的地方不是新功能写不出来,而是改完新功能把老功能弄坏了。所以每次改造后我都会执行一遍回归验证:先跑python weather.py A市确认单次查询没坏,再跑python weather.py A市 --days 3确认参数解析没坏,最后跑新加的--auto确认定时模式工作正常。这三条命令大约十秒钟,但能把回归风险压到最低。

还有一个值得养成的习惯是给改造后的代码加一个最小化测试。比如format_weather函数,可以直接在命令行里传一个写死的JSON字典进去打印输出,不发起真实HTTP请求,验证格式化逻辑。这样就把“依赖网络的接口测试”和“纯函数的逻辑测试”分开了,出问题时能快速定位是哪个环节的问题。

从那以后,我每次给这类小工具加新功能,都会强制走一遍“旧命令回归加新功能验证”的流程,哪怕只加了一行参数也要跑。别看这过程有点机械,它帮我拦住过不少低级错误,也让你对项目的每一次改动都有把握。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询