最近在准备AI课程的练习素材,一直想找一个既能讲清楚Python基础语法、又能让学生一眼看到有趣结果的例子。前后试过猜数字、爬虫抓标题,效果都一般。最后定了这个方案:输入你的名字和城市,程序自动打印一段彩色个性化问候,顺便把当地实时天气也带上,整个代码不到一百行,却把输入输出、函数、字典映射、异常处理、第三方库、API调用全串起来了。
这个项目挂在AI课程的“综合练习”阶段超级合适。学生写完代码,运行起来看到自己名字以彩色出现,还有实时天气,那种“这玩意真的在我电脑上跑起来了”的兴奋感,比刷二十道语法题都管用。对自学Python的人来说,这也是一个很好的“第一个有点用的程序”模板——你能看懂它、改它、塞进自己的工具箱。
下面我把整个项目的设计思路、代码拆解、踩坑记录全部写出来,想自用还是想拿去当教学案例都行。
1. 项目背景与设计思路
1.1 为什么选“名字+城市+天气”这个组合
AI课程里给学生布置综合练习题,我一直坚持一个原则:要能立刻看到反馈,要和真实世界有关系。打印九九乘法表符合第一条,不符合第二条;爬一个新闻标题符合第二条,但反爬、解析、乱码一堆破事,新手很容易卡住。我需要一个“看起来不难,但五脏俱全”的项目。
“名字+城市+天气”赢在哪?输入只有两个普通的信息,输出却是一段带着温度的问候语。学生输入自己的名字,看到程序叫自己“林晓”,再输入城市,看到上海现在的气温,这种“程序认识我、程序知道世界”的体验,是极好的激励。而且它天生带扩展空间:你可以加穿衣建议,可以加多个城市对比,可以给不同天气配不同颜色,后面想往哪发展都顺。
另一个现实原因是成本。免费且不用注册的天气接口有,调用学习成本极低,不用担心学生人手一个API key造成滥用。这个项目把“别人提供的服务怎么集成进来”这件事讲清楚了,而这恰恰是AI课程里非常重要的一个素养——不是所有功能都要自己造轮子。
1.2 技术选型:天气数据从哪来
天气数据的来源是整个项目的核心决策。当时我列了个对比清单:
| 方案 | 是否需要Key | 是否需要注册 | 返回中文 | 新手友好度 |
|---|---|---|---|---|
| Open-Meteo | 不需要 | 不需要 | 支持 | 高 |
| 高德开放平台 | 需要 | 需要 | 支持 | 中 |
| 和风天气 | 需要 | 需要 | 支持 | 中 |
| 心知天气 | 需要 | 需要 | 支持 | 中 |
如果有人问我为什么不选国内平台,我会说:不是不好,而是课程场景下每个学生都去注册一个key,排队、审核、限额,一堂课半小时就耗完了。Open-Meteo不需要任何认证,拿到经纬度就能查天气,全球范围都覆盖,而且支持中文城市名解析,刚合适。
这里要拆分两个接口。Open-Meteo的天气查询接收的是经纬度坐标,不是城市名,所以要先通过它的地理编码接口把“上海”变成“纬度31.23,经度121.47”,再用这个坐标去拿天气数据。逻辑上就像你先查邮编再寄信,两个动作分开。代码里体现为两个函数,职责清晰,也方便以后替换成其他服务。
1.3 彩色输出:终端也能有视觉温度
为什么非得搞彩色输出?说实话,最初版没有色彩,纯黑底白字的问候语,学生看完反应平淡。加上颜色之后,晴天是亮黄色,雨天是蓝色,雪天是青色,整个终端像一个迷你天气仪表盘,观感完全不同。这个“视觉反馈”的成本极低,但带来的体验提升非常大。
原理不复杂。终端里有一种叫ANSI转义序列的东西,本质是往标准输出里塞一串特殊字符,比如\033[31m表示红色,后面的文字都会变红,直到遇到\033[0m复位。Python的print天生支持输出这些字符,不需要额外库。但Windows老版本终端不支持,所以我在Windows下调用colorama.init()做兼容,它会把ANSI转义序列自动翻译成Windows API调用。注意,这个库只在你用Windows而且遇到颜色显示异常时才必须,其他场景可有可无。
2. 核心环节拆解与代码落地
2.1 城市名转坐标:一条隐藏的分支
前面提到天气接口只要经纬度,所以第一步是“地点解析”。Open-Meteo的地理解析接口长这样:
url = "https://geocoding-api.open-meteo.com/v1/search" params = { "name": city_name, "count": 1, "language": "zh", "format": "json" } resp = requests.get(url, params=params, timeout=10) data = resp.json()返回结果里results[0]就是最匹配的城市,里面包含latitude和longitude。这里有个细节,count=1只取最匹配的一条,是因为很多城市存在重名,比如“中山”在广东和台湾都有,取第一条至少保证程序不崩。如果你教学生,可以用个循环把results里的所有候选城市都打印出来,让用户自己选,这是很好的交互扩展点。
实际跑的时候有个坑:中文城市名有时能解析出来,有时解析不到。因为地理编码服务对中文的支持并不完美,这时候我建议学生在自己的代码里加一个“别名映射表”,最常用的是把拼音也接上。比如:
CITY_ALIAS = { "上海": "shanghai", "北京": "beijing", "广州": "guangzhou", }查不到中文名就自动换上拼音再查一次。这个兜底逻辑在课程里可以单独讲一遍,因为它就是“容错设计”的启蒙案例。
2.2 天气码与中文描述的映射
拿到坐标后调用天气接口:
url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": lat, "longitude": lon, "current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m", "timezone": "Asia/Shanghai" }返回的current里有一个weather_code,这是WMO(世界气象组织)定义的天气现象代码,纯数字。我需要把代码翻译成中文。这个翻译用字典搞定,是Python里最直观的映射方式:
| 天气码 | 含义 | 推荐颜色 |
|---|---|---|
| 0、1 | 晴、以晴为主 | 黄 |
| 2、3 | 多云、阴 | 白 |
| 45、48 | 雾、冻雾 | 白 |
| 51、53、55 | 毛毛雨系列 | 蓝 |
| 61、63、65 | 小雨、中雨、大雨 | 蓝 |
| 71、73、75 | 小雪、中雪、大雪 | 青 |
| 80、81、82 | 阵雨系列 | 蓝 |
| 95 | 雷暴 | 蓝 |
这个表直接放在代码里就是一个全局字典WEATHER_CODE_MAP。天气码一来,中文描述和颜色一起确定。我特意把颜色选择也做成一个函数,对着天气码给出颜色名,这样以后想扩展比如“雷暴显示红色”就只改一处。
2.3 问候语生成:按时间和温度调整
问候语不能写死,否则每个人看到的都一样,这就失去“个性化”的意义了。我的做法分两层:第一层看时间,第二层看气温。
时间判断用datetime.now().hour,分成几个区间:深夜、清晨、上午、中午、下午、晚上。不同区间用不同的开头词,“早上好”“下午好”这种。第二层看温度,给一条穿衣建议,比如超过30度提醒防暑,低于0度提醒保暖。这两层信息拼起来,就成了一个看着很“懂你”的问候:
下午好,林晓!你在上海,当前天气小雨,气温18℃,相对湿度82%,风速11km/h。温度有点凉,记得加件外套。愿你今天心情也像天气一样清爽~这里的语气词“愿你今天心情也像天气一样清爽”是我故意留的,它让程序不再像机械报表,更像一个真人朋友在说话。如果你想让学生改动,可以让他们自己写一句结尾祝福,看看谁的文案更有意思,课堂气氛一下就活了。
3. 完整实现与运行效果
3.1 环境准备与依赖安装
拿这个项目做教学,环境准备越简单越好。我推荐Windows笔记本上用VSCode,Python3.8以上都行,不需要虚拟环境,直接装两个包:
pip install requests pip install coloramarequests是HTTP客户端,封装了所有网络请求细节;colorama只在Windows下处理ANSI转义序列兼容,Linux和macOS终端原生支持,不装也能跑。如果你用PyCharm,安装路径在Settings里找Project Interpreter,点加号搜索就行。
为什么要单独讲环境?因为AI课程里十个学生有八个第一次跑命令行程序,最容易反馈“我运行了但没反应”“颜色没出来”。提前把终端切到支持UTF-8这一步做好,后面真正写逻辑时就能少打断课堂节奏。
3.2 完整代码(可以直接抄作业)
下面是我实际在用的版本,注释我尽量留得清楚:
import sys import requests from datetime import datetime COLOR_MAP = { "red": "\033[31m", "green": "\033[32m", "yellow": "\033[33m", "blue": "\033[34m", "magenta": "\033[35m", "cyan": "\033[36m", "white": "\033[37m", "bold": "\033[1m", "reset": "\033[0m" } def color_print(text, color="white"): if color in COLOR_MAP: start = COLOR_MAP[color] end = COLOR_MAP["reset"] print(f"{start}{text}{end}") else: print(text) WEATHER_CODE_MAP = { 0: "晴", 1: "以晴为主", 2: "多云", 3: "阴", 45: "雾", 48: "冻雾", 51: "小毛毛雨", 53: "毛毛雨", 55: "大毛毛雨", 61: "小雨", 63: "中雨", 65: "大雨", 71: "小雪", 73: "中雪", 75: "大雪", 80: "小阵雨", 81: "阵雨", 82: "强阵雨", 95: "雷暴" } CITY_ALIAS = { "北京": "beijing", "上海": "shanghai", "广州": "guangzhou", "深圳": "shenzhen" } def get_coordinates(city_name): if city_name in CITY_ALIAS: city_name = CITY_ALIAS[city_name] url = "https://geocoding-api.open-meteo.com/v1/search" params = {"name": city_name, "count": 1, "language": "zh", "format": "json"} resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() data = resp.json() if not data.get("results"): raise ValueError(f"未找到城市:{city_name}") item = data["results"][0] return item["latitude"], item["longitude"], item.get("name", city_name) def get_weather(lat, lon): url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": lat, "longitude": lon, "current": "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m", "timezone": "Asia/Shanghai" } resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json()["current"] def weather_color(weather_code): if weather_code in (0, 1): return "yellow" if weather_code in (2, 3, 45, 48): return "white" if weather_code in (51, 53, 55, 61, 63, 65, 80, 81, 82, 95): return "blue" if weather_code in (71, 73, 75): return "cyan" return "green" def build_greeting(name, city, weather_info, weather_text): hour = datetime.now().hour if hour < 6: time_word = "夜深了" elif hour < 9: time_word = "早上好" elif hour < 12: time_word = "上午好" elif hour < 14: time_word = "中午好" elif hour < 18: time_word = "下午好" else: time_word = "晚上好" temp = weather_info["temperature_2m"] humidity = weather_info["relative_humidity_2m"] wind = weather_info["wind_speed_10m"] if temp > 30: advice = "热得够呛,注意防暑补水" elif temp > 20: advice = "体感舒适,适合出门走走" elif temp > 10: advice = "温度有点凉,记得加件外套" elif temp > 0: advice = "比较冷,注意保暖" else: advice = "天寒地冻,围巾手套安排上" return (f"{time_word},{name}!你在{city},当前天气{weather_text}," f"气温{temp}℃,相对湿度{humidity}%,风速{wind}km/h。{advice}。" f"愿你今天心情也像天气一样清爽~") def main(): if sys.platform.startswith("win"): import colorama colorama.init() name = input("请输入你的名字:").strip() city = input("请输入所在城市:").strip() if not name: name = "朋友" if not city: city = "北京" try: lat, lon, city_std = get_coordinates(city) weather_info = get_weather(lat, lon) code = weather_info["weather_code"] weather_text = WEATHER_CODE_MAP.get(code, "未知天气") color = weather_color(code) msg = build_greeting(name, city_std, weather_info, weather_text) color_print(msg, color) color_print("=" * 40, "magenta") color_print(f"数据来源:Open-Meteo | 当前时间 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "white") except requests.exceptions.RequestException as e: color_print(f"网络请求失败:{e}", "red") except ValueError as e: color_print(f"输入异常:{e}", "red") except Exception as e: color_print(f"运行出错:{e}", "red") if __name__ == "__main__": main()说几个我测试过的细节。build_greeting里的f-string跨行拼接,我用了括号包住,这样代码不会超过80个字符,阅读起来舒服。CITY_ALIAS里的兜底逻辑在上文讲过,它解决的是中文名解析率不高的问题,如果你想把代码精简掉,也可以删掉这个字典,直接全部传中文。
3.3 运行效果与参数调整
实际运行长这样(我输入“林晓”和“上海”):
请输入你的名字:林晓 请输入所在城市:上海 下午好,林晓!你在上海,当前天气小雨,气温18℃,相对湿度82%,风速11km/h。温度有点凉,记得加件外套。愿你今天心情也像天气一样清爽~ ======================================== 数据来源:Open-Meteo | 当前时间 2025-06-24 15:30:05第一行问候语的字体是蓝色的,因为天气码对应了“小雨”,系统自动选了blue。分割线是品红色,信息行是白色。整体视觉效果,一句话就是“终端的仪式感”。
调整也很容易。想换颜色,改COLOR_MAP里对应颜色的ANSI转义值就行,比如红色\033[31m改成粉色,你查一下对应代码填进去即可。想改文案风格,直接动build_greeting里的字符串。想加“风力”判断,把wind_speed_10m拿来做if判断,再拼一段文案,很顺手。
4. 常见问题排查与避坑经验
4.1 Windows终端中文乱码
这个坑太常见了,几乎每届学生都会踩。Windows默认控制台代码页经常是GBK,我print出来带中文的字符串就会出现乱码。我自己跑教学班的时候,最直接的办法是让同学在VSCode里运行,把终端编码设为UTF-8;或者打开CMD后先执行chcp 65001切到UTF-8代码页。
Python3本身的源码是UTF-8编码,问题主要出在控制台显示环节。如果你用的是命令行跑脚本,chcp 65001是最省事的。要是还是乱码,检查一下系统区域的“Beta版使用Unicode UTF-8提供全球语言支持”这个选项,勾上重启,绝大多数情况都好了。
4.2 颜色不生效或出现大量转义字符
如果你看到输出是一堆\033[31m这样的字符,而不是颜色,说明当前终端不支持ANSI转义序列。Windows老终端(尤其是Cmd的旧版)就是这样。解决方式就是让colorama.init()生效,这个库在程序启动时把终端切到“支持ANSI”模式。加了之后还不行,就换Windows Terminal,新终端默认支持。
还有一种情况是你在IDE的Output面板里运行,有些IDE的日志窗口不解析ANSI。这种情况下颜色输出不了但也不会出现乱码的转义字符,属于预期行为。教学时我会提醒学生:想要最佳效果,请在真正的终端里运行,比如VSCode的集成终端。
4.3 找不到城市或解析到错误城市
刚才代码里抛了ValueError("未找到城市"),这是兜底。但更气人的是解析到错误城市,比如你输入“中山”,结果匹配到别的国家的中山。Open-Meteo的geocoding接口有一个参数countryCode,你可以限定国家,比如中国是CN,能减少误匹配。我不建议新手加太多参数,但可以在课程扩展环节提一下。
最稳妥的方案是你在代码里维护一个小字典,把你需要的城市写成“中文名-经纬度”的键值对。虽说写死了不够优雅,但在演示场景里是最稳定、最不依赖外部服务的做法。
4.4 请求超时和免费接口的限制
Open-Meteo虽然是免费接口,但也不是无限量给你刷的。如果你连续高频请求,服务会拒绝你。我在代码里给每个请求都加timeout=10,就是避免某个请求卡死时整个程序挂着不动。这个方法虽然简单,但能救场。
如果哪天接口真的不可用了,代码里的异常捕获会把“网络请求失败”打出来,学生至少知道不是自己语法写错了。至于要不要换备用接口,课程场景下我一般不展开,重点是把“外部服务可能挂,程序要想好退路”这个意识传递到位。
4.5 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 中文输出乱码 | 控制台代码页不对 | CMD执行chcp 65001,或VSCode终端用UTF-8 |
| 颜色没出来 | 终端不支持ANSI | 加colorama.init(),换Windows Terminal |
| 出现\033转义字符 | 同上 | 同上 |
| 城市找不到 | 中文名解析失败 | 改用CITY_ALIAS拼音兜底,或维护经纬度字典 |
| 程序卡住不动 | 网络请求无响应 | 检查网络,代码里加timeout参数 |
| 天气数据是“未知天气” | 新天气码没映射 | 打印weather_code,对照WMO表补进字典 |
5. 从这个小工具到AI课程的思维跃迁
5.1 可以往哪些方向扩展
这个小程序完全具备“长成大树”的潜质。我给课程设计的扩展路径有几条,你可以按学生的兴趣挑:
第一,命令行参数化。把input()改成用argparse接收命令行参数,运行方式变成python weather.py 林晓 上海,听起来就专业不少。第二,多城市对比。把城市列表传进去,循环输出好几个城市的天气卡片,用分隔线隔开,很有仪表盘的感觉。第三,写入Excel。把每天的数据追加到openpyxl工作表里,累积一周就能画气温折线图,这就自然衔接到了数据分析。
更贴近AI课程主题的扩展,是让大模型来生成问候语。你可以保留“获取天气”这个后端逻辑,把天气数据拼成一段Prompt,丢给大模型生成更自然、更有梗的问候文案。学生看到“AI生成”和“规则生成”两种效果对比,对这个项目的理解会立刻上一个层次。
5.2 教学场景中的使用建议
作为AI课程的教练项目,我建议分“三步走”来带学生。第一步先让他们把程序原样跑起来,什么都别改,体验完整的“输入-处理-输出”流程。第二步让他们改颜色和问候语,这能加深对字典、字符串拼接和函数返回值的理解。第三步再引入一个没有被映射的天气码,让学生自己查WMO表、加进字典,这一步考验的是“看懂代码以后动手改”的能力。
课时紧张的话,第二步和第三步可以合并,让每个学生做一个“只属于自己版本”的问候程序,分享到群里互相看看效果。这种即时反馈带来的成就感,比任何测试数据都能说明问题。
最后分享一个我自己的小体会:这类综合小项目,最重要的价值不在于代码量有多大,而在于它把一个真实场景的完整链路压缩到了新手能消化的范围内。学生在它身上练熟的不只是语法,还有“拆解需求、调用服务、处理异常、迭代优化”这套做事的思维。我的建议是,把这个程序反复改三遍以上。第一遍跑通,第二遍加功能,第三遍精简代码。三次下来,Python基础基本就活了。