ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python+微信测试号实现每日天气自动推送:从API调用到定时任务

Python+微信测试号实现每日天气自动推送:从API调用到定时任务 简介这是一份微信公众号每日自动推送天气与纪念日通知的制作教程资源包面向有少量编程基础、希望用代码向伴侣表达心意的人群。基于GitHub Actions实现每日定时线上运行无需手动触发可部署后长期稳定使用。资源共5个文件压缩包约5KB目录结构一目了然main.py为推送逻辑主脚本config.txt用于填写API密钥与消息配置weixin.yml是GitHub Actions的定时任务定义requirements.txt列出全部依赖README.md则提供从申请到部署的完整说明。脚本支持调用天行与和风API获取天气、星座信息并嵌入纪念日提醒功能消息模板可自由修改运行前需自行申请对应API。整体操作门槛低跟着说明即可完成配置和上线已有7688人学习下载很适合想快速完成一个暖心自动化天气推送项目的开发者也可以作为学习GitHub Actions定时任务的练手样例。1. 公众号天气推送卡点在接口权限不在代码想给自己和对象做一个“每天早上 8 点自动推送天气”的微信公众号最大的拦路虎不是 Python 不会写而是账号权限。个人订阅号没有模板消息接口服务号注册又需要企业资质很多人到这里就放弃了。其实微信官方给开发者留了一个测试号入口扫码登录就有模板消息权限拿它当个人天气通知中心完全够用。这篇教程用 Python 调高德天气接口拿数据再把数据拼成模板消息推到微信最后用 cron 定时跑跑通后每天自动发全程不碰付费服务。适合会一点 Python、想用自动化表达心意的人也适合想从零走一遍微信接口的开发者。2. 准备三步测试号、天气 Key 和城市编码这套链路其实很短定时任务到点触发 Python 脚本脚本先拿微信接口凭证再拿天气数据最后拼一条模板消息发给目标用户。整条链路里没有服务器也能跑Windows 计划和树莓派都行只要它能定时执行 Python。唯一逃不开的是两个外部账号微信测试号和高德开放平台。下面把每个环节的准备工作拆开讲透免得后面代码写完了才发现少了某个 ID。2.1 为什么选测试号而不是订阅号个人主体能注册的公众号只有订阅号但订阅号并不提供“模板消息”这个能力。模板消息是给用户发结构化通知的接口它在服务号手里个人资质又注册不了服务号两条常规路径都被堵死。微信公众平台测试号是官方给开发者做接口调试用的环境权限表里把模板消息放开了所以它反而成了个人做自动化通知最顺的一条路。测试号的全称叫“微信公众平台接口测试账号”定位不是对外运营而是测接口。它支持扫码添加最多两个测试关注者这个限制对“推给对象”“推给自己”足够。要注意测试号跟正式服务号的数据完全隔离测试号里建的模板、拿到的 appID不能直接搬到正式服务号两边的模板 ID 也互不通用。如果你以后想把推送做成面向大量用户的产品再走服务号认证个人日常用测试号就是零成本最优解。2.2 从测试号页面拿 appID、appsecret、模板 ID 和 openid浏览器打开https://mp.weixin.qq.com/debug/cgi-bin/sandbox用微信扫码登录。登录后就是测试号控制台页面顶部有两个字段最关键appID 和 appsecret。这两个值决定脚本能不能拿到调用凭证建议先复制到本地文本里但不要提交到公开仓库。测试号看起来不起眼被人拿到也能给你的关注者发消息泄露了一样得重置。页面中部是“模板消息”区块点“新增测试模板”标题填“天气提醒”模板内容按下面这段填早上好今日天气信息 {{keyword1.DATA}} {{keyword2.DATA}} {{keyword3.DATA}} {{remark.DATA}}提交后模板列表里会出现一条记录那串像wx1234567890abcdef_abc123的字符串就是模板 ID脚本发送时必须传它。页面底部是测试号二维码用微信扫码即视为“关注”测试号。关注后底部“关注者列表”会多出一行里面有微信号和 openid。openid 是用户在该公众号下的唯一标识模板消息的touser字段填的就是它。最终要保存的凭证整理如下表参数来源脚本里的用途appID / appsecret测试号页面顶部换取 access_token模板 ID测试号模板消息列表发送时指定用哪个模板openid测试号关注者列表指定推给谁提示关注者列表为空说明还没扫码拿手机微信扫页面底部的二维码列表刷新后就会出现你的 openid。别从网上复制别人的 openid 来测试那只会给陌生人推送。2.3 高德天气 APIKey 类型与 6 位 adcode天气数据我选高德开放平台的天气接口原因很直接国内访问稳定、免费6 位城市编码公开可查。申请方式打开高德开放平台控制台注册后进入“应用管理”创建一个应用再给应用添加 Key。服务类型一定选“Web服务”而不是“Web端(JS API)”。Web 服务类型的 Key 是给服务端请求用的不需要配域名白名单选错类型就会出现后面INVALID_USER_KEY的报错。天气接口地址是https://restapi.amap.com/v3/weather/weatherInfo关键参数就三个key、city、extensions。其中city不是城市名也不是拼音而是 6 位数字 adcode。比如北京东城区是 110101上海黄浦区是 310101杭州是 330100。想知道城市 adcode去高德开放平台的行政区划接口查或者在高德地图 PC 版搜索城市后从地址栏 URL 里提取。adcode 填错不会立刻报错只会返回错误城市的天气所以部署前先自己确认一次返回体里的forecasts[0].city对不对。我选extensionsall它返回 3 天预报casts[0]是今天的数据extensionsbase只返回实时天气早晚推送的场景下预报里的“晴天转多云、最高温最低温”比单一实时温度更适合写成一句完整通知。高德在手整个准备阶段就齐了下一步进入代码。3. 推送脚本拆成三段取 token、取天气、发模板消息脚本结构不复杂但每一个环节都有独立的返回码和失败模式。我把整件事拆成三个函数get_access_token负责微信凭证get_weather负责天气数据send_template_msg负责最终推送。三个函数各自抛出异常主函数统一捕获这样出错时能一眼定位是微信的问题、高德的问题还是参数问题。3.1 access_token 必须缓存两小时有效期与 200 秒裕量微信接口都要求带 access_token它由 appID 和 appsecret 换得有效期 7200 秒。获取接口是https://api.weixin.qq.com/cgi-bin/token?grant_typeclient_credentialappidAPPIDsecretAPPSECRET。如果每次推送都现取 token短时间请求多了会触发频率限制而且两小时过期机制会让调试时反复遇到 40001。我的习惯是把 token 缓存到本地文件并在过期前预留 200 秒余量。import json import os import time import requests APPID wx你的appID APPSECRET 你的appsecret TOKEN_CACHE_FILE token_cache.json def get_access_token(): # 先读缓存没过期就直接复用 if os.path.exists(TOKEN_CACHE_FILE): with open(TOKEN_CACHE_FILE, encodingutf-8) as f: cache json.load(f) if time.time() cache[expire_time]: return cache[access_token] url https://api.weixin.qq.com/cgi-bin/token params { grant_type: client_credential, appid: APPID, secret: APPSECRET, } resp requests.get(url, paramsparams, timeout10).json() if access_token not in resp: raise RuntimeError(f获取 access_token 失败: {resp}) # 提前 200 秒过期避免临界时间点 token 还在缓存里但已被微信判死 expire_time time.time() int(resp[expires_in]) - 200 with open(TOKEN_CACHE_FILE, w, encodingutf-8) as f: json.dump({access_token: resp[access_token], expire_time: expire_time}, f) return resp[access_token]逻辑说明先检查本地缓存是否在有效期内有效就直接返回无效才请求微信接口。异常时把微信返回的完整 JSON 抛出去排查时能看到是 appID 错了还是 secret 复制多了空格。timeout10是必须的不然网络卡住会拖死整个定时任务。参数说明grant_type固定写client_credential不需要改。expires_in单位是秒不同环境返回可能略有差异按返回值算即可。为什么预留 200 秒而不是用满 7200 秒因为脚本执行那一刻系统时钟和微信服务器时钟可能有一两分钟的偏差如果 token 刚好在临界点微信会报 40001而你又没有走到重新获取的逻辑。如果你每天只跑一次预留 600 秒刷新也完全可以。3.2 天气字段解析extensionsall 与 casts 数组高德天气接口返回的是嵌套 JSON不能靠印象取字段。核心结构是forecasts数组按城市分组里面每个城市的casts数组按日期从今天到后天排列。casts[0]就是今天。字段dayweather是白天天气nightweather是夜间天气daytemp和nighttemp分别是高低温daywind和daypower是风向和风力等级。AMAP_KEY 你的高德Web服务Key def get_weather(adcode: str) - dict: url https://restapi.amap.com/v3/weather/weatherInfo params { key: AMAP_KEY, city: adcode, extensions: all, # all 返回 3 天预报base 只返回实时天气 } resp requests.get(url, paramsparams, timeout10).json() if resp.get(status) ! 1: raise RuntimeError(f天气接口返回异常: {resp}) forecast resp[forecasts][0] today forecast[casts][0] day_weather today[dayweather] night_weather today[nightweather] weather_desc day_weather if day_weather night_weather else f{day_weather}转{night_weather} return { city: forecast[city], weather: weather_desc, temp: f{today[daytemp]}℃ ~ {today[nighttemp]}℃, wind: f{today[daywind]} {today[daypower]}, }逻辑说明status是字符串1才代表成功不要下意识用 1。白天天气和夜间天气一致时直接用一个词不一致时拼成“多云转阴”这种写法比单拿白天天气更贴近人的感知。比如白天晴夜间多云单写“晴”会让人误判一整天的状态。参数说明extensionsall多返回预报和温度区间代价只是响应包稍大对脚本没有影响。需要今天高低温必须用 allbase 返回的实时天气只有一个当前温度。daypower返回类似“3级”的文本拼出来就是“东南风 3级”。如果你在南方还想拼湿度高德在lives里提供湿度字段那需要extensionsbase两个模式不能同时返回按需取舍即可。3.3 发送模板消息errcode0 才算成功拼模板消息的本质是把测试号页面里写好的模板内容逐个变量填值再 POST 给微信接口。模板里写了几个变量data里就传几个一个不多一个不少。发送接口是https://api.weixin.qq.com/cgi-bin/message/template/send?access_tokenACCESS_TOKEN请求体是 JSON。def send_template_msg(openid: str, template_id: str, weather: dict) - str: body { touser: openid, template_id: template_id, url: http://weixin.qq.com/download, # 点击消息后跳转的链接测试期可留空 data: { keyword1: {value: f{weather[city]} {weather[weather]}}, keyword2: {value: weather[temp]}, keyword3: {value: weather[wind]}, remark: {value: 今天也要元气满满出门记得看天气}, }, } url fhttps://api.weixin.qq.com/cgi-bin/message/template/send?access_token{get_access_token()} resp requests.post(url, jsonbody, timeout10).json() if resp.get(errcode) ! 0: raise RuntimeError(f模板消息发送失败: {resp}) return resp[msgid] def main(): weather get_weather(110101) msgid send_template_msg(你的openid, 你的模板ID, weather) print(f发送成功, msgid: {msgid}) if __name__ __main__: main()逻辑说明url字段是用户点击这条模板消息后跳转的地址微信要求合法 URL测试阶段填http://weixin.qq.com/download不会真的触发下载只是占位。data里每个 value 都是字符串color字段可以省略省略后默认黑色不需要花哨配色。参数说明errcode为 0 时微信才会返回msgid。几个常见报错码要背下来47003 是模板参数错误40001 是 token 失效45009 是接口调用超限。发送失败时把resp完整打印到日志不要去猜原因errmsg里已经写了原因。weather字典在 3.2 里构造好后直接透传main 函数保持薄薄一层这是定时任务脚本最容易维护的结构。4. 定时执行cron、Windows 计划任务与日志留痕脚本能跑只是第一步每天自动跑才是这个项目存在的意义。部署环境不同定时方案也不同。Linux 服务器用 cronWindows 用任务计划程序两条路都能走通但都有各自的坑。这一章把两种方案讲透顺便解决“跑是跑了但跑没跑对”的问题。4.1 crontab 与时区把北京时间 8 点坐实Linux 上最顺手的方案是 cron。编辑定时任务用crontab -e加入一行0 8 * * * cd /home/weather /usr/bin/python3 send_weather.py weather.log 21这行意思是每天 8 点进入/home/weather目录用绝对路径指定的 python3 执行脚本标准输出和错误输出都追加写到weather.log。cron 的环境变量和登录 shell 不一样直接用python3可能因为 PATH 不同而报“命令找不到”所以写绝对路径。先执行which python3确认解释器位置再填进去。时区是 cron 里最大的坑。很多云服务器系统时区是 UTC比北京时间慢 8 小时。如果直接写0 8 * * *它会在北京时间下午 16:00 执行到时候推送就成了“晚安天气”。解决方式两种一是在 crontab 顶部加一行CRON_TZAsia/Shanghai但这依赖 cron 实现版本老版本不一定认识二是直接把系统时区设为 Asia/Shanghai执行sudo timedatectl set-timezone Asia/Shanghai后再写0 8 * * *就稳妥。写完想立刻验证可以临时把分钟改成当前时间往后 3 分钟的数值跑通了再改回 8 点。cron 的最小粒度是分钟不要尝试写 30 秒执行一次。如果定时任务没触发查看系统日志是一个办法sudo tail -f /var/log/syslog看到CRON开头的行就知道调度器有没有启动任务。这一步排查成本很低能省掉第二天发现没推送的尴尬。4.2 Windows 计划任务睡眠和相对路径两个坑如果只有一台 Windows 电脑用任务计划程序也行。触发器选“每天”时间设 8:00操作选“启动程序”程序填 python.exe 的完整路径参数填脚本的完整路径。两个坑必须提前埋好。第一个是睡眠。笔记本合盖就睡睡着的机器不会执行计划任务。要在计划任务属性的“条件”标签里取消“只有在计算机使用交流电源时才启动此任务”再勾上“唤醒计算机来运行此任务”。即便如此休眠状态也可能醒不过来建议把电源计划里的“休眠等待时间”调长或者接受偶尔漏推的现实。台式机没有这个问题但也要注意计划任务默认可能在空闲时才启动。第二个是相对路径。脚本里如果有open(token_cache.json)这种写法计划任务的工作目录默认不是你双击脚本时的目录运行时会报FileNotFoundError。统一改成基于脚本文件所在目录拼绝对路径BASE_DIR os.path.dirname(os.path.abspath(__file__)) TOKEN_CACHE_FILE os.path.join(BASE_DIR, token_cache.json)逻辑说明__file__是当前脚本的路径abspath处理相对路径和符号链接dirname取出目录。所有读写文件的地方都用BASE_DIR拼路径这样无论从哪里启动都不会找不到文件。这个习惯在 Windows 计划任务里是刚需在 Linux cron 里同理。4.3 日志与验证让失败在当天暴露宁可多写日志也别让脚本静默失败。前面的函数里已经raise RuntimeError主函数外围必须接住异常并落盘import logging logging.basicConfig( filenameweather.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, ) if __name__ __main__: try: main() logging.info(天气推送成功) except Exception as exc: logging.exception(天气推送失败: %s, exc)逻辑说明logging.exception会把完整堆栈写进文件出错时不需要人守在电脑前。如果不想用 loggingcron 那行的 weather.log 21也能把 print 和 traceback 捞进来。两种方式不冲突我一般两个都用Python 里 logging 写结构化信息cron 重定向再兜底一层。参数说明levelINFO会把 WARNING 和 ERROR 一并记录。format里的asctime是本地时间如果你的机器是 UTC日志时间会跟北京时间差 8 小时排查时注意换算。部署后第一件验证的事就是cat weather.log看有没有“天气推送成功”这行字如果没有按堆栈第一行去查是哪个接口报错比盲目改时间参数靠谱得多。5. 避坑推送跑起来之后最容易翻车的五个地方脚本跑通只是开始定时任务跑起来之后才是问题爆发的高峰期。这一章列五个真实踩过的坑每个都按“现象 → 原因 → 解决”写省得你走我走过的弯路。5.1 errcode0 但对方没收到先查 openid 和推送位置现象脚本返回errcode: 0日志也写了“发送成功”但女朋友说没收到任何消息。原因有两个。第一模板消息在微信里的展示位置是“服务通知”不是聊天窗口很多人不知道在小程序消息列表里找。第二更常见的是touser填错了 openid。测试号页面关注者列表里有好几行复制时可能复制到另一条记录或者复制了别人的 openid。解决先把自己的 openid 填进去发一次在手机微信里打开“服务通知”确认能收到再把 openid 换成对方的。测试号最多两个关注者够用。如果换成对方后仍然收不到让对方向下拉一下通知栏或者确认她的微信版本不过旧。调试阶段不要同时发给两个人容易互相干扰判断。5.2 47003 参数错误逐个数一遍 keyword现象接口报47003 argument invalid代码看起来完全没问题模板 ID 也是从页面复制的。原因模板里写了 4 个变量data 里只给了 3 个或者某个 key 跟模板里的占位符名字不一致。模板消息的变量校验严格到了死板的程度多一个、少一个、名字差一个字母都会触发 47003。解决把模板内容从测试号页面“模板消息”列表里完整复制出来一行一行数变量。比如模板是早上好今日天气信息 {{keyword1.DATA}} {{keyword2.DATA}} {{keyword3.DATA}} {{remark.DATA}}那data里就必须有keyword1、keyword2、keyword3、remark四个 key一个不多一个不少。占位符必须全大写DATA写成{{keyword1.data}}会被当成普通文本消息发出去就是原样的花括号。大小写不一致同样触发 47003比如代码里写成Keyword1微信不认。5.3 40001 invalid credentialappsecret 与 token 并发现象日志里出现40001 invalid credential有时候是偶发有时候是稳定复现。原因appsecret 复制时带了空格或换行appID 和 appsecret 填反了或者同一套 appID 被两个进程同时使用进程 A 刷新了 token进程 B 手里拿着旧 token 再去调接口就被判无效。解决先在测试号页面重新复制一遍 appsecret确认代码里没有首尾空格。再看是不是同时开了两个定时任务比如系统里既配置了 cron 又配置了 systemd timer两把“枪”同时打token 自然互相打架。token 缓存文件在 3.1 里已经做了过期时间只要保证脚本单实例运行40001 基本能消除。5.4 高德 status 0 和 INVALID_USER_KEYKey 类型选错现象天气接口返回status: 0info字段是INVALID_USER_KEY代码逻辑没问题key 也从控制台复制得很完整。原因高德 Key 的类型创建错了。控制台创建 Key 时如果选了“Web端(JS API)”这个 Key 只能浏览器端配合域名白名单用服务器直接requests.get会被判定为无效调用。解决去高德开放平台“应用管理”重新加一个 Key服务类型选“Web服务”这类 Key 直接服务端调用不校验域名白名单。还有一个隐藏坑city参数填成拼音或城市全名会返回参数类报错换成 6 位 adcode 即可。adcode 粒度可以到区县北京朝阳区是 110105上海浦东新区是 310115精度由你自己控制。5.5 cron 重复执行或没执行绝对路径和 flock 锁现象cron 任务要么一次都不跑要么一天跑两次两条一模一样的天气消息。原因一次都不跑基本是解释器路径不对、时区错了、脚本没有执行权限。跑了两次则比较隐蔽——调度器在分钟边界抖动同一个任务被触发了两次或者系统里同时存在 crontab 和 systemd timer 两个定时源。解决cron 命令里用绝对路径的 python3脚本文件加执行权限chmod x send_weather.py。防重复执行加一把 flock 锁0 8 * * * flock -n /tmp/weather_push.lock -c cd /home/weather /usr/bin/python3 send_weather.py weather.log 21flock -n表示拿不到锁就放弃不排队等待这样即使 cron 重复触发也不会出现两条相同的推送。这个技巧同样适用于电脑上存在多个定时源的情况锁文件路径换一个不冲突的位置即可。6. 进阶带伞预警、失败重试与自检习惯6.1 把天气文案翻译成提醒雨雪大风关键词判断高德返回的 weather 是中文文本判断逻辑可以直接用关键词。在send_template_msg之前加一层逻辑把remark从固定文案变成动态提醒def build_remark(weather: dict) - str: rain_keys [雨, 雪, 雷, 大风] if any(k in weather[weather] for k in rain_keys): return 今天可能有降水或大风出门记得带伞注意安全 return 天气不错保持好心情今天也要开心然后把 3.3 里的remark值换成build_remark(weather)。这个判断能覆盖常见情况“冻雨”“雨夹雪”都包含“雨”能命中“阴天”不命中正好不必瞎提醒。想更精确可以读高德返回的天气编码但对日常推送来说中文关键词已经够用。你还可以把“降温超过 8℃”这类判断加进去逻辑是一样的换成比较daytemp和前一天daytemp的差值即可。6.2 防重复与重试当日标记文件 dry-run 验证发送失败常见原因是 token 临界过期加一层重试能救回一半的漏发import time def send_with_retry(openid, template_id, weather, max_retry3): for attempt in range(max_retry): try: return send_template_msg(openid, template_id, weather) except RuntimeError as exc: if attempt max_retry - 1: raise time.sleep(10)重试只适合偶发失败47003 这类参数错误重试多少次都没用。为了防重复调度发两次再加当日标记发送成功后写一个以日期命名的标记文件脚本启动时先检查存在就直接退出。发成功后记一行日志隔天看日志就知道是不是真的发了。从那以后我每次换测试号、改模板或换电脑都强制先跑一遍 dry-run把城市固定成自己的 adcode直接运行 main确认 token、天气、模板三步日志全绿再挂到 cron 上。这个习惯帮我避免过至少两次“第二天才发现没推”的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表