Python与Flask实现微信公众号每日天气推送:从测试号到上线全指南

发布时间:2026/10/15 15:39:59
Python与Flask实现微信公众号每日天气推送:从测试号到上线全指南
简介这是一份教你搭建微信公众号每日自动推送天气与纪念日通知的教程资源包解决“想表达心意但不懂自动化部署”的痛点适合想给伴侣或家人制造固定小惊喜、且具备少量编程基础的入门读者。包内共5个文件以Python脚本实现天气与纪念日信息的组装与发送txt文件用于填写API密钥、整理依赖清单yml文件配置GitHub Actions定时任务md文档撰写完整部署说明压缩包整体仅5KB非常轻量。已有7692人学习/下载多版本下载验证了该方案的可行性与实用性。拿到后可按说明申请天行与和风天气API填入配置并推到GitHub即可每日自动运行无需自备服务器或手动触发源码结构、消息模板和定时工作流均已就绪还可用同一套逻辑扩展星座运势等消息适合想快速复刻并微调内容的读者。1. 微信公众号每日天气推送这不是学编程是一次情感工程的交付有一种需求看起来是技术题本质上是产品题。女朋友说“今天好冷”的时候你补发的天气提醒已经晚了真正该做的是在每天早上七点半让一条带着今日温度、穿衣建议和降雨概率的消息准时出现在她的微信对话框里。这也是很多非计算机专业的人第一次想写代码的真实动机。但问题在于网上搜“微信公众号给女朋友推送天气”要么是过期的接口文档要么是只讲了注册流程就断掉的半篇教程照着做根本跑不通。这篇方案解决的是从零到上线的一整条链路注册测试号、获取天气数据、写推送服务、挂定时任务、处理掉线断电。技术栈锁定 Python 加 Flask天气源用高德开放接口公众号侧走模板消息。整个过程可以在一台普通电脑上跑通不必买服务器。适合三类人想学 Python 入门但缺一个真实项目的在校生想做个自动化工具有点仪式感的开发者以及任何需要向别人证明“我能把一个需求完整落地”的人。接下来按一个带过项目的工程师的思路把每一步怎么设计、怎么实现、怎么验收讲透。2. 技术选型思路为什么不用企业公众号也不用推送 API做这类私人天气提醒第一个误区是纠结“要不要注册一个企业主体公众号”。个人订阅号虽然也能发消息但接口权限里没有模板消息只能走客服消息而客服消息要求用户在 48 小时内有互动否则推不出去。真正常用的做法是申请一个“公众号测试号”它不需要企业资质申请完就有完整的模板消息接口权限AppID 和 AppSecret 都是真实可用的唯一的限制是关注者数量有上限但这正好符合单用户推送的场景。第二个关键选择是天气数据源。和风天气和心知天气都需要先申请 API key虽然有免费额度但注册流程和实名验证会比较耗时。相比之下高德开放平台的天气查询接口直接提供免 key 的 Web 服务只需在控制台创建一个应用并获取 key就能按城市编码拿到当天和未来四天的天气数据。返回结果是 JSON解析非常方便。这里需要留意的是高德接口返回的天气描述比较简要只有“晴”“多云”这类文本没有湿度、风力、空气质量指数。所以设计时要把推送文案的侧重点放在温度曲线、温差提醒、降雨概率上而不是强求体感温度。第三个决定是推送方式。微信模板消息的字段是固定的你只能替换模板里预先定义的变量不能自由写正文。这意味着项目里需要准备多套模板一套用于日常天气播报另一套用于特殊天气预警比如降雨、大风。推送逻辑要根据天气数据做判断决定走哪套模板。框架选型坚持用 Flask 而不上 Django是因为这个项目只有一个回调路由和一个定时任务Flask 足够轻、足够容易理解对第一次接触 Web 开发的人也更好调试。提示不要用第三方“微信推送平台”或各类通知聚合服务。那些服务虽然一条 API 就能推送免费消息但数据流经过第三方消息内容和送达时间都不可控而且存在隐私风险。自己拉起一个最小服务接口逻辑完全在手上出了问题每一条日志都能查这才是合格的工程习惯。3. 总体架构与关键数据设计三层结构与两张业务表3.1 整体链路与模块划分系统由三个核心模块组成天气数据采集模块、消息渲染模块、公众号接口交互模块。天气模块负责按城市编码请求高德接口解析当天的温度和天气现象并输出结构化数据消息渲染模块把天气数据按照配置好的推送模板填充成一段自然语言文案并且加入穿衣建议公众号交互模块负责获取 access_token、接收用户消息并记录 openid、调用模板消息接口完成最终推送。模块之间用函数解耦不引入消息队列。定时任务调度器APScheduler每天在设定的时间点调用天气模块和公众号模块整个流程是同步串行的拉天气、拼文案、调接口、写日志。这样的设计看起来不够“分布式”但它有几个实际好处一是失败点少任何一步出错日志里一眼可见二是便于单测每个模块可以独立传入假数据验证三是不需要额外的服务部署直接 Python 环境就能托管全流程。3.2 用户与配置数据表设计虽然项目只服务一个接收者但代码仍建议按两张数据表来组织因为好的结构才能应对以后的扩展。第一张是 user 表存储接收者的 openid、昵称、关注状态、所在城市编码、期望推送时间、是否启用推送。第二张是 push_log 表按天记录每次推送的结果推送日期、使用的模板 ID、发送状态、失败原因、天气数据快照、耗时。字段类型上openid 长度设为 64因为微信返回的 openid 是 28 到 32 位字符串城市编码用字符串型而不是整型因为高德的城市编码是 6 位字符串首位的 0 会被整型吞掉这是一个非常容易忽略的坑。推送状态用 tinyint 表示1 为成功0 为失败2 为用户未关注不采用枚举避免后续扩展时改表结构。3.3 关键接口函数出参设计天气模块的主函数 get_weather(city_code) 返回一个字典包含六个键today_weather 表示今天的天气现象temp_high 为最高温temp_low 为最低温precipitation 是降雨概率高德接口不直接给百分比所以由天气现象推导wind_direction 和 wind_level 是风向与风级。消息渲染模块的 render_message(weather, user_config) 接收天气字典和用户配置返回推送给微信的模板字段结构其中 keyword1 到 keyword5 对应模板里定义的变量名。模块交互过程强调的是单入口原则所有对外部 API 的请求都收敛到单独的 client 模块中业务层不直接调用 requests 库。这样带来的工程收益是后续想加缓存、加重试、加代理只需要改 client 模块业务代码完全不受影响。这也降低了日志追踪的成本——每个外部请求都有统一的出入参记录。4. 核心模块实现从申请测试号到第一条消息送达4.1 微信公众号测试号注册与基本环境配置申请测试号不需要企业资质直接访问微信公众平台的测试号页面用个人微信扫码即可。登录后能看到两件关键信息测试号 appID 和 appsecret这两个值在后续的配置文件中都会用到。页面上还有一个“模板消息”区域点击“新增测试模板”可以自定义模板内容需要选择一个模板行业然后填写模板标题和字段。实际项目中我建议配置两组模板一组叫“每日天气提醒”包含天气现象、温度范围、风向风力和温馨提示四个字段另一组叫“天气预警通知”内容侧重非正常天气的特别提醒。完成申请后先做一件事把目标微信号关注到测试号上。扫码关注后在测试号管理页面能看到该用户的 openid这个 openid 就是消息推送的目标地址。要注意同一个微信在测试号下的 openid 和企业号、订阅号下都不相同属于测试号专属不能想当然地沿用其他场景里的值。环境配置方面需要安装 Python 3.8 以上版本并创建独立的虚拟环境。然后在虚拟环境下安装 Flask、requests、APScheduler、python-dotenv。依赖版本并不严格只要保证是近几年内的稳定版本即可但建议把依赖写入 requirements.txt 方便复现环境。4.2 天气数据获取与字段处理承接高德 API 的原始返回高德天气接口的请求方式是 GET参数包含 key 和 citycity 填城市编码。以北京为例编码 110000。请求地址是https://restapi.amap.com/v3/weather/weatherInfo设置extensionsall可以拿到未来四天的预报数据。实现模块时要注意响应体里 lives 和 forecasts 两个字段在整天和一个星期的不同层级用错了层级会导致 KeyError。先看天气模块的具体实现代码做了三件事请求数据、容错处理、结构转换。import requests from typing import Dict, Optional def get_weather(city_code: str, amap_key: str) - Optional[Dict]: 获取指定城市的实时天气与今日预报。 参数: city_code: 高德城市编码例如 110000 表示北京 amap_key: 高德开放平台应用的 Key 返回: 结构化的天气字典请求失败时返回 None由调用方决定是否重试 url https://restapi.amap.com/v3/weather/weatherInfo params { key: amap_key, city: city_code, extensions: all } try: resp requests.get(url, paramsparams, timeout5) resp.raise_for_status() data resp.json() except (requests.RequestException, ValueError) as e: # 记录异常便于在定时任务日志中定位网络或超时问题 print(f[天气模块] 请求高德接口失败: {e}) return None if data.get(status) ! 1: print(f[天气模块] 高德接口返回异常: {data.get(info)}) return None try: forecast data[forecasts][0][casts] today forecast[0] weather_info { weather: today[dayweather], temp_high: int(today[temp_day]), # 白天最高温整型便于比较 temp_low: int(today[temp_night]), # 夜间最低温 wind_dir: today[daywind], wind_level: today[daypower], } # 根据天气现象推导降雨概率属于业务规则而非接口字段 weather_info[precipitation] infer_rain_probability(weather_info[weather]) return weather_info except (KeyError, IndexError) as e: print(f[天气模块] 解析天气数据异常: {e}) return None def infer_rain_probability(weather_text: str) - int: 根据天气现象文本粗略推断降雨概率用于文案中提示带伞。 if 雨 in weather_text: return 85 if 云 in weather_text: return 30 return 10这段实现的细节上有几个值得说明的地方。第一使用resp.raise_for_status()会把 HTTP 层面的 4xx、5xx 异常显式抛出来配合try的捕获避免静默失败。第二高德接口返回的温度是字符串这里用int()做了类型转换因为后续模板消息里要计算温差并填入“较昨日升高3度”这类文本字符串拼接容易出错。第三降雨概率不是接口字段是根据天气现象词包推断的归属于业务规则单独抽成函数方便以后接入其他数据源时替换逻辑。4.3 access_token 管理与模板消息发送绕开微信接口的时效性限制微信公众号接口的 access_token 有效期是 7200 秒且每天调用次数有限最忌讳每次发消息前都去请求一次。标准做法是把 token 缓存到本地文件带过期时间判断。下面代码里实现了获取、缓存和失效处理的全套逻辑。import json import os import requests import time TOKEN_FILE access_token.json BASE_URL https://api.weixin.qq.com/cgi-bin def get_access_token(app_id: str, app_secret: str) - str: 读取本地缓存的 access_token不存在或已过期则重新请求。 token_data load_token_cache() if token_data and token_data[expires_at] time.time() 200: # 预留200秒缓冲防止并发时token刚好过期 return token_data[access_token] url f{BASE_URL}/token params { grant_type: client_credential, appid: app_id, secret: app_secret, } resp requests.get(url, paramsparams, timeout5) resp.raise_for_status() data resp.json() if access_token not in data: raise RuntimeError(f获取access_token失败: {data}) save_token_cache(data[access_token]) return data[access_token] def save_token_cache(token: str) - None: 把token写入本地文件设置过期时间为当前时间 7000秒。 之所以不是7200秒是因为文件读写有微小延迟网络不稳时也可能导致请求到达微信服务器时token已经过期。 expires_at time.time() 7000 cache {access_token: token, expires_at: expires_at} with open(TOKEN_FILE, w, encodingutf-8) as f: json.dump(cache, f)token 缓存到本地文件是单机部署下的最优解。如果以后迁移到云函数或多实例环境则需要把存储位置换到 Redis并加上分布式锁。现在这个方案把过期余量设为 200 秒而不是 0 秒背后是一个常见翻车点到期时间刚好卡在临界点请求在网络上传输用了 3 秒到达微信时已经失效。加上缓冲后即使网络延迟较大也能保证请求成功。若 token 获取失败函数抛异常而不是返回空字符串是为了让调用方感知到服务不可用避免后续模板消息拿着空 token 去请求。模板消息的发送接口是message/template/sendPOST 一个 JSON 对象。其中 touser 填接收者的 openidtemplate_id 填测试号后台创建的模板 IDdata 字段里的每个 key 对应模板内容里的变量名。这里有一个容易忽略的约束模板消息的 data 字段中变量值不能为空字符串且总长度有上限。把天气数据填入模板时要对字段值做空值兜底处理。def send_template_message(access_token: str, openid: str, template_id: str, data: dict) - bool: 发送模板消息。 参数: access_token: 已缓存的access_token openid: 接收者的openid template_id: 测试号里创建的模板ID data: 形如 {keyword1: {value: 晴}, ...} 的结构 返回: True表示发送成功False表示发送失败(如用户未关注) url f{BASE_URL}/message/template/send headers {Content-Type: application/json} payload { touser: openid, template_id: template_id, data: data, } resp requests.post(url, params{access_token: access_token}, jsonpayload, headersheaders, timeout5) result resp.json() if result.get(errcode) 0: return True # 错误码43004表示用户未关注这是最常见的失败原因 print(f[推送模块] 模板消息发送失败: {result}) if result.get(errcode) in (43004, 40003): return False return False发送函数之所以设计成返回布尔值是为了让上层定时任务能基于返回值更新 push_log 表的状态。43004 这类用户未关注的错误通常在部署初期出现——你把 openid 写在配置文件里但接收者最后取消了关注代码还在往旧 openid 推送。工程化的做法是在每天的定时任务里把这一个失败结果落库连续失败 3 天就触发一个“疑似取关”的告警提示。4.4 定时任务与消息文案组装在合理的时间做合理的事消息推送时间的设定要考虑接收方的生活节奏。工作日的 07:30 是相对稳妥的选择周末再晚半小时避免吵醒睡懒觉的人。定时任务用 APScheduler 的 BlockingScheduler 实现搭配 cron 触发器。在 Flask 应用初始化时启动调度器但注意不要把调度器放进 Flask 的 debug 模式里否则代码改动触发重载时会重复启动两个调度器导致一天收到两条推送。再来看消息文案的组装。模板消息不能自由排版但可以通过换行符和字符来优化视觉结构。以下代码展示了文案拼接逻辑核心思想是“结构化但不死板”。def build_weather_message(weather: dict) - dict: 根据天气字典生成模板消息的data字段。 模板示例: {{keyword1.DATA}} # 天气现象 {{keyword2.DATA}} # 温度范围 {{keyword3.DATA}} # 风力风向 {{keyword4.DATA}} # 温馨提示 temp_range f{weather[temp_low]}℃ ~ {weather[temp_high]}℃ wind_info f{weather[wind_dir]}风 {weather[wind_level]}级 # 根据温度和降水概率生成差异化提示 if weather[precipitation] 70: tip 今天大概率有雨出门记得带伞别嫌麻烦。 elif weather[temp_high] 33: tip 气温偏高注意防暑多补充水分别硬扛。 elif weather[temp_low] 5: tip 低温天气多穿一件外套早晚尤其注意保暖。 else: tip 天气不错适合出门走走注意早晚温差。 return { keyword1: {value: weather[weather]}, keyword2: {value: temp_range}, keyword3: {value: wind_info}, keyword4: {value: tip}, }这段代码里的推荐规则并不复杂但它是方案里“像产品”的部分。很多人把推送做成一句冷冰冰的“晴 10℃-20℃”接收者不觉得被关心。加了差异化的温馨提示后同样的天气数据读起来就有了人的味道。差异化提示的判断顺序也有讲究降雨最高优先级其次高温再次低温避免多个条件叠加时提示语出现矛盾这个顺序可以记录在代码注释里方便后续维护。5. 部署上线与日常运维常见问题排查、防翻车的五个关键点5.1 本地长期运行的注意事项与无人值守策略方案的基础部署方式是一台长期开机的电脑或家用小主机用nohup python app.py的方式把服务挂在后台。但这样会带来两个隐患一是断电后服务不自动恢复二是日志文件无限增长。更可控的做法是把服务注册为系统守护进程以本机 Ubuntu 环境为例先写一个 service 单元文件定义启动命令、工作目录和日志输出路径然后通过 systemctl 管理它的启动、停止、状态查看和开机自启。开机自启必须在服务文件里声明Restartalways这样即使程序异常退出守护进程也会在几秒后重新拉起。这个配置对长期无人值守非常重要否则断电重启后推送服务静默消失接收者会连续几天收不到消息。日志文件建议按日期分割每天写一个log-2025-xx-xx.txt保留最近 30 天。写日志不是只记“发送成功/失败”还要把当天的天气原始数据、模板消息的完整 data 字段记下来这样接收者反馈消息异常时你可以直接判断是天气源问题还是模板字段问题而不是反复猜测。5.2 高频翻车点排查微信接口与定时任务最常踩的五个坑坑点 1消息推不出去了但接口返回 errcode 0现象日志显示发送成功但接收者没收到消息。原因模板消息发送成功后微信有 5 分钟的网络延迟绝大多数情况 1-2 分钟内会到达但如果你在测试号后台把模板下线或改过模板 ID老 ID 会静默失效接口照样返回 0。解决检查测试号后台“模板消息”列表里的模板 ID 是否和代码配置一致特别是重新编辑模板后 ID 会不会变化。通常的验证方法是先调一次单发接口确认返回 ercode 0 后再走定时任务。坑点 2全天只推送了一条“消息已送达”的服务通知现象接收者看到的是“公众号提醒”的服务通知而不是你排版的天气卡片。原因在测试号后台申请模板时选错了模板类型用了“服务通知”而不是“模板消息”。解决在申请模板时确认是“测试模板”下的模板消息并检查模板内容里的字段是否与代码 data 的 keyword 数量一致。字段个数不匹配时微信会报参数错误或直接忽略多余字段。坑点 3定时任务到点不执行现象系统时间正确服务在运行但到点没推送。原因Flask 的 debug 模式启动了 reloader导致调度器被初始化了两次第一个进程的调度器被 kill第二个进程的调度器因端口占用而初始化失败。解决在入口文件里加一个判断if app.debug时不要启动调度器或者用单独脚本启动调度器与 Flask 进程解耦。这个问题在部署时非常典型很多项目翻车都翻在这里。坑点 4天气数据全是昨天或者返回 303 错误现象推送文案里显示的温度明显不是当天。原因高德接口的 forecasts 数组里第一项是当天预报但接口存在缓存某些时段更新延迟另外如果请求参数里extensions漏掉或写错接口返回的 live 数据是实时天气而不是预报数据。解决在天气模块中增加一个数据新鲜度校验把 forecasts 里的 date 字段与系统当天日期比较不一致时主动降级为“昨天天气”并补充说明同时写入 warning 日志。不能拿昨天的数据硬推送那样接收者会觉得这个工具不靠谱。坑点 5openid 写死导致换微信号后推送全部失败现象一切都正常但换了新手机号、新微信绑定后接收者收不到消息。原因openid 是微信用户在当前公众号下的唯一身份标识换了微信就变了。解决设计一个绑定接口让接收者主动发送一条消息给公众号应用通过接收消息回调记录新的 openid 并更新数据库。项目代码里直接留一个/wechat/callback路由处理普通文本消息并自动回写用户表。5.3 异常场景降级当你出门在外服务也要兜底如果接收者处在另一个城市而天气模块只配置了一个城市编码推送的天气预报就是错的。针对异地场景合理的做法是在 user 表里增加一个“当前城市”字段并通过回调消息中的地理位置类型消息来动态更新。微信的用户在会话里发送地理位置后测试号能拿到经纬度再通过逆地理接口解析城市编码从而自动切换天气预报的城市。这个功能不是什么“高级技巧”但对项目完整性来说很重要。真实场景中接收者不可能长期待在一个城市出差、旅游、回老家推送的天气必须跟着人变。因此部署时要写一个简单的迁移脚本把原城市字段扩展为支持多城市配置后端逻辑以用户最近一次上报的位置为准。若用户从未上报位置则采用默认城市配置。这是一个典型的业务需求驱动的设计而不是拍脑袋想出来的功能。6. 系统验收与答辩准备用一份交付清单证明项目能打6.1 可交付的功能验收清单与测试方法找人看你的项目第一眼盯的不是代码而是“这东西真的能用吗”。所以验收要分成三层来准备功能验收、鲁棒性验收、代码质量验收。功能验收条目包括输入正确的城市编码和 key是否能拿到今日天气并输出符合预期的模板消息定时任务在指定时间触发后push_log 表是否新增一条成功记录手动把模板 ID 改成错误值推送失败后日志里是否能记录到明确错误信息取消关注后再次执行任务push_log 里是否记录“用户未关注”状态接收者发送文本“天气”给公众号时是否能收到绑定成功提示并更新用户表。这五条是演示的核心场景每一条都要能现场跑通。鲁棒性验收要制造异常断网后调用天气接口是否会超时并优雅报错access_token 过期后是否会自动刷新并重试高德接口返回空数据时推送文案是否正常降级而不抛异常。这些测试建议写一个test_manually.py脚本用真实接口但不真正推送只打印中间数据验证模块的输出结构。代码质量方面重点关注三个点是否配置了独立的虚拟环境并提供 requirements.txt配置文件是否把 AppSecret、高德 Key 等敏感信息用环境变量或.env管理而不是硬编码日志输出是否包含时间戳、模块名、成功/失败状态便于追溯。这三条看着基础但恰恰是答辩时评委最看重的工程素养。6.2 答辩演示脚本八分钟内讲完一套完整项目答辩演示建议按以下顺序进行先用两分钟展示微信聊天记录证明消息按时推送再花两分钟切入代码按“天气模块 - 渲染模块 - 推送模块”的路径讲解关键实现接着用两分钟演示日志文件和 push_log 表说明项目在真实运行中是可观测的最后两分钟介绍异常处理机制比如把模板 ID 改错现场展示报错日志和降级逻辑。这八分钟的脚本覆盖了“需求 - 设计 - 实现 - 运维”的全流程不会出现评委问一个问题就卡壳的情况。更深一层准备一个热点问题是“为什么不用现成的天气 App 或第三方推送服务”。回答的角度不是技术比较而是需求切片接收者要的不是一份完整的天气报表而是每天固定时间点、固定入口、带有人情味的一段提醒。这是个人工具的定位自己做才能做到这个颗粒度的定制。回答时顺便提到“模板消息字段的定制就是我把需求翻译成产品逻辑的过程”评委能感受到你对需求的把握。最后再分享一个习惯从那以后我每次做类似的自动推送工具都会强制自己先写一页验收清单再开始写代码哪怕只有三行字也先写着做完逐条打勾。这比事后补测试更节省时间也避免了很多“自我感动型交付”。希望你也能用这套思路把你手里的项目真正打磨到可交付、可展示、可复盘的程度。希望这篇文章对你有帮助。本文还有配套的精品资源点击获取