1. 从手动打卡到自动化:一个真实需求的拆解
每天睁眼第一件事就是摸手机、点开某个页面、找到打卡入口、勾选健康状态、提交——这套动作我连续做了大半年,直到某天早上因为睡过头漏了一次,被提醒补报,才下定决心把它彻底自动化掉。这个项目的核心目标很明确:用 GitHub Actions 这个免费的持续集成服务,在每天固定时间自动触发一个脚本,完成健康信息填报和网站签到,全程不需要本地电脑开机,也不需要服务器常驻。
说白了,就是把"人每天手动点一遍"这件事,交给云端的一台临时机器去干。GitHub Actions 对公开仓库完全免费,私有仓库每月也有 2000 分钟的额度,而一次打卡任务通常十几秒就跑完了,一个月撑死消耗不到 10 分钟,成本几乎为零。这套方案适合所有被重复性网页操作困扰的人——不管是每日健康填报、论坛签到、还是各种需要"每天点一下"的积分任务,思路都是通用的。
我把它拆成三个层次来理解:触发层(谁来定时叫醒任务)、执行层(脚本怎么模拟人的操作)、通知层(跑完了怎么告诉我结果)。很多人一上来就写脚本,结果定时不准、登录失效、失败了还不知道,问题全堆在一起。正确的做法是先想清楚这三层各自用什么方案,再动手写代码。
2. 整体方案设计与技术选型
2.1 为什么选 GitHub Actions 而不是其他方案
自动化定时任务的选择其实不少,我对比过几种常见路线,最后选 GitHub Actions 是有明确理由的。
| 方案 | 成本 | 稳定性 | 维护难度 | 是否需要常驻设备 |
|---|---|---|---|---|
| 本地电脑定时任务 | 免费 | 差(关机就断) | 低 | 是 |
| 云服务器 cron | 每月十几到几十元 | 好 | 中 | 否 |
| 树莓派等设备 | 一次性硬件成本 | 中(断电断网就挂) | 中 | 是 |
| GitHub Actions | 免费 | 好 | 低 | 否 |
本地电脑方案最大的问题是"你永远不知道它什么时候会关机或休眠",我试过用系统自带的任务计划,结果出差几天笔记本合盖,任务全断了。云服务器稳定但要花钱,而且为了一个打卡任务养一台机器实在浪费。GitHub Actions 的优势在于:它本身就是为"定时跑一段代码"设计的,仓库里放好脚本和配置文件,剩下的交给平台,你连服务器都不用碰。
注意:GitHub Actions 的定时触发(schedule)在仓库长时间无活动时可能会被平台暂停,这是很多人踩过的坑。解决办法是偶尔手动触发一次,或者让任务本身产生一次提交记录,保持仓库活跃。
2.2 执行层:脚本用什么写
执行层我推荐用Python + requests或者Python + Playwright,具体看目标网站的类型。
如果打卡页面是纯表单提交、接口清晰,用 requests 直接构造 HTTP 请求最轻量,几行代码搞定,运行时间以毫秒计。但现实情况往往是页面有 JavaScript 渲染、有验证码、有动态 token,这时候 requests 就力不从心了,得请出 Playwright 这种能驱动真实浏览器的工具。
我的经验是:先抓包看接口,能用 requests 就别上浏览器。浏览器方案虽然通用,但启动慢(一次要十几秒)、依赖重、在 CI 环境里还容易因为缺少系统库而报错。只有当接口加密复杂到逆向成本过高时,才退而求其次用浏览器模拟。
2.3 通知层:跑完了怎么知道结果
自动化任务最怕的是"静默失败"——你以为它在跑,其实早就挂了。所以通知层不是可选项,是必选项。常见的通知方式有几种:
- 邮件通知:GitHub Actions 自带失败邮件,但只在任务失败时发,成功不发,而且邮件容易被淹没。
- Server 酱 / 推送机器人:国内常用的微信推送方案,配置简单,成功失败都能推。
- 企业协作工具的 Webhook:如果团队在用,可以直接推到群里。
我个人的做法是脚本里主动调用一个推送接口,把执行结果(成功/失败/关键返回值)发出来。这样每天收到一条消息,心里踏实。配置方式通常是在仓库的 Secrets 里存一个推送 key,脚本读取环境变量后发送。
2.4 敏感信息怎么存:Secrets 的正确用法
账号密码、Cookie、推送 key 这些东西绝对不能明文写在代码里,尤其是公开仓库。GitHub 提供了Secrets功能,在仓库的 Settings → Secrets and variables → Actions 里添加,脚本通过${{ secrets.XXX }}读取。
这里有个细节很多人不知道:Secrets 一旦保存就无法再查看原值,只能覆盖。所以第一次配置时一定要确认无误。另外,Secrets 在日志里会被自动打码成***,但如果你在脚本里把它打印出来又被平台识别失败,仍有可能泄露,所以永远不要在脚本里 print 密码。
3. 核心实现细节与实操要点
3.1 抓包分析:搞清楚打卡到底发了什么请求
这是整个项目最关键的一步,也是最容易被跳过的一步。很多人上来就问"脚本怎么写",其实应该先问"这个网站到底是怎么完成打卡的"。
打开浏览器的开发者工具(F12),切到 Network 面板,然后手动完成一次打卡,观察期间发出的请求。你要重点关注这几个信息:
- 请求 URL:打卡接口的地址。
- 请求方法:GET 还是 POST。
- 请求头(Headers):尤其是 Cookie、User-Agent、Referer、Content-Type,有些网站还会校验自定义头。
- 请求体(Payload):POST 请求提交的表单数据或 JSON。
- 响应内容:返回的 JSON 里通常有状态码和提示信息,用来判断是否成功。
我实测下来,大部分健康打卡类页面的接口都比较直白,就是一个 POST 请求带着几个字段。但要注意,有些网站会在请求头里带一个动态生成的 token,这个 token 可能是页面加载时由 JS 算出来的,直接复制会过期。遇到这种情况,要么找到 token 的生成规律用代码复现,要么改用浏览器方案。
提示:抓包时记得勾选"Preserve log"(保留日志),否则页面跳转后请求记录会被清空,你就抓不到关键请求了。
3.2 用 requests 构造请求的完整思路
假设抓包后发现打卡就是一个简单的 POST,那脚本结构大致如下(以下为通用示例,字段名需按实际替换):
import os import requests # 从环境变量读取敏感信息,避免硬编码 cookie = os.environ.get("TARGET_COOKIE") push_key = os.environ.get("PUSH_KEY") url = "https://example.com/api/checkin" headers = { "Cookie": cookie, "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", "Content-Type": "application/json", "Referer": "https://example.com/checkin/page", } payload = { "status": "healthy", "location": "school", } resp = requests.post(url, headers=headers, json=payload, timeout=15) result = resp.json() print("返回状态:", result.get("code"))这段代码有几个要点值得展开。第一,timeout 必须设,否则网络卡住时任务会一直挂着,直到 GitHub Actions 的默认超时(6小时)才结束,白白浪费额度。第二,headers 要尽量还原浏览器,尤其是 User-Agent 和 Referer,很多网站会做基础校验,缺了就直接拒绝。第三,判断成功不能只看 HTTP 状态码,很多接口 HTTP 返回 200 但 JSON 里的 code 是失败的,必须解析响应体确认。
3.3 Cookie 的获取与保鲜问题
Cookie 是这类脚本的命脉,也是最容易失效的部分。手动从浏览器复制 Cookie 能用一段时间,但通常几天到几周就会过期,届时任务会静默失败。
解决思路有两条。一是接受手动更新,把 Cookie 存在 Secrets 里,失效了就去浏览器重新复制一次覆盖。这种方式简单但需要你留意通知,收到失败提醒就去更新。二是自动登录获取 Cookie,脚本里先用账号密码调登录接口,拿到新的 Cookie 再去打卡。后者一劳永逸,但前提是登录接口没有验证码、没有复杂的加密。
我个人的建议是:如果登录流程简单,优先做自动登录;如果登录有验证码等障碍,就老老实实手动维护 Cookie,同时把失败通知做扎实,别让自己蒙在鼓里。
3.4 浏览器方案:Playwright 的取舍
当接口实在搞不定时,Playwright 是可靠的备选。它的思路是启动一个无头浏览器,像真人一样打开页面、填表单、点按钮。
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() context = browser.new_context() page = context.new_page() page.goto("https://example.com/login") page.fill("#username", os.environ["USERNAME"]) page.fill("#password", os.environ["PASSWORD"]) page.click("#submit") page.wait_for_load_state("networkidle") # 后续打卡操作... browser.close()在 GitHub Actions 里跑 Playwright 需要额外安装浏览器依赖,配置稍麻烦,启动也慢。所以我的原则始终是:能用接口就别用浏览器,浏览器是最后的手段。它的价值在于通用性强,页面改版了往往也不用大改脚本,容错率高。
3.5 定时触发的写法与坑
GitHub Actions 的定时用 cron 表达式,写在 workflow 文件里:
on: schedule: - cron: '0 22 * * *' workflow_dispatch:这里有个极其重要的坑:cron 用的是UTC 时间,不是北京时间。北京时间比 UTC 快 8 小时,所以如果你想在早上 6 点打卡,UTC 时间应该是前一天 22 点,也就是0 22 * * *。我第一次配置时没换算,结果任务在下午才跑,白白错过。
另外,GitHub 的定时任务不保证准点执行,高峰期可能延迟几分钟到几十分钟。所以别把时间卡得太死,比如要求"必须 8:00 整打卡",留出缓冲更稳妥。workflow_dispatch这一行是手动触发开关,方便你调试时不用等定时。
注意:定时任务只在默认分支上生效。如果你在别的分支测试,schedule 是不会触发的,必须合并到主分支。
4. 完整实操流程与配置落地
4.1 仓库结构规划
一个清晰的仓库结构能让后续维护省心很多。我习惯这样组织:
repo/ ├── .github/ │ └── workflows/ │ └── checkin.yml # 工作流定义 ├── scripts/ │ └── checkin.py # 打卡脚本 ├── requirements.txt # Python 依赖 └── README.md脚本和配置分离,依赖单独管理,这样以后要加第二个打卡任务时,直接复制一份脚本改改就行,工作流里加一行调用即可。
4.2 工作流文件的完整写法
下面是一个可以直接参考的工作流模板:
name: Daily Checkin on: schedule: - cron: '0 22 * * *' workflow_dispatch: jobs: checkin: runs-on: ubuntu-latest steps: - name: 拉取代码 uses: actions/checkout@v4 - name: 配置 Python 环境 uses: actions/setup-python@v5 with: python-version: '3.11' - name: 安装依赖 run: pip install -r requirements.txt - name: 执行打卡 env: TARGET_COOKIE: ${{ secrets.TARGET_COOKIE }} PUSH_KEY: ${{ secrets.PUSH_KEY }} run: python scripts/checkin.py逐段解释一下。runs-on: ubuntu-latest指定运行环境,用最新的 Ubuntu 就行。actions/checkout把仓库代码拉到运行环境里。setup-python装好指定版本的 Python。依赖安装和脚本执行是核心两步。env块把 Secrets 注入成环境变量,脚本里用os.environ读取。
4.3 参数计算与时间换算实操
时间换算是新手最容易翻车的地方,我专门整理一个对照表,省得每次都要心算:
| 想要的北京时间 | 对应的 UTC 时间 | cron 表达式 |
|---|---|---|
| 06:00 | 前一天 22:00 | 0 22 * * * |
| 08:00 | 00:00 | 0 0 * * * |
| 12:00 | 04:00 | 0 4 * * * |
| 20:00 | 12:00 | 0 12 * * * |
cron 五个字段依次是:分、时、日、月、周。0 22 * * *表示每天 UTC 22:00,也就是北京时间次日 6:00。如果你想工作日才打卡,把周字段改成1-5,即0 22 * * 1-5。
4.4 首次调试的正确姿势
别指望一次就成功,调试是必经环节。我的调试流程是这样的:
- 先在本地把脚本跑通,确认能成功打卡,这一步排除掉脚本本身的逻辑问题。
- 把脚本推到仓库,配置好 Secrets。
- 用
workflow_dispatch手动触发一次,看日志。 - 日志里重点看脚本的 print 输出和报错堆栈。
- 确认成功后,再等定时触发验证一次。
本地跑通这一步千万别省。我见过太多人直接在 Actions 上调试,结果分不清是脚本问题还是环境问题,来回折腾。本地能跑通,说明逻辑没问题,剩下的就是环境配置,排查范围一下子缩小了。
4.5 通知推送的接入
在脚本末尾加上推送逻辑,把结果发出来:
def notify(push_key, title, content): if not push_key: return requests.post( "https://push.example.com/send", data={"title": title, "content": content}, timeout=10, ) # 打卡结束后调用 notify(push_key, "打卡结果", f"状态码: {result.get('code')}, 消息: {result.get('msg')}")推送内容里带上关键返回值,这样你一眼就能看出是真成功还是假成功。有些网站打卡成功和"今日已打卡"返回的 code 不同,脚本里要分别处理,避免重复打卡报错。
5. 常见问题与排查技巧实录
5.1 任务没触发或触发时间不对
这是最高频的问题。排查顺序如下:先确认 workflow 文件在默认分支上;再确认 cron 时间换算是否正确;然后检查仓库是否因为长期无活动被暂停了定时任务。如果都正常但还是不准点,那就是平台调度延迟,属于正常现象,把时间预期放宽即可。
5.2 脚本报错但看不出原因
Actions 的日志其实很详细,关键是会看。展开失败的 step,从下往上看报错堆栈。常见错误类型我整理成表:
| 报错关键词 | 可能原因 | 解决方向 |
|---|---|---|
| ModuleNotFoundError | 依赖没装 | 检查 requirements.txt |
| KeyError | 环境变量没读到 | 检查 Secrets 名称拼写 |
| ConnectionError | 网络问题或地址错误 | 检查 URL 和超时设置 |
| JSONDecodeError | 返回的不是 JSON | 打印原始响应排查 |
| 401 / 403 | Cookie 失效 | 更新 Cookie |
5.3 Cookie 频繁失效怎么办
先判断是 Cookie 本身过期,还是被网站的风控识别了。如果是前者,考虑做自动登录;如果是后者,检查请求头是否完整、请求频率是否过高。我遇到过一次,是因为脚本没带 Referer,被判定为异常请求,补上之后就稳定了。
5.4 打卡显示成功但实际没生效
这种情况通常是脚本判断逻辑太宽松,只要 HTTP 200 就认为成功。解决办法是解析响应体里的业务状态码,只有明确的成功标识才算通过。另外,有些网站打卡后需要二次确认,抓包时要留意是否有后续请求。
5.5 独家避坑经验
分享几个文档里不会写、但实际很关键的技巧。第一,给脚本加详细的日志,每一步都 print 出来,出问题时日志就是你的眼睛。第二,把关键配置做成可开关的,比如调试时打开详细输出,稳定后关掉。第三,定期检查任务是否还在跑,别配完就不管了,我一般每周瞄一眼通知记录。第四,Secrets 更新后要重新触发一次验证,别想当然以为改了就生效。
6. 方案的可扩展性与个人体会
这套框架搭好之后,它的价值远不止打卡这一件事。任何"每天需要点一下"的重复操作,都可以套用同样的模式:抓包分析接口、写脚本模拟请求、用 Actions 定时触发、加通知兜底。我后来陆续把好几个类似的日常任务都迁移到了这个框架上,维护成本极低。
扩展的方向也很多。比如把多个任务合并到一个工作流里顺序执行,减少触发次数;比如把执行结果写入一个文件并提交回仓库,形成历史记录方便回溯;再比如用矩阵策略(matrix)同时跑多个账号。这些都属于锦上添花,核心逻辑始终是那三层:触发、执行、通知。
我个人在实际操作中的体会是,自动化这件事最大的门槛不是写代码,而是耐心地抓包分析和反复调试。很多人卡在第一步就放弃了,其实只要把请求搞清楚,后面的代码都是体力活。另外,一定要对"静默失败"保持警惕,通知层做扎实,比脚本写得多漂亮都重要。最后再分享一个小技巧:把常用的抓包字段和请求头整理成一个模板存起来,下次遇到新任务时直接套,能省下大量重复劳动。