简介这是一份由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, paramsparams, timeout10)就完成同样的事。个人开发不是技术竞赛选更省心的库是明智的。尤其是在中文编码处理上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, typeint, default1, choices[1, 3], help预报天数默认1天可选1天或3天 ) parser.add_argument( --verbose, actionstore_true, help打印接口原始返回的JSON便于排查问题 ) return parser.parse_args()这段代码有几个关键点。city是位置参数用户必须提供顺序也不能乱--days是可选参数用default1保证不传时走默认值用choices[1, 3]限制取值范围用户传2会被argparse直接拒绝并报错--verbose是布尔开关actionstore_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, days1): 请求第三方天气接口返回解析后的字典。 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, paramsparams, timeout10) resp.raise_for_status() # 非200状态码直接抛异常 return resp.json() # 请求体转成Python字典第一行url是服务商提供的接口地址通常以/v1/weather结尾。params字典里的key是服务商分配的密钥city是目标城市名days是预报天数unitmetric表示用摄氏温度langzh_cn要求返回中文天气描述。这些参数名是不同天气服务商最常见的约定实际使用时要对照自己申请的接口文档做调整。requests.get(url, paramsparams, timeout10)这行代码里timeout10是连超时时间都考虑进去了单位是秒。加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 textdata.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_asciiFalse, indent2)) else: print(format_weather(data)) if __name__ __main__: main()main函数的职责是编排而不是实现。它先把parse_args的结果存进args然后调用get_weather拿数据最后根据verbose开关决定打印原始JSON还是格式化文本。所有具体逻辑都已经下沉到前面三个函数里main函数里没有任何一个if分支在做数据处理这是模块边界清晰的标准姿势。json.dumps(data, ensure_asciiFalse, indent2)这行值得单独解释。dumps把字典再转回JSON字符串ensure_asciiFalse保证中文原样输出而不是显示成\uXXXX的转义序列indent2让嵌套结构有缩进调试时一眼能看清层级关系。很多人调试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.txtrequirements.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, days1, api_keyos.getenv(WEATHER_API_KEY))那函数定义时环境变量就已经被读走了即使后面在终端export也不会变。解决方式是把读取环境变量的动作放进函数体内部每次调用都重新读取。第二类原因需要登录服务商控制台检查密钥状态密钥有时会因为长时间未使用被自动回收。第三类原因对照接口文档就能发现有些服务商用api_key有些用key还有的用Authorization请求头。排查时打开verbose模式看原始返回错误信息里面通常会直接提示是鉴权失败还是参数错误。5.3 现象三中文全部变成乱码或问号症状是城市名、天气描述在终端里显示成????????或者乱码。这个问题的根源在终端编码不在代码逻辑。macOS和Linux的终端默认UTF-8编码基本不会出问题Windows的命令行窗口默认GBK编码Python的print输出UTF-8字符时就容易显示异常。解决方式有三个层级。最临时的是在运行命令前执行set PYTHONIOENCODINGutf-8让Python强制以UTF-8输出最一劳永逸的是把Windows终端的代码页切到UTF-8在cmd里执行chcp 65001后再重启终端如果要让代码适配到底可以在代码入口处加上sys.stdout.reconfigure(encodingutf-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, interval7200): 每隔interval秒查询一次天气结果追加到日志文件。 while True: data get_weather(city, days1) text format_weather(data) stamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) with open(weather.log, a, encodingutf-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, actionstore_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请求验证格式化逻辑。这样就把“依赖网络的接口测试”和“纯函数的逻辑测试”分开了出问题时能快速定位是哪个环节的问题。从那以后我每次给这类小工具加新功能都会强制走一遍“旧命令回归加新功能验证”的流程哪怕只加了一行参数也要跑。别看这过程有点机械它帮我拦住过不少低级错误也让你对项目的每一次改动都有把握。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?