1. 项目思路拆解:先搞清楚这套流程要解决什么问题
很多做信息采集、行情监控、舆情追踪的朋友,其实都遇到过同一个尴尬场景:人在工位上敲了一天键盘,晚上回家或者第二天一早打开电脑,才发现该抓的数据没抓到、该看的报表没生成、该发的通知没发出去。尤其是那些需要固定频率刷新的数据源,靠手动跑脚本根本撑不住,哪怕你定了闹钟,也总有开会、通勤、临时岔开事情的时候。
这个项目的出发点很朴素:把手里的一个爬虫脚本,从“本地偶尔跑一次”变成“云端定时自动跑”,然后把结果或者异常通知通过邮件送到指定收件箱。之所以选 GitHub Actions 而不是一台长期开机的服务器,核心就三点:免费、配置即代码、上手门槛足够低。GitHub Actions 每个月的免费额度对爬虫这种轻量任务来说非常充裕,而且工作流文件写在仓库里,天然有版本管理,换了电脑、换了环境也能一键复现,不用像以前一样在服务器上装一堆依赖还要担心环境被搞坏。
适合读这篇文章的,是那些已经会写 Python 爬虫、但还不太熟悉 CI/CD 工具链的人;或者对 GitHub Actions 有点概念,但不知道任务计划、密钥管理、邮件发送这几个模块具体怎么拼起来的同学。慢一点没关系,这篇文章会把每一步为什么这么写、哪里有坑、怎么排查都讲清楚,看完你能直接套到自己的项目上去。
2. 关键技术点与方案选型逻辑
2.1 为什么选择 GitHub Actions 而不是自己的服务器
很多人的第一反应是:我有台云服务器,直接 crontab 不就行了?确实,如果你已经有一台稳定运行的服务器,cron 是最直接的方案。但问题在于:不是所有人都有云服务器,也不是每个人的服务器都愿意为了一个爬虫任务长期开机。
GitHub Actions 在这里的核心价值,是把“定时调度”和“执行环境”全部托管了。你的爬虫脚本跑在 GitHub 的虚拟机里,跑完就销毁,不需要你关心系统维护、进程守护、crond 服务是否开启。而且 GitHub Actions 的调度是基于 UTC 时间的 cron 表达式,配好之后到点自动触发,跑完能在网页上直接看日志,失败了还能收到通知,省心程度远超自己搭的定时任务。
另一个优势是隔离性。每跑一次任务,都是在一个全新的虚拟机环境里执行,依赖冲突、脏环境的问题天然不存在。你只需要在 workflow 里把 Python 版本、依赖安装命令写清楚,剩下的交给平台。对于那些对数据敏感度不高、量级也不大的爬虫任务来说,这套方案完全够用。
2.2 邮件发送的两种主流方案对比
爬虫跑完怎么把结果送出去?邮件是最通用、最不用看别人脸色的方式。在实际落地时,有两套方案可以选择,各有各的适用场景。
第一套是标准的 SMTP 方式,也是最贴近底层、兼容性最好的方式。Python 里用smtplib配合email模块,或者直接封装一个yagmail库,把邮件服务器地址、端口、账号、授权码配置好,就能把正文、附件一起发出去。这种方式的优点是只要你有一个邮箱账号(QQ邮箱、网易邮箱等)就可以用,无需额外注册任何云服务;缺点是需要在邮箱后台开启 SMTP 服务并生成专用授权码,对不熟悉邮箱设置的人来说会卡一下。
第二套是利用云服务商的邮件 API,比如 SendGrid、Mailgun 这类专门的邮件发送服务。它们的优势是送达率高、有完整的发送日志、信头规范也做得好,不容易进垃圾箱。缺点是你要去注册账号、配置域名验证、绑定发件人,对于只是“想把爬虫结果发给自己看看”的场景来说,略微重了。
我个人的建议是:如果你只是发给自己的邮箱,或者发给固定几个内部同事,直接走 SMTP 方案就够了,配置量最小,也不依赖第三方服务的免费额度。如果你需要高频发送、对送达率和专业度有要求,再考虑邮件 API 方案。后面讲实操的时候,我会以 SMTP 方案为主展开。
2.3 仓库结构与脚本组织的常规布局
很多新手在布置项目时容易乱,把所有文件堆在一起,workflow 文件乱放,Python 脚本和配置文件混在一起,结果出问题排查半天。这里给一个经过实践检验的标准布局:
project-root/ ├── .github/ │ └── workflows/ │ └── scheduler.yml # 工作流定义文件 ├── src/ │ ├── __init__.py │ ├── crawler.py # 爬虫核心逻辑 │ └── mailer.py # 邮件发送模块 ├── requirements.txt # Python 依赖 └── README.md这样组织的几个理由:.github/workflows是 GitHub Actions 约定的目录,任何 push 到仓库的 workflow 文件会直接被 GitHub 识别并注册;src目录用来放业务代码,把爬虫和邮件逻辑拆分到两个模块,便于后续单独测试和替换;requirements.txt由 workflow 在运行时读取并安装依赖,项目与运行逻辑分离。
如果你的项目只有一个脚本,也可以简化为脚本文件放在根目录、workflow 放在.github/workflows下,但至少保持“代码和配置分离”“爬虫和邮件逻辑分文件”这两个习惯,会让后续维护舒服很多。
3. 实操全流程:从写代码到配置定时任务
3.1 爬虫脚本的核心实现与注意事项
爬虫部分,我用一个通用示例来讲。假设我们要抓取某个公开网站上的行情列表数据,解析出关键字段后保存为 JSON 文件,并在邮件中附带摘要。这里用requests抓取页面,用BeautifulSoup解析 HTML,用json存储结果。
一个简化但完整的爬虫模块crawler.py可以这样写:
import requests import json from bs4 import BeautifulSoup def fetch_and_parse(url, output_path="data.json"): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept-Language": "zh-CN,zh;q=0.9", } try: resp = requests.get(url, headers=headers, timeout=15) resp.raise_for_status() except requests.RequestException as e: log_error(f"请求失败: {e}") return None soup = BeautifulSoup(resp.text, "html.parser") # 这里根据实际页面的 DOM 结构做抽取 items = [] for row in soup.select(".item-row"): title = row.select_one(".title").get_text(strip=True) price = row.select_one(".price").get_text(strip=True) items.append({"title": title, "price": price}) with open(output_path, "w", encoding="utf-8") as f: json.dump(items, f, ensure_ascii=False, indent=2) return items这里有两个值得强调的点:
一是User-Agent不要用默认值。很多网站会对空 UA 或者 Python-requests 默认 UA 直接拒绝访问,设置一个常规浏览器的 UA 能减少很多被拦截的概率。二是一定要加超时和异常捕获。GitHub Actions 的虚拟机网络环境不算特别稳定,万一目标网站响应慢或者临时挂了,脚本要能优雅退出,而不是抛一个堆栈异常导致整个任务失败。脚本里我没写全日志函数,实际项目中建议加入 logging,方便在 workflow 日志里定位问题。
抓取频率也要控制,如果你需要每小时跑一次,目标站点是小网站,建议在脚本里做一次本地缓存比对,只有页面内容变化时才发送邮件,避免每次都是一模一样的邮件轰炸收件人。这个逻辑可以很粗暴地实现:比较本次结果和上次结果,如果无差异,直接跳过发送阶段。
3.2 邮件发送模块的编写与授权码获取
邮件模块mailer.py,我优先推荐用yagmail,因为代码量小、可读性好:
import yagmail def send_email(receiver, subject, body, attachments=None): sender = "your_account@example.com" password = "your_smtp_auth_code" yag = yagmail.SMTP(user=sender, password=password, host="smtp.example.com") yag.send( to=receiver, subject=subject, contents=body, attachments=attachments, )但注意,这里直接把邮箱账号和授权码写死在代码里,是绝对不可取的,原因有两个:第一,仓库一旦是 public 的,密钥等于裸奔,谁都能看到利用你的邮箱发垃圾邮件;第二,就算仓库是 private 的,也存在被泄露的风险。所以正确的做法是在 GitHub 仓库设置里的 Secrets 配置项中存放这些信息,然后在 workflow 中用环境变量的方式读入。
具体操作路径是:仓库页面 -> Settings -> Secrets and variables -> Actions -> New repository secret。依次添加MAIL_ACCOUNT、MAIL_AUTH_CODE、MAIL_RECEIVER等。这样在代码中改成从环境变量读取:
import os import yagmail def send_email(receiver, subject, body): sender = os.environ["MAIL_ACCOUNT"] password = os.environ["MAIL_AUTH_CODE"] yag = yagmail.SMTP(user=sender, password=password, host="smtp.example.com") yag.send(to=receiver, subject=subject, contents=body)关于授权码,以 QQ 邮箱为例(其他邮箱类似),流程是:进入邮箱设置 -> 账户 -> 开启 POP3/SMTP 服务,开启时系统会给你一个十六位的授权码,把那个授权码作为密码填入即可。不要用邮箱本身的登录密码,这是最常踩的坑。
主机名和端口要注意:SMTP 服务商不同,端口会不一样。QQ 邮箱是smtp.qq.com端口 465,网易是smtp.163.com端口 465 或 994。yagmail 默认会给 SSL 加密连接,所以如果连不上,先检查端口是不是 465,再确认是否需要在 host 参数中显式声明。
3.3 Workflow 文件的完整配置与定时逻辑详解
workflow 是整个项目的调度核心,也是大多数人最容易写错的地方。一个完整可用的scheduler.yml文件长这样:
name: Daily Crawler on: workflow_dispatch: schedule: - cron: "0 1 * * *" jobs: crawl-and-notify: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies run: | pip install --upgrade pip pip install -r requirements.txt - name: Run crawler env: MAIL_ACCOUNT: ${{ secrets.MAIL_ACCOUNT }} MAIL_AUTH_CODE: ${{ secrets.MAIL_AUTH_CODE }} MAIL_RECEIVER: ${{ secrets.MAIL_RECEIVER }} run: python src/crawler.py - name: Send notification run: python src/mailer.py重点拆解几个部分:
on字段定义了触发方式。workflow_dispatch允许你在 GitHub 网页上手动点击运行,这个配置对调试非常重要,因为你第一次写完 workflow 后不可能干等定时触发,手动触发是最快的验证方式。schedule字段配置 cron 表达式,格式为标准的五段式:分 时 日 月 周。
jobs下面的runs-on指定运行环境。ubuntu-latest最常用,构建快、软件包全面;如果你的爬虫需要特定系统库,再考虑别的镜像。
steps中,actions/checkout@v4用于拉取仓库代码,actions/setup-python@v5用于指定 Python 版本,然后通过 pip 安装依赖。注意,actions/checkout是必须的,没有这一步,后续的install dependencies、run crawler都会因找不到代码而失败。
env部分的关键作用是把 Secrets 中的值注入到运行环境。代码中通过os.environ读取,整个流程不需要在仓库里出现任何真实密钥。
cron 表达式这块是大多数人最容易懵的地方。GitHub Actions 的调度采用 UTC 时间。也就是说,如果你期望的是每天早上 9 点执行,而你在东八区,实际对应的 UTC 时间是凌晨 1 点,表达式要写成"0 1 * * *"。换算公式很简单:期望北京时间 - 8 小时 = UTC 时间。
3.4 requirements.txt 的锁定策略
依赖锁定这块,有些细节值得单独说。直接用pip install requests beautifulsoup4 yagmail写进 requirements.txt 当然省事,但你没法保证每次运行时安装的都是同一个版本,很可能今天跑得好好的,下个月依赖库升级了一个大版本,接口变动,脚本直接崩了。
更稳的做法是把精确版本号写死,比如:
requests==2.31.0 beautifulsoup4==4.12.2 yagmail==0.15.293版本号怎么确定?在你的本地环境跑一遍pip freeze,把与项目相关的那个几个库版本抄进去就行。锁定版本的好处是,GitHub Actions 每次执行安装的都是指定版本,线上环境和本地开发环境一致,排查问题时候不会出现“本地跑得好好的,一到线上就报错”这类经典尴尬。
如果你担心未来某个依赖有安全漏洞想升级,那就升级后在本地测试通过,再提交新的版本号,流程上完全受控。
4. 定时任务部署后的调参与经验总结
4.1 首次部署的调试顺序建议
很多人第一次配置完就在那等定时触发,结果等了两小时没跑,还以为配置错了。这里分享一个稳妥的调试顺序:先手动触发,再改定时规则。
具体做法是:推送代码后,打开仓库的 Actions 页面,你会看到刚推送的东西已经生成了一个名为Daily Crawler的工作流。右侧有一个Run workflow按钮,点击后选择分支运行。这个操作会立即激活一次任务。你可以用这个方式验证 3.3 节中的 workflow 是否完全跑通,观察每一步日志输出,确认爬虫脚本有没有出错、邮件有没有发送成功。
确认手动触发没问题之后,再检查 cron 设置。GitHub Actions 的调度有个小特点需要注意:cron 的最小单位是分钟,但实际执行可能会因为服务器负载而出现一定延迟,所以如果你的任务要求极高精度的发送时间,这套方案并不适合。常规的信息推送、每日报表,完全没问题。
第一次跑通后,哪怕工作流文件没有改动,每次在仓库里推送任意代码,都会让workflow_dispatch保持可点击状态,但不会因为普通 push 就自动触发定时任务。定时触发是独立于 push 的,只有 cron 满足时才执行。
4.2 常见报错与排查实录
下面几个问题,是我自己实操和帮人调试时遇到频率最高的,整理成表格方便查阅。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 定时任务从不触发 | cron 时区换算错误 | 确认 UTC 时间与本地时间差,调低频率后手动触发验证逻辑 |
| 邮件发送失败,提示 535 认证错误 | 授权码错误或者 SMTP 服务没开 | 重新检查邮箱后台,确认使用的是授权码而非登录密码 |
脚本运行时报ModuleNotFoundError | requirements.txt 遗漏依赖,或版本拼写错误 | 本地pip freeze核对,确认库名大小写 |
| 页面抓取不到数据,返回空列表 | 目标网站 DOM 结构变化,或请求被反爬拦截 | 检查 UA、cookie、页面结构选择器是否失效 |
工作流直接失败,日志显示No such file or directory | checkout 步骤缺失或路径错误 | 确认 workdir 是否正确,文件是否在仓库里 |
| 任务运行时间远超预期 | 脚本未设置最长执行时间,目标站响应慢 | 给每个请求加 timeout,控制等待时间 |
有些日期的潜在问题需要单独强调。周末不少站点只维护或数据不更新,如果你的任务每天固定跑,这会导致一整天的邮件内容与前一天重复。常见的解决方式是加一个去重逻辑,或者把 cron 表达式限定在工作日,例如"0 1 * * 1-5",表示每周一到周五凌晨 1 点执行。
另外,如果 GitHub Actions 的免费额度超出限制,任务会排队不执行。个人项目的爬虫频率控制在每小时一次以内,一般不会触碰用量限制。但在高峰期,免费账户的任务可能等待更久,需要预留一点时间余量。
4.3 权限、安全与合规的补充提醒
可能有人会觉得,爬虫只是个脚本而已,怎么还要考虑安全和合规?实际上这种自动化任务,安全意识必须前置,否则不只是技术问题。
第一,脚本中不要写死任何个人信息,包括邮箱地址、收件人列表。哪怕仓库是 private,也应该通过 Secrets 管理。因为 GitHub 的 fork 机制、协作成员权限都可能导致信息暴露面扩大,密钥放代码里百害无一利。
第二,控制抓取频率。如果你盯着的目标网站访问比较频繁,请把定时任务的间隔放宽,并在脚本里加限速逻辑。这不只是对目标服务器负责,也是在保护你的任务不被封 IP。很多站点有反爬策略,频率过高会被拉黑,届时整个脚本就废了。
第三,明确数据使用边界。爬取的数据应该仅用于个人的学习研究或者内部整理,不要直接对外发布二次加工的敏感信息,这一点每个从业者心里都要有根弦。
4.4 从单任务到多任务的扩展思路
这套方案跑顺之后,你大概率会想接入更多数据源。到时候不一定要把每个爬虫都单独写一个 workflow 文件,可以复用同一条流水线,只是给每个任务传入不同的参数。
我的建议是:把爬虫脚本设计成支持从命令行接收参数,比如第一个参数是数据源名称,第二个参数是目标 URL,在 workflow 的run步骤里用${GITHUB_REF_NAME}或者环境变量区分任务。结合 GitHub Actions 的矩阵构建功能,定义多个目标 URL,让同一个 job 批量跑多组数据,整体维护成本会下降很多。
如果你有更复杂的需求,比如需要把结果存到云存储、推送到其他平台,也可以在这个 workflow 里继续加 steps,思路完全一致。GitHub Actions 的能力远不止跑爬虫和发邮件,但凡你想定时做的自动化操作,都可以用同一套模式去套。
最后再分享一个小技巧:如果你不想每次跑完都去网页上看日志,可以在 workflow 里启用失败通知,让 GitHub Actions 在任务失败时自动发邮件到仓库关联邮箱。在 workflow 文件底部加一个用于异常上报的 job 就行,配合通知类 action 在你定义的if: failure()条件下执行。这样,你的监控系统才真正闭环了,出了问题第一时间就能收到信号,不至于等用户来投诉才发现数据已经断更好几天。