Python定时任务+微信公众号天气推送小工具

发布时间:2026/10/29 23:46:45
Python定时任务+微信公众号天气推送小工具 简介围绕「微信公众号每日定时推送天气与纪念日提醒」的自动化小项目这份教程资源面向有少量编程基础、希望亲手为伴侣制作关怀工具的开发入门者与爱好者。包内提供 GitHub Actions 每日自动运行的线上版本配合消息模板可直接替换内容涵盖最新完整版、简化版、星座版与 PHP 版等多种形态选择空间较大。压缩包共 5 个文件、约 5KB以 py 主程序、yml 工作流配置、txt 参数配置与 md 说明文档为主分别承担业务逻辑、定时任务调度、密钥与推送参数填写、部署指引等职责结构精简便于逐行阅读与二次修改。目前已有 7698 人学习下载。读者可从中获得一套可跑通的推送脚本与消息模板理解定时任务、API 调用与参数配置之间的关系并借助配置文件快速切换天气源与推送内容减少从零摸索的成本。1. 天气推送这件事本质是四个小零件串起来去年冬天某个周一早上七点她的手机弹出一条消息抬头是她所在城市正文写着“多云转小雨18~24℃东南风 2 级出门带伞”末尾还多了一句“你上周说这两天要体检别喝豆浆”。她截图发过来问是不是我搞的我说是。这就是一个微信公众号天气推送小工具能做到的全部——定时器准点跑、天气接口拉数据、模板消息塞进微信对话框三个动作代码不到三百行。它不需要买服务器白嫖额度就能长期活着。适合会一点 Python、想让公众号干点私活的人也适合拿它练“定时任务 开放接口 消息下发”这条完整链路的人。下面按我拆解的顺序原样走一遍先看它由哪几块拼成再一步步跑通然后抓出最容易翻车的地方。2. 架构与技术选型一条消息背后的四段链路2.1 从触发到送达拆成四个可以独立替换的环节很多人第一次做这类推送喜欢写一个大函数从头撸到尾结果天气接口一换、消息格式一改整段代码就得重写。我习惯按“谁触发、谁供数、谁渲染、谁发送”切成四段每段留一个明确的输入输出契约后面想换成邮件、钉钉或者短信只动最后一段就行。环节干的事常见方案这里的选型触发到点启动程序服务器 cron、云函数定时触发器、流水线定时任务流水线定时任务免运维取数拉当天/未来三天天气各家天气开放接口一个走 HTTP JSON 的天气服务渲染把字段拼成消息体模板消息、订阅通知、客服消息公众号模板消息送达调接口推给指定用户公众号 / 小程序订阅公众号靠 openid 定位收件人这四个环节里只有“送达”是强绑定公众号的其余三个都能复用。我一直觉得这种切法是这类小项目最值钱的地方——它让你后面加功能时不用推倒重来。2.2 账号类型直接决定你能发哪种消息这是新手最容易忽略、也最容易白干一天的地方。公众号的消息能力跟账号类型强相关订阅号每天能群发一次但模板消息这类“服务通知”能力受限服务号能力完整可需要主体认证而个人开发者最省事的路子是用测试号先把链路跑通。测试号的好处是模板消息几乎随便加、模板内容自己定不用等审核扫码就能拿到一个可用的 openid。血泪经验是先用测试号把“取数 → 渲染 → 发送”全跑通再考虑要不要迁到正式号。因为正式号的模板字段是固定的你得先知道自己需要哪几个字段再去挑模板顺序反了就得反复改结构。常见做法是维护一份配置表把 appid、secret、模板 ID、收件人 openid 全放环境变量里代码里只读不写死。这样测试号和正式号之间切换改的只是运行环境不是源码。2.3 目录结构小项目也要有边界项目不大但我不建议所有代码塞一个文件。按职责切一层后面加人多城市、加纪念日倒计时才不至于乱。weather-push/ ├── app/ │ ├── config.py # 环境变量集中读取唯一配置入口 │ ├── weather.py # 取数 字段归一化 │ ├── wechat.py # token 管理 消息发送 │ ├── template.py # 消息体组装与文案规则 │ └── storage.py # token / 发送记录的持久化 ├── scripts/ │ └── run_once.py # 一次完整执行的入口便于手动干跑 ├── .github/workflows/ │ └── daily.yml # 定时触发配置 ├── requirements.txt └── README.mdrun_once.py这个文件值得单独说。它把“一次推送”封装成可手动执行的命令本地python -m scripts.run_once就能跑不用等到第二天早上七点看结果。调试期我几乎全靠它流水线只是把这个命令换个地方执行而已。3. 跑通全流程从环境变量到手机弹出第一条消息3.1 把凭证收进配置层先把所有会变的东西抽到环境变量。这一步看起来啰嗦但它决定了你后面能不能安全地把代码传到公开仓库——密钥写死在源码里一旦推上去就得全部重置。# app/config.py import os from dataclasses import dataclass dataclass(frozenTrue) class Settings: app_id: str os.getenv(WX_APP_ID, ) app_secret: str os.getenv(WX_APP_SECRET, ) template_id: str os.getenv(WX_TEMPLATE_ID, ) openid: str os.getenv(WX_OPENID, ) city: str os.getenv(WEATHER_CITY, 101010100) # 城市编码按服务商规则填 weather_key: str os.getenv(WEATHER_API_KEY, ) weather_host: str os.getenv(WEATHER_API_HOST, ) token_cache: str os.getenv(TOKEN_CACHE_FILE, .cache/token.json) settings Settings()用dataclass(frozenTrue)是为了让它一旦实例化就不可改避免某个模块顺手把settings.openid覆盖掉造成“发给了错误的人”这种玄学问题。城市用编码而不是中文名是因为多数天气接口对中文名的匹配是模糊的同名地名会被解析到别的区域。3.2 取数与字段归一化接口返回的原始 JSON 通常字段很多、命名风格各异直接拿去拼消息会让模板层跟服务商强绑定。加一层normalize把需要的东西挑出来重命名后面换服务商只改这一个函数。# app/weather.py import requests from app.config import settings def fetch_weather(city: str) - dict: url fhttps://{settings.weather_host}/v7/weather/3d params {location: city, key: settings.weather_key} resp requests.get(url, paramsparams, timeout8) resp.raise_for_status() data resp.json() if str(data.get(code)) ! 200: raise RuntimeError(f天气接口返回异常: {data.get(code)}) return normalize(data) def normalize(raw: dict) - dict: today raw[daily][0] # 第 0 项是今天 return { city: raw.get(city) or settings.city, date: today[fxDate], text_day: today[textDay], text_night: today[textNight], temp_min: int(today[tempMin]), temp_max: int(today[tempMax]), humidity: today[humidity], wind: f{today[windDirDay]}{today[windScaleDay]}级, }timeout8是必须的。定时任务跑在别人的机器上网络抖动时没有超时的请求会一直挂着直到整个任务被判超时失败日志里还看不出到底卡在哪。raise_for_status()负责把 4xx/5xx 变成异常业务层的code判断负责把“请求成功但业务失败”也拦下来——这两种失败在日志里必须能区分开不然排查时全是黑匣子。3.3 access_token 的取用与消息发送公众号的接口凭证有有效期每次发送前都重新取一遍会在高频调用时被限流完全不缓存又容易在关键时刻拿到过期值。我的做法是落一份带过期时间的本地缓存。# app/wechat.py import json, os, time, requests from app.config import settings API https://api.weixin.qq.com def _load_cache() - dict: if not os.path.exists(settings.token_cache): return {} with open(settings.token_cache, encodingutf-8) as f: return json.load(f) def _save_cache(obj: dict) - None: os.makedirs(os.path.dirname(settings.token_cache), exist_okTrue) with open(settings.token_cache, w, encodingutf-8) as f: json.dump(obj, f) def get_access_token(force: bool False) - str: cache _load_cache() # 提前 5 分钟过期避免边界时刻拿到已失效的 token if not force and cache.get(expires_at, 0) time.time() 300: return cache[access_token] resp requests.get(f{API}/cgi-bin/token, params{ grant_type: client_credential, appid: settings.app_id, secret: settings.app_secret, }, timeout8) data resp.json() if access_token not in data: raise RuntimeError(f取 token 失败: {data}) _save_cache({ access_token: data[access_token], expires_at: time.time() data[expires_in], }) return data[access_token]发送函数里带一次强制刷新重试是因为 token 可能被别处比如你手动调接口顶掉这时候缓存里的值虽然没过期但已经失效了。# app/wechat.py 续 def send_template(payload: dict) - dict: url f{API}/cgi-bin/message/template/send token get_access_token() data requests.post(url, params{access_token: token}, jsonpayload, timeout8).json() if data.get(errcode) 40001: # 凭证失效刷新后重试一次 token get_access_token(forceTrue) data requests.post(url, params{access_token: token}, jsonpayload, timeout8).json() return data参数上touser是收件人 openidtemplate_id是模板编号data里的每个 key 必须和模板里定义的字段名完全一致url可选点了消息会跳转。3.4 消息体组装把数字翻译成人话模板消息本身只是个填空题真正决定体验的是文案规则。我把它单独放一层方便按天气条件改语气。# app/template.py def make_tip(w: dict) - str: tips [] if 雨 in w[text_day] or 雨 in w[text_night]: tips.append(出门带伞) if w[temp_max] - w[temp_min] 10: tips.append(早晚温差大备件外套) if w[temp_max] 32: tips.append(注意补水) if not tips: tips.append(今天适合出门走走) return .join(tips) def build_payload(w: dict) - dict: from app.config import settings return { touser: settings.openid, template_id: settings.template_id, data: { date: {value: w[date]}, city: {value: w[city]}, weather: {value: f{w[text_day]}转{w[text_night]}}, temp: {value: f{w[temp_min]}~{w[temp_max]}℃}, wind: {value: w[wind]}, tip: {value: make_tip(w)}, }, }safe之外别自作聪明加富文本模板消息的 value 会原样渲染塞 HTML 只会显示成一堆尖括号。3.5 挂上定时任务最后一环是让它每天自动跑。用流水线的定时触发器最省事不用维护机器。# .github/workflows/daily.yml name: daily-weather-push on: schedule: - cron: 0 23 * * * # UTC 23:00 北京时间次日 07:00 workflow_dispatch: # 保留手动触发方便调试 jobs: push: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 cache: pip - uses: actions/cachev4 with: path: .cache key: token-cache restore-keys: token-cache - run: pip install -r requirements.txt - run: python -m scripts.run_once env: WX_APP_ID: ${{ secrets.WX_APP_ID }} WX_APP_SECRET: ${{ secrets.WX_APP_SECRET }} WX_TEMPLATE_ID: ${{ secrets.WX_TEMPLATE_ID }} WX_OPENID: ${{ secrets.WX_OPENID }} WEATHER_CITY: ${{ secrets.WEATHER_CITY }} WEATHER_API_KEY: ${{ secrets.WEATHER_API_KEY }} WEATHER_API_HOST: ${{ secrets.WEATHER_API_HOST }}cron用的是 UTC这一点坑了我一次——按本地时间写 7 点结果实际在下午三点推。缓存那一步是为了让 token 跨次复用不然每天都要重新取一次。4. 核心模块拆解几个决定成败的细节4.1 token 缓存放在哪决定了它会不会重复推送文件缓存最简单但在流水线环境里有个前提运行环境必须能把文件留下来。如果每次都是干净容器、又没有缓存步骤那这份缓存等于没有每天都重新取 token。这本身不算错只是浪费调用真正麻烦的是多个任务并发跑时各自取一次 token先取的会被后取的顶掉于是出现“刚刚还好好的突然 40001”。我一般会把 token 的获取收敛成单入口并在异常信息里带上errcode和errmsg。日志里只写“发送失败”是没用的你得知道是凭证问题、参数问题还是用户没关注。4.2 模板字段映射名字对不上就是 47003模板消息最常见的报错是 47003意思是参数不匹配。原因几乎都是data里的 key 跟模板里定义的字段名差了哪怕一个字符。稳妥的做法是先在测试号里把模板字段定下来把 key 抄进代码再去做发送。模板字段含义示例值注意事项date日期2025-03-18别用带时分秒的格式city城市杭州太长会被截断weather天气多云转小雨昼夜拼接控制长度temp温度区间18~24℃用~不用-wind风力东南风2级不要写成数字tip提醒语出门带伞别超过一行字段值长度其实有上限超了会被静默截断你看到的消息就少半句。我一般把 tip 控制在 20 字以内。4.3 文案规则别让模板变成复读机同一个模板天天发很容易被忽略。加一点条件判断让它每天说的话不一样效果差别很大。除了天气还可以塞进“距离某个日子还有几天”“今天是周几”这类信息。这部分不建议搞太复杂规则式判断就够硬上生成模型反而容易输出不合时宜的内容。4.4 发送记录出问题时唯一的后悔药哪怕不建数据库也建议每天落一行记录日期、城市、消息体、返回码。有一次我改了文案规则结果连续三天都没收到消息翻日志才发现是tip字段超长导致整条消息被拒而我那几天根本没看返回码——因为脚本退出码是 0。# app/storage.py import json, os, datetime def log_send(payload: dict, result: dict, path: str logs/send.log) - None: os.makedirs(os.path.dirname(path), exist_okTrue) line { ts: datetime.datetime.now().isoformat(timespecseconds), city: payload[data][city][value], errcode: result.get(errcode, 0), errmsg: result.get(errmsg, ok), } with open(path, a, encodingutf-8) as f: f.write(json.dumps(line, ensure_asciiFalse) \n)关键是要在run_once.py里判断errcode非 0 就sys.exit(1)。不然流水线显示绿色成功你却在那边纳闷为什么手机没响。5. 避坑与排查几个真实翻过的车5.1 现象流水线显示成功手机没收到原因发送接口返回了业务错误码但脚本没检查进程正常退出。公众号的接口 HTTP 状态码通常是 200错误全藏在响应体的errcode里。解决发送后立刻判断errcode非 0 打印完整响应并退出非零码。同时把返回体写进日志文件方便事后翻。5.2 现象报 40001刷新 token 也没用原因多半是 appid 和 secret 不匹配或者 secret 被重置过。还有一种情况是同一套凭证被两处代码同时刷新互相顶掉。解决先确认环境变量里填的 secret 是当前生效的那一份再确认只有一处代码在取 token。如果是多任务并发把取 token 和发送放在同一个任务里别拆成两步。5.3 现象定时任务在错误的时间触发原因cron表达式按 UTC 计算本地时间写进去会差 8 小时。另外流水线的定时触发本身有延迟高峰期可能晚十几分钟。解决换算成 UTC 再写并在注释里标明对应的本地时间。对时间精度要求高的场景比如必须七点整这套方案不合适得换常驻机器加系统级定时。5.4 现象连续几天推送内容都一样原因天气接口的响应被运行环境缓存了或者代码里的日期取的是容器时区。解决请求时带上时间戳参数或用Cache-Control: no-cache日期显式用目标时区计算别依赖datetime.now()的默认时区。5.5 现象迁移到正式号后全部报错原因测试号的模板字段可以自由定义正式号的模板是平台给定的字段名和数量都不一样。解决先在正式号里挑模板把字段名抄下来改build_payload里的 key再跑一次手动触发验证。别直接改完就等第二天。6. 二次开发把它改成你自己的每日简报跑到这一步其实你已经有了一个“定时取数 消息下发”的骨架天气只是最顺手的一个数据源。我后来在这个骨架上做了几件事值得说一说。第一件是多收件人。把openid从单个值改成列表循环发送每次之间加 200 毫秒间隔避免触发频率限制。列表可以放配置文件里也可以放一张简单的表。如果要给不同的人推不同城市就把city也一起存进去build_payload多接一个参数。第二件是条件推送。不是每天都值得打扰别人可以加一层判断只有下雨、温差超过 10 度、或者空气质量差的时候才发。这需要把“是否推送”的逻辑从发送层里抽出来单独写一个should_push(w)函数返回布尔值加原因。这样做的好处是你可以先干跑一周看看到底会触发几次再决定阈值。第三件是加被动回复。公众号除了主动推还能在用户发消息时回复。加一个简单的关键词解析“天气”回当天天气“明天”回明天的“倒计时”回距离某个日子还有几天。这块要注意的是接收消息需要配置服务器地址并做签名校验比主动推送麻烦不少。我的建议是先用主动推送把体验跑顺再考虑被动回复。第四件是换存储。文件日志适合单人使用多人多城市之后就该上数据库了。最省事的是一张发送记录表加一张用户偏好表字段不用多够查就行。改造完怎么验证我习惯做两件事一是干跑模式加一个DRY_RUN1的环境变量走到发送那一步就打印消息体不真发二是构造异常输入把城市编码改成不存在的值、把 token 手动改坏看程序是不是按预期报错而不是静默成功。这两步花不了十分钟但能省掉很多第二天早上的困惑。有个教训我记了很久最早那版我把所有逻辑写在一个脚本里token 缓存、文案规则、发送全糊在一起。后来想加个“下雨才提醒”改了半小时发现改错了地方把温度判断的条件也带崩了连续两天推了错误的提醒。从那以后我每次给这类小项目加功能都强制先把要改的那层单独跑一遍确认输入输出对得上再合进去。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询