简介:微信公众号每日自动推送天气与纪念日通知的制作教程,专门面向想用技术为女朋友制造惊喜、有一点编程基础的学习者,目标是实现无需手动干预的每日定时消息提醒。包体很精简,共5个文件、大小仅5KB,涵盖Python主程序、配置项、GitHub Actions工作流与说明文档;py承担天气与纪念日信息组装,yml负责每日自动运行,txt与md提供API参数和部署指引。目前已有7692人学习浏览,属于热度较高的小项目资源。内容包括成品源码、消息模板和完整的线上运行配置,拿到后替换天行/和风的API密钥、填好祝福语即可部署;作者亲测可行,并分享了申请API、配置定时任务与排错的经验,照着操作就能为女朋友做出专属的天气提醒公众号。相比普通礼物,这类用心的代码小功能更容易打动人心,适合节日、纪念日或日常浪漫时刻使用。
1. 项目定位:用微信公众号每天给女朋友推天气,做一件能跑起来的浪漫小事
早晨八点,手机弹出一条微信通知,发件人不是同事也不是快递,而是自己写的一个公众号,内容是今天的天气、温度和一句“记得带伞”。很多人第一眼看到这类教程,觉得难度在“调天气接口”,真正动手才发现,卡人的永远是消息通道:公众号消息权限没理清、access_token 缓存没写好、定时任务在服务器上悄悄失效,代码在本地跑得再欢,线上还是发不出去。这套制作教程把天气获取、文案拼装、微信推送、定时执行拆成四个独立模块,每一步都给出可运行代码和验证手段,目标是让一条天气通知从接口数据变成每天准时出现在手机上的消息。适合想给女朋友做自动化推送的开发者也适合第一次摸公众号接口的人。
2. 技术方案与整体架构:一条天气数据从云接口到手机微信的完整链路
先把这项目当一条数据管道看。每天触发一次,天气数据从外部接口拉回来,程序把结构化数据加工成一句人话,再通过微信接口推给指定用户。整条链路上没有复杂的业务逻辑,但每一环都依赖一个外部系统,任何一个环节出问题,这一天的推送就没了。所以架构设计追求的不是花哨,而是“每个环节可以独立验证,失败了知道该看哪里”。
2.1 技术选型:Python、Requests、APScheduler 配合公众号模板消息
选型不用纠结,这种轻量定时任务用 Python 写成本最低。requests 负责所有 HTTP 调用,APScheduler 负责进程内定时调度,配置用 YAML 外置。消息通道我选公众号模板消息接口而不是客服消息接口,原因很实际:客服消息有 48 小时会话限制,用户不和公众号说话,这条通道根本发不出去;模板消息属于应用通知类消息,用户关注了公众号、拿到对方 openid,就能主动下发。这正好匹配“每天一条天气通知”的场景。
天气数据源我习惯用某地图开放平台的天气接口,返回结构稳定,区分白天和夜间预报,拼文案很顺手。真实项目里当然也可以换其他天气服务商,差别只在请求参数和返回字段名。下面这张选型表,后面换成别的接口时也留着参照。
| 模块 | 选择 | 理由 | 注意点 | | Python 3.8+ | 轻量任务 | 生态成熟,写小任务不折腾 | 服务器上别装太新版本,避免第三方库兼容问题 | | requests | HTTP 调用 | 同步请求写起来最简单 | 必须设 timeout,不设可能长挂 | | APScheduler | 进程内调度 | 改时间不用碰 crontab | 进程退出调度就没了,需要守护 | | 公众号模板消息 | 消息通道 | 主动推送不受会话限制 | 模板 ID 和字段 key 必须精确匹配 | | YAML | 配置外置 | 城市、账号、模板 ID 不写死在代码里 | 密钥不要提交进代码仓库 |
这里有个容易被忽略的细节:requests 的超时设置。不少教程里直接 requests.get(url) 不带 timeout,某天天气接口响应卡住,定时任务整个堵在那里,消息晚了一两个小时。所以后面所有外部请求我都会加 timeout,宁可这次失败等下次重试,也不能让任务挂死。
2.2 目录结构:把天气、文案、微信三个关注点拆开
代码量不大,但为了二次开发方便,建议最小结构就按下面这样拆:
weather-push/ |-- config.yaml # 城市编码、公众号 appid、secret、模板 ID、openid |-- requirements.txt # requests、pyyaml、apscheduler |-- weather.py # 天气接口拉取与解析 |-- message.py # 文案拼接 |-- wechat.py # access_token 管理与模板消息发送 |-- push.py # 主流程:拉数据、拼文案、发消息 |-- scheduler.py # 定时调度入口 |-- run_cron.sh # 给 crontab 调用的包装脚本 └-- logs/ # 运行日志目录weather.py 只做一件事:给一个城市编码,返回解析后的天气结果。message.py 把天气结果转成最终下发的文本。wechat.py 封装微信的两个接口,一个是拿 token,一个是发模板消息。push.py 是所有模块串起来的主入口。scheduler.py 是“进程内定时”方案,如果你最终用系统 crontab,这个文件可以不部署。
拆这么细的原因不是故作优雅,而是每个外部接口都可能抽风。天气数据出问题,只需要看 weather.py;微信报错,只需要看 wechat.py;文案想改,碰 message.py 就够了。真到排查问题那一天,你会感谢当初少写了几个“大而全”的函数。
2.3 一次推送从触发到送达的完整生命周期
整条链路按时间顺序可以拆成六步:
- 定时器触发,进入 push 入口;
- 读 config.yaml,拿到城市编码、天气 key、公众号参数;
- weather.py 请求天气接口,解析出当天白天和夜间的天气、温度、风;
- message.py 把数据拼成一段生活化文案,同时拆成模板消息需要的数据结构;
- wechat.py 先取 access_token(带缓存),再调用模板消息接口发送;
- 微信服务器完成下发,用户微信里出现这条天气通知。
第一次运行不要直接挂定时器,应该先手动执行 push.py 一次,确认第 3、4、5 步各自返回正常。这六个步骤里最容易被忽略的是第 5 步的 token 缓存,很多人第一次跑通了,第二天就没消息了,原因就是 token 被重复刷新导致旧的失效。后面的避坑章节会专门说这个问题。
3. 核心代码拆解:天气查询、文案拼接、模板消息一个环节一个环节写
这一章按模块来写,顺着调用顺序走。三个模块之间没有隐藏依赖,每个都能单独抽出来测,这一点对排错非常重要。
3.1 天气数据获取:接口参数、返回解析与异常降级
先看 weather.py。以某地图开放平台天气接口为例,核心参数是三个:城市编码、接口 key、是否返回全天预报。代码里强制设置了 timeout,并对 status 做了判断:
import requests def fetch_weather(city_code: str, key: str) -> dict: url = "https://restapi.example.com/v3/weather/weatherInfo" params = { "city": city_code, "key": key, "extensions": "all", } resp = requests.get(url, params=params, timeout=8) data = resp.json() if data.get("status") != "1": raise RuntimeError(f"weather api error: {data.get('info')}") forecasts = data.get("forecasts") or [] if not forecasts: raise RuntimeError("weather api return empty forecasts") return forecasts[0]这段代码做了两个防御动作。第一个是 status 判断,接口返回 status 为“1”才表示成功,否则 info 字段会带出错原因,直接抛异常比带病往下走好排查。第二个是 forecasts 空判断,城市编码传错时接口经常返回成功状态但结果为空,这种“假成功”如果不在源头拦住,后面拼出来的文案就是空的,消息照样能发出去,但内容没法看。
第一次接入时我强烈建议把返回的完整 JSON 存一份到日志里,因为字段名是 daytemp 还是 daytemp_float,不同接口差异很大,存下来看一遍比反复读文档快得多。等项目跑稳了,再把这个调试日志降到 debug 级别。
3.2 文案拼接:把数据变成一句带情绪的话
拿到了天气数据,接下来是 message.py。这里要把气象数据翻译成人话,同时为模板消息准备好数据字段。我的习惯是先拼成纯文本,再在接口层拆字段,这样后面改文案只动一个函数:
def build_message(forecast: dict, city_name: str) -> dict: today = forecast["casts"][0] report_time = forecast.get("reporttime", "")[:10] text = ( f"早安,今天{report_time}。\n" f"{city_name}白天:{today['dayweather']}," f"{today['daytemp_float']}℃。\n" f"夜间:{today['nightweather']}," f"{today['nighttemp_float']}℃。\n" f"风向:{today['daywind']},风力{today['daypower']}级。\n" f"出门看天,带伞不带伞自己判断~" ) return { "city": city_name, "time": report_time, "text": text, }注意模板消息的字段不能随便给。微信模板消息里每个变量都必须在公众平台选用的模板里存在,比如模板里写了 {{city.DATA}}、{{time.DATA}}、{{text.DATA}},那 data 的 key 只能是 city、time、text,多一个少一个都报错。所以 message.py 返回的是一个和模板一一对应的 dict,后面发送时直接套上去。
如果想让文案每天不重样,可以把天气情况映射成一组形容词,比如“晴天”对应“适合散步”,“阵雨”对应“记得带伞”,这种映射表放在文件底部,运行时根据 today['dayweather'] 去查表。这是成本最低的个性化改造。
3.3 微信模板消息发送:access_token 的缓存与接口调用
wechat.py 是整个项目最容易翻车的地方,因为 access_token 有 7200 秒有效期,并且有效期内重复获取会导致之前的 token 失效。最经典的翻车现场是:多次重启或两个任务各拿了一次 token,后拿到的把先拿到的干掉,某次发送突然报 40001。
我的做法是进程内加一个简单缓存,token 没过期就继续用,并且提前 5 分钟视为过期,避免在临界点踩到接口还没同步的坑:
import time import requests _token_cache = {"token": None, "expire_at": 0} def get_access_token(appid: str, secret: str) -> str: now = time.time() if _token_cache["token"] and _token_cache["expire_at"] - now > 300: return _token_cache["token"] resp = requests.get( "https://api.weixin.qq.com/cgi-bin/token", params={ "grant_type": "client_credential", "appid": appid, "secret": secret, }, timeout=5, ).json() if "access_token" not in resp: raise RuntimeError(f"get access_token failed: {resp}") _token_cache["token"] = resp["access_token"] _token_cache["expire_at"] = now + int(resp.get("expires_in", 7200)) return resp["access_token"]缓存这个动作看起来不起眼,实际上决定了公众号会不会在某天突然被限流。微信对获取 token 的接口有频率限制,反复刷新很容易触发,轻则接口报错,重则短暂封禁。我预留了 300 秒的余量,也就是 token 实际还剩不到 5 分钟时就重新获取,不会卡在最后几秒发送时发现 token 失效。
发送模板消息是一个 POST 请求搞定:
def send_template_message(token: str, template_id: str, openid: str, data: dict) -> dict: resp = requests.post( f"https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={token}", json={ "touser": openid, "template_id": template_id, "data": data, }, timeout=8, ).json() if resp.get("errcode") != 0: raise RuntimeError(f"send template message failed: {resp}") return resp返回值里 errcode 为 0 才是发送成功,其他错误码需要对照微信官方文档。注意 data 里的值必须是字符串,别顺手传成数字,微信接口对类型很挑。
主流程 push.py 就是把两个模块串起来:
def main(): cfg = load_config("config.yaml") forecast = fetch_weather(cfg["city_code"], cfg["weather_key"]) message_data = build_message(forecast, cfg["city_name"]) token = get_access_token(cfg["appid"], cfg["app_secret"]) send_template_message(token, cfg["template_id"], cfg["openid"], message_data)这里没有加任何重试逻辑。定时任务本身允许失败后第二天再补,如果加了重试,反而要小心同一分钟发重复消息。我见过有人把整个 main 包在 while 循环里重试,结果女朋友在 1 分钟内收到两条一模一样的天气,比没收到还尴尬。
4. 部署运行与定时调度:从本地手动触发到服务器每天自动执行
代码写完,接下来是把任务变成“每天早上自动跑一次”。建议分三步走:先把公众号参数准备好,再本地手动触发一次,最后上服务器挂调度。不跳过前两步直接上服务器,是血泪经验换来的习惯。
4.1 公众号接入参数从哪里拿:appid、secret、模板 ID、openid
先准备配置文件 config.yaml:
weather_key: "你的天气接口key" city_code: "某市的城市编码" city_name: "某市" appid: "公众号appid" app_secret: "公众号secret" template_id: "模板消息模板ID" openid: "接收消息的用户openid"appid 和 secret 在公众号后台的基本配置里找,第一次获取 secret 时平台会要求管理员扫码确认。template_id 在后台“模板消息”功能里选用你想要的模板,选好后会给出模板 ID。openid 获取有两种方式:公众号已经上线的话,可以在用户管理里找到关注用户,详情里有对方的 openid;如果是开发阶段,可以用开发者工具模拟或通过网页授权换取。个人开发者场景下,最快的是从用户管理页面把 openid 复制进配置。
这里有个关键提醒:选用的模板内容决定了后续 data 字段的 key。比如模板里定义了“城市”“日期”“内容”三个变量,那 data 里只能用 city、date、content 这三个 key。所以第 3 章里我把 message.py 返回的 dict 设计成和模板一一对应,就是为这个。
4.2 本地手动触发一次:先确认链路通畅再谈自动化
不要第一时间去配定时器,先把主流程跑通:
pip install -r requirements.txt python push.py如果配置没问题,脚本没有任何输出说明发送成功。如果报错,就按错误出处往上追:报错在 weather.py,说明天气接口参数有问题;报错在 get_access_token,说明 appid、secret 不对或 IP 白名单没加;报错在 send_template_message,把返回 JSON 原样贴出来和错误码对照,最常见的 40003 是 openid 错误,41030 是模板 ID 或模板字段错误。
第一次跑通之后,我习惯手动连跑三遍。第一遍验证接口没问题,第二遍验证 access_token 缓存是否正常复用,第三遍确认连续发送不会被微信限流。三遍都稳定返回 errcode 0,才说明链路基本可靠。
4.3 上服务器:crontab 优先,APScheduler 作为备选方案
部署有两条路线。一条是把 scheduler.py 带上去,用 APScheduler 在代码里定时执行。好处是时间规则写在 Python 里,改起来方便;坏处是进程必须一直活着,进程挂了调度就没了,还得再套一个守护进程。另一条是干脆不用进程内调度,只留 push.py,在系统 crontab 里加一行,让系统每天早上固定时间调用一次。我个人推荐后者,cron 是操作系统级保障,比自建守护进程靠谱,而且这个任务本身是“一次执行完就退出”的类型,天然幂等,不需要常驻。
run_cron.sh 长这样:
#!/bin/bash cd /opt/weather-push /usr/bin/python3 push.py >> logs/cron.log 2>&1crontab 里这样挂:
0 8 * * * TZ=Asia/Shanghai /opt/weather-push/run_cron.sh这里有两个细节容易踩。第一是脚本里必须 cd 到项目目录,不然代码里的相对路径 config.yaml 找不到;第二是 crontab 默认可能用系统时区,如果不显式指定 TZ=Asia/Shanghai,你以为的早上 8 点实际可能是下午 4 点,早安推送变成了晚安问候。日志重定向到 cron.log 也不是摆设,每天早上看一眼这个文件,确认今天有没有正常发送,这是整套系统唯一的可视化窗口。
注意:服务器时区如果已经设置成北京时间,crontab 里的 TZ 可以省略。但与其依赖服务器环境,不如在 cron 表达式里写死时区,这样换机器不换行为。
5. 避坑指南与常见问题排查:消息发不出去,通常卡在这五个地方
这一章把实际跑项目过程中遇到的高频问题整理成清单。每条按现象、原因、解决方式写,遇到报错建议先来对照一遍,再去看完整错误信息。
5.1 发送报 41030:模板 ID 没问题,模板字段却不对
现象:send_template_message 返回 errcode 41030,查文档说模板不存在,但你在后台明确复制了这个模板 ID。
原因:多数情况不是模板 ID 写错,而是模板 ID 属于另一个公众号,或者用了测试号的模板在正式号上发。还有一种可能:data 的 key 和模板变量不一致,微信也会把这个错误归类到模板不匹配。
解决:确认当前 appid 对应的公众号后台确实选用了这个模板,然后把 data 的 key 和模板变量逐个对照,多一个少一个都会被拒绝。我有段时间为了省事把三个字段合并成一个 text 字段,结果微信直接报 41030,分开字段后立刻正常。
5.2 access_token 突然失效,重启后恢复正常,看着像玄学
现象:连续几天发送正常,某天开始偶尔报 40001,过一会儿又自己好;重启进程后必然好一段时间。
原因:access_token 在 7200 秒内重复获取会让旧 token 失效。如果同时开了多个定时任务,或者手动调试了一次又让定时任务立刻跑,第二次拿的 token 会把第一次的作废,定时任务里那个缓存 token 就成了死 token。
解决:先从代码上保证 token 缓存复用,进程级缓存足够。如果服务器上部署了多个副本,就得把 token 放到 Redis 或文件里共享。更省事的办法是把调度统一到同一个入口,避免“手动执行一遍 + 定时任务再执行一遍”的并发场景。
5.3 天气接口返回成功,但 forecasts 是空的,白推了一天
现象:status 是“1”,看起来成功,但 forecasts 数组为空,推送文案变成“None℃”这类字符串。
原因:这个假成功通常由城市编码错误引起。直接把城市名“某市”传给接口,接口不报错,但找不到匹配数据,返回空数组。
解决:确认 city_code 用的是行政区划编码而不是城市名。去查一下目标城市对应的 6 位编码,填进 config.yaml 再跑一次。编码对了,forecasts 自然有数据。
5.4 定时任务时间不准:说好的 8 点变成了下午
现象:crontab 里写的 0 8 * * *,但日志显示每天下午才执行。
原因:服务器系统时区是 UTC,crontab 按系统时间解析,8 点指的是 UTC 8 点,换算成北京时间就是 16 点。
解决:在 crontab 行里显式加 TZ=Asia/Shanghai,或者用 timedatectl set-timezone Asia/Shanghai 把服务器时区改成北京时间。改完记得用 date 命令确认,别光改文件不验证。
5.5 同一分钟收到两条一样的推送
现象:对方连续两天在同一分钟收到两条相同的天气消息。
原因:部署阶段埋的雷。有人在进程里跑了 APScheduler,又同时在 crontab 里加了同一行,两边在同一时间各触发一次。也有人给 push.py 加了失败重试逻辑,第一次发送超时但实际发出去了,重试补发又送了一条。
解决:调度入口只保留一个,选了 crontab 就把 scheduler.py 收起来。重试逻辑必须做成只在明确收到网络错误时才重试,并且加一个基于文件锁或 Redis 的去重判断,十几行代码就能避免这种社死现场。
6. 二次开发方向:从固定城市推送变成会记住纪念日的天气助手
项目跑稳之后,想让它更懂人,可以从三个方向改造。这些都不需要动主体架构,只是往一个模块里加逻辑,这也是当初把代码拆成 weather、message、wechat 三个文件的原因。
第一个方向,把配置外置得更彻底。现在城市、openid、模板 ID 都在 config.yaml,但如果想用同一套代码推给两个人、两个城市,就把 config.yaml 改成用户列表,让 push.py 循环处理:
for user in cfg["users"]: forecast = fetch_weather(user["city_code"], cfg["weather_key"]) message_data = build_message(forecast, user["city_name"]) send_template_message(token, user["template_id"], user["openid"], message_data)这样一份代码多城市多人推送,只需要在 YAML 里追加用户块。我会把每个用户的 city_name、openid、模板 ID 独立成块,方便女朋友、家人、自己各自维护。
第二个方向,加纪念日和倒计时。在 message.py 里维护一个日期表,每次推送前计算距离目标日期还有多少天:
from datetime import datetime anniversary = datetime(2025, 6, 1).date() days_left = (anniversary - datetime.now().date()).days if days_left < 0: celebration = "纪念日已经过去了,来日方长" elif days_left == 0: celebration = "就是今天,别忘了准备惊喜" else: celebration = f"距离纪念日还有 {days_left} 天"把 celebration 拼进 text,模板消息里就能显示出倒计时。这个改动只加一个新函数,不碰天气模块和微信模块。
第三个方向,接入大模型接口生成早安语。天气数据依然是结构化的,但文案不再用固定模板,而是把温度、天气、风力作为参数交给大模型,生成一句口语化的话。这里重点要做好 fallback:模型调用可能超时、可能生成奇怪内容,一旦失败就回落到本地模板文案,保证消息永不缺席。这也是我把项目送进生产环境后才明白的原则——花活可以上,但底线逻辑必须是最稳的那条。
这三个方向做完,项目就不再是单纯天气推送,而是一个把数据、文案、消息通道三件事都摸清楚的小系统。从那以后,我每写一个依赖外部接口的定时任务,都会强制保留 push.py 这种“手动触发一次”的入口,先把链路跑通再挂调度,省掉了不知道多少次半夜查日志的麻烦。希望帮到你。
本文还有配套的精品资源,点击获取