2026/10/12 19:51:52

用GitHub Actions实现定时爬虫与邮件通知的完整指南

用GitHub Actions实现定时爬虫与邮件通知的完整指南 如果你手里有一个数据采集的小项目需要每天定时跑一次把最新结果发到自己的邮箱你会怎么做我见过很多人第一反应是去云厂商买一台最便宜的服务器装好 Python 环境配一个 crontab再祈祷它安静跑上半年不出事。其实对个人项目、低频爬虫来说这条路又贵又麻烦GitHub Actions 完全可以把这个活整个包下来它负责定时触发、准备执行环境、安装依赖、跑脚本跑完自动销毁容器全程不需要你维护任何一台机器。这篇文章就用一个完整的示例把GitHub Actions 部署爬虫并定时发送邮件这件事讲透——从一个能跑的爬虫代码长什么样到工作流文件怎么写、定时 cron 怎么避坑、SMTP 邮件怎么发、失败日志怎么查再到怎么把它从“能跑”优化成“半年不用管”。适合谁看手上有小爬虫需求、又不想为它养一台服务器的开发者以及刚开始接触 GitHub Actions、想找一个完整落地例子的人。1. 定时任务那么多方案为什么偏偏选 GitHub Actions1.1 先算一笔账传统跑法其实都不太划算我自己最早跑定时爬虫用的是云服务器最低配一年下来几百块成本还要自己处理环境依赖、系统更新、进程守护这些问题。后来试过在本地电脑上挂 crontab电脑必须保持开机一睡眠任务就断出门几天数据就断档。也了解过一些在线定时调用服务但那些更适合“到点请求一个 URL”没法真正执行一个完整的 Python 爬虫、装第三方库、跑完再把结果发邮件。GitHub Actions 的逻辑不太一样。它给你一个真实的 Linux 容器环境里面可以执行任意 shell 命令装依赖、跑 Python、请求外网都没问题。调度由 GitHub 负责触发时间精确到分钟级跑完自动销毁。这相当于你白拿了一台“用完即走”的服务器还不用管运维。对于个人爬虫这种低频、短时、轻量的任务属于杀鸡用牛刀但刚好顺手。1.2 免费额度到底够不够用很多人担心免费额度不够。这里分两种情况如果你的仓库是 public公开仓库GitHub Actions 完全不限执行分钟数如果是 private私有仓库免费额度是每月 2000 分钟。对爬虫场景来说按每天跑一次、每次执行 5 分钟算一个月大约 150 分钟连一个零头都用不完。就算以后加了任务每天跑 10 分钟的工作流月消耗也就 300 分钟距离上限还有很大余量。真正需要注意的反而是并发数和执行时长限制。免费账户对同一仓库的并发任务数有限制但单任务串行跑问题不大每个任务最长执行时间是 6 小时爬虫类任务通常在几分钟内结束远够用。1.3 边界在哪里别把 Actions 当长期服务器Actions 有它的坑位也有它的边界。执行环境每次都是全新启动容器内所有文件在任务结束后全部消失所以你不能像用服务器那样“跑完把数据留在原地”。另外任务的出口 IP 不是固定的目标网站如果对单一 IP 做高频访问限制你就会遇到问题。还有它不适合需要常驻进程、需要固定代理池、需要处理海量数据的场景——那种需求还是老实上服务器。所以我的判断标准很简单任务时长以分钟计、每天跑一两次、结果靠邮件或仓库保存、不想养服务器——这样的需求GitHub Actions 就是最优解。2. 爬虫代码先照顾一下云端执行环境的脾气2.1 数据落盘思路和本地完全不同在本地写爬虫很自然的想法是“抓完数据写进 MySQL 或者 SQLite”以后随时查。但 Actions 的执行环境是临时容器任务结束主机就没了本地文件系统也跟着消失。如果不做处理等于白跑。常用的做法是两条抓取结果通过邮件发出来——这是本文的核心路径原始数据文件回传 GitHub 仓库作为存档下次任务还能读取对比我的习惯是两者结合正文摘要走邮件完整数据提交回仓库。这样即使邮件被清理了仓库里还留着一份完整历史。2.2 请求头、超时和重试云端最容易翻车的三个点云端环境跑爬虫和在公司网络或家里网络跑爬虫最大的区别是出口 IP 往往是云服务商的机房 IP目标站点更容易触发反爬机制。应对手段首先是把请求头伪装成正常浏览器——User-Agent别省最好连Accept-Language、Referer都带上。不然第一次跑就可能收到 403。其次是超时设置。云端网络到目标站之间链路更复杂DNS 解析、连接建立都可能变慢没有超时的话requests会一直挂住直到任务超时被杀。每个requests.get()都要带上timeout参数比如timeout(5, 10)分别表示连接超时 5 秒、读取超时 10 秒。再就是重试。网络抖动是常态简单的重试逻辑能救回大部分偶发失败。我习惯写一个小函数配合tenacity库或者手动循环import time import requests def fetch_page(url, max_retries3): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, } for attempt in range(max_retries): try: resp requests.get(url, headersheaders, timeout(5, 10)) resp.raise_for_status() return resp except requests.RequestException as e: if attempt max_retries - 1: raise time.sleep(2 ** attempt) # 指数退避1秒、2秒、4秒这里指数退避是个小经验遇到短暂抖动直接重试但如果目标站确实在限流快速重试反而更容易被封。退避时间随尝试次数翻倍给了对方窗口期。2.3 异常兜底宁可发一封“失败报告”也不要静默失败真实爬虫最大的敌人是“今天还能跑明天页面结构微调就崩了”。所以脚本里一定要做好异常捕获和兜底单个数据字段解析失败不要整个程序崩掉记录 warning 继续跑整个页面抓取失败fetch_page抛异常后主流程要 catch 住最终不管有没有抓到数据都把“本次运行状态”发一封邮件出去这样做的目的很朴素让失败可感知。邮件里标明“今日抓取异常列表页结构变化”你看到标题就知道要处理了最怕的是任务显示成功但因为解析逻辑错位抓到一堆脏数据。主流程可以这样组织def main(): status 正常 summary_lines [] try: data crawl() summary parse(data) if summary: summary_lines.extend(summary) else: status 异常 summary_lines.append(未解析到有效数据) except Exception as e: status 异常 summary_lines.append(f运行出错: {e}) finally: send_email(subjectf[爬虫日报] {today} {status}, body\n.join(summary_lines))这一步做完爬虫代码部分就稳定了接下来才是本项目的核心怎么让 GitHub Actions 按照你的节奏跑起来。3. 工作流配置把定时、依赖、密钥一次讲透3.1 定时触发cron 的时区陷阱GitHub Actions 的schedule用的是标准 cron 语法但时区是UTC。这是最容易被坑的地方——你想北京时间每天早上 8 点跑写成0 8 * * *实际会变成 UTC 早上 8 点也就是北京时间下午 4 点数据晚了一整个白天。换算方法很简单目标北京时间减 8 小时。比如想要北京时间早上 8 点跑UTC 时间就是凌晨 0 点对应 cron 是0 0 * * *。想要北京时间早上 9 点就是 UTC 1 点对应0 1 * * *。更稳妥的做法配置里同时保留workflow_dispatch手动触发。这样你每次改完代码不用干等定时时刻在 Actions 页面点一下 “Run workflow” 就能立刻验证。3.2 一个可以直接抄的 workflow 文件下面这个文件是我目前项目在用的结构已经跑了大半年基本稳定name: daily-spider on: schedule: - cron: 0 0 * * * # UTC 0 点即北京时间早上 8 点 workflow_dispatch: # 支持手动触发方便调试 jobs: run-spider: runs-on: ubuntu-latest steps: - name: 拉取代码 uses: actions/checkoutv4 - name: 配置 Python uses: actions/setup-pythonv5 with: python-version: 3.11 cache: pip - name: 安装依赖 run: pip install -r requirements.txt - name: 运行爬虫并发送邮件 run: python main.py env: SMTP_SERVER: ${{ secrets.SMTP_SERVER }} SMTP_PORT: ${{ secrets.SMTP_PORT }} SMTP_USER: ${{ secrets.SMTP_USER }} SMTP_PASS: ${{ secrets.SMTP_PASS }} MAIL_TO: ${{ secrets.MAIL_TO }}逐个解释一下每个 step 的作用。actions/checkout把仓库代码拉到执行环境里没有它后面的命令什么都跑不了。actions/setup-python负责装指定版本的 Python我这里加了cache: pip它会自动缓存 pip 依赖第二次运行速度会快很多。pip install -r requirements.txt安装依赖注意这份 requirements.txt 要提前准备好列出requests、beautifulsoup4这些库。最后一步run: python main.py就是真正干活的核心。env里的这些变量不会写入代码而是从 GitHub Secrets 里读取——这就解决了密码安全的问题。3.3 secrets邮箱密码绝不能出现在代码里如果你把 SMTP 密码写进 Python 文件再推到 GitHub哪怕仓库是私有的也存在泄露风险。正确做法是使用 GitHub 的Secrets功能。在仓库页面点 Settings → Secrets and variables → Actions → New repository secret把SMTP_PASS这类敏感信息填进去。之后在 workflow 里通过${{ secrets.SMTP_PASS }}引用。这样做的安全性在于Secrets 内容不会显示在日志里也不会被下载到执行环境之外GitHub 会自动对输出中的密钥做脱敏。就算 workflow 文件本身是公开的别人也只能看到${{ secrets.SMTP_PASS }}这个变量引用拿不到真实密码。在 Python 端读取环境变量用的是标准库os.environimport os SMTP_SERVER os.environ[SMTP_SERVER] SMTP_PORT int(os.environ[SMTP_PORT]) SMTP_USER os.environ[SMTP_USER] SMTP_PASS os.environ[SMTP_PASS] MAIL_TO os.environ[MAIL_TO]有个小坑要提醒如果你加了一个新的 secret需要重新运行一次 workflow 才能生效而且 secret 设置成功后 GitHub 不会给你任何可视化提示你自己心里记住即可。3.4 首次运行前要做的三个检查第一次跑 Actions 工作流最容易出问题反而不是代码本身而是这些前置条件代码必须已经 push。仓库完全空白时Actions 标签页里什么都看不到。先 push 一次让 GitHub 识别到这个仓库。确认 workflow 文件路径。文件必须放在.github/workflows/目录下文件名任意但后缀必须是.yml或.yaml。位置不对GitHub 根本不会解析。先手动触发一次。点开仓库的 Actions 标签页选中左侧的daily-spider右侧会出现 “Run workflow” 按钮点一下确认能跑通再等定时调度。别一上来就改 cron 然后干等第二天。完成这三步工作流基本就活了。接下来关注邮件模块本身——这也是很多人第一次跑通后在收件箱里找不到邮件或者邮件进了垃圾箱的真正原因。4. 邮件发送模块与首次排错smtplib 和日志阅读4.1 用 smtplib 发一封带正文的邮件Python 标准库smtplib配合email模块几行代码就能发信import smtplib from email.mime.text import MIMEText from email.header import Header from datetime import date def send_email(subject, body): msg MIMEText(body, plain, utf-8) msg[Subject] Header(subject, utf-8) msg[From] SMTP_USER msg[To] MAIL_TO with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server: server.login(SMTP_USER, SMTP_PASS) server.sendmail(SMTP_USER, [MAIL_TO], msg.as_string())这里我直接用了SMTP_SSL。大部分邮箱服务器的 SSL 端口是 465比如 QQ 邮箱和 163 邮箱都支持。如果你用的服务商只提供 587 端口那对应的是 STARTTLS要用下面的写法with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server: server.starttls() server.login(SMTP_USER, SMTP_PASS) server.sendmail(SMTP_USER, [MAIL_TO], msg.as_string())两种方式别混否则会报SMTPNotSupportedError或者ssl.SSLError。4.2 授权码、端口号和“进垃圾箱”问题用 QQ 邮箱或 163 邮箱发信密码不是你的登录密码而是开启 SMTP 服务后生成的授权码。以 QQ 邮箱为例在邮箱设置里开启 SMTP会给你一串 16 位授权码把它填到 secrets 的SMTP_PASS里。很多人失败就是因为直接填了网页登录密码。邮件发出去却找不到大概率是进了垃圾箱。原因很直接From地址是个人邮箱没有配置 SPF/DKIM 域名校验内容又带着链接容易被判定为营销邮件。经验做法是标题写得朴素别带“特价”“免费”“点击链接”这类敏感词正文尽量只放纯文本不放 HTML 里的复杂样式定期检查垃圾箱如果连续几次都在那里把发件地址加入白名单4.3 失败日志怎么看Actions 页面是最直接的调试窗口如果工作流运行失败打开仓库的 Actions 标签页能看到每次运行的记录列表。点进失败的那一条展开run-spiderjob再点开报错的 step终端日志就会展示在眼前。这里没有 scp、没有 SSH 登录所有信息都在页面上。你的 Python 脚本里要习惯用print()输出关键状态比如“开始抓取”“抓取到第几页”“邮件已发送”。这些都会出现在日志里是你唯一的调试线索。另外工作流运行失败时GitHub 会自动往账号绑定的邮箱发一封失败通知——这意味着即使脚本挂了你也会收到 GitHub 的通知邮件这本身就是一道免费的异常告警。有一个细节我踩过坑如果你在 Python 里用os.environ读取某个 secret而忘记在 workflow 的env里传递脚本会直接抛KeyError但是 Actions 日志里的报错信息非常克制只会显示KeyError: SMTP_PASS。看到这种报错第一反应不是去改代码而是回头检查 workflow 文件的env段是不是漏了变量。5. 从能跑到跑得稳去重、限速与异常通知的迭代思路5.1 别让相同内容天天轰炸收件箱爬虫第一次跑通后很快会遇到另一个烦恼每天收到的邮件内容完全一样因为目标页面根本没更新。看着收件箱里几十封雷同邮件你会觉得这个自动化系统有点蠢。解决办法是做一个“内容变化才发送”的判断。思路很简单把上次抓到的关键数据比如最新一条记录的标题存到仓库的一个文件里本次抓完和上次对比没有变化就只在日志里输出一行“内容未更新跳过发送”有变化才触发邮件。这样收件箱里每一封邮件都有实际价值。5.2 把历史数据回传仓库形成闭环如果想把原始数据积累下来最好让 Actions 在每次跑完后把数据文件提交回仓库。下面这段 shell 步骤可以追加在工作流末尾- name: 提交数据回仓库 run: | git config user.name github-actions git config user.email actionsgithub.com git add data/ git diff --quiet git diff --cached --quiet || git commit -m daily data update git push注意最后一行用了||做判断没有变更时不生成空提交否则每天都会多一条无意义的 commit。这种做法的好处是你可以在任何时间翻看仓库里的历史数据文件等于建立了一个简单的数据仓库。配合 GitHub 的版本历史还能看到每天数据的变化轨迹。5.3 限速、随机延迟与运行时长的自我保护无论目标站点大小都不希望你的脚本把它抓崩。爬虫代码里加入随机的休眠时间是基本礼貌import random import time time.sleep(random.uniform(1, 3)) # 每次请求之间随机休息 1~3 秒对页面列表的抓取最好控制总页数上限比如只抓前 3 页满足日报需求即可。Actions 任务虽然有 6 小时的执行上限但你不希望一个异常页面让任务挂 2 小时所以脚本内部可以用signal模块设置超时保护或者把单次运行逻辑包成带timeout的调用。我个人的经验是整个任务理应 3 分钟内完成如果超过 10 分钟大概率是哪里出了问题还不如让它快速失败触发 GitHub 的失败通知邮箱你再登录日志看原因。5.4 给脚本加上自我检查状态码与空结果最后再分享一个很值的小优化每个步骤都检查产出。抓取列表页后确认解析出的条目数大于 0构造邮件前确认 body 非空发送前确认MAIL_TO不是你的默认值这些检查看似幼稚但爬虫类任务的失败模式恰恰就是“看似成功、实则空转”——HTTP 200但页面结构变了解析出来一个空列表。程序没有报错你也收不到邮件因为逻辑上还在正常走但数据就是没有进来。加一个显式的空结果校验让脚本在这种场景下主动抛异常才能触发失败通知进入你的视野。写在最后最后分享两个我在实际项目中特别觉得值的小技巧。第一个workflow_dispatch一定要加。它能让你在改完代码后不用干等定时时间手动点一下就能立刻验证这个体验差异非常大尤其在你频繁调解析规则的那几天。第二个邮件标题里带上日期和运行状态例如[爬虫日报] 2025-06-01 正常。这样收件箱里扫一眼就知道今天任务到底成没成都不需要打开正文。一旦看到“异常”就立刻去查日志看到“正常”就不用搭理它。跑定时爬虫这件事真正难的不是把第一封邮件发出去而是让它半年都不出问题地帮你守着那个数据源。我的体会是与其一上来就买台服务器不如先让 GitHub Actions 替你干上一个月数据稳定了再认真评估要不要上更重的方案。反正从目前跑下来的情况看这笔账怎么算都是划算的。