我最近把一套用 RPA-Python 思路写的 Web 巡检脚本搬到了 Netlify 上,做成了一个每天自动执行、结果直接以网页形式展示的测试站点。整个过程其实就 5 步:本地写脚本、固定依赖、接 Netlify、配定时触发、加通知。整套链路落地之后,我再也不用每天手动打开浏览器做回归检查,也不用担心测试报告散落在本地文件里没人看。
这篇文章就围绕这个标题展开——RPA-Python 本身不神秘,它就是用脚本模拟真实用户操作浏览器、填表、点击、断言页面状态;Netlify 作为前端托管平台,恰好能把这种定期执行的“测试机器人”变成一个云上运行的定时任务,顺便把生成的 HTML 报告当作静态站点发布出去。适合已经在用 Selenium、Playwright 写自动化脚本,但还停留在“本机跑跑”阶段的开发者;也适合想给团队搭建一个低成本测试看板,但不想自己维护服务器的同学。下面从头到尾讲清楚,尽量把每个选择和每一步操作背后的原因也讲透。
1. 方案拆解:一个“会自己干活”的测试站点
1.1 这个集成到底解决了什么问题
先说痛点。Web 测试自动化这个事,大多数人走到“本地能跑”就停了。脚本写了一堆,能登录、能点击、能断言,但跑完以后报告扔在本地,过两天就被覆盖;或者依赖本机的 Chrome、Python 环境,换一台电脑就起不来;再想定时跑,就得开一台服务器挂着。这些问题叠加在一起,自动化脚本的价值就打了大折扣。
RPA-Python 的思路在这里刚好补位。RPA(机器人流程自动化)强调的本来是“模拟人操作业务系统”,但它和 Web 自动化测试的本质其实是同一件事:用程序驱动浏览器,完成真实用户会做的动作,再对结果做校验。区别只是业务目标不同——RPA 关注的是流程能不能走通,测试关注的是页面有没有按预期渲染。把这个认知拉通之后,你就发现现有的 Playwright、Selenium 脚本,完全可以套一个“巡检机器人”的壳,当作一个长期运行的线上服务来看待。
Netlify 在这个链路里承担三个角色:第一,它提供一个和 Git 仓库联动的构建环境,每次 push 或调用构建 Hook,都会在干净容器里重新安装依赖、跑脚本;第二,它的构建产物天然就是一个静态网站,测试报告直接发布成可访问的 URL;第三,它允许通过外部定时任务触发构建,这样“每天自动跑一次测试”就不需要你维护任何服务器。
也就是说,这套方案用一个已经存在的托管平台,同时解决了执行环境、展示层、定时调度三个问题。这也是我为什么选它而不是去折腾一台云主机的核心原因——少维护一个东西,长期就少一堆事。
1.2 为什么选择构建触发而不是 Serverless 定时函数
我最初也考虑过用 Netlify 的定时函数(Scheduled Functions)来执行测试逻辑。毕竟那看起来更“原生”,直接在 Functions 里写定时逻辑,好像很省事。但深入了解之后放弃了,原因很实在。
Netlify Functions 底层是函数即服务(FaaS),适合短小轻量的请求处理,运行时长和内存限制都比较严格。常规的 RPA-Python 脚本要启动浏览器、加载页面、模拟点击输入、截图,再等网络响应,一套流程跑下来少说几十秒,复杂业务甚至几分钟。硬塞进函数里,很容易打到超时上限。更麻烦的是,FaaS 环境里想要跑完整 Chromium 无头浏览器,涉及的依赖安装和系统库配置非常别扭,远不如构建环境来得灵活。
反过来看 Netlify 的构建任务,它的定位是“跑构建、出产物”。部署一个静态站点本来就需要完整执行环境,大部分时候还有十几分钟的构建时间窗口。我只不过把构建命令从单纯的eleventy或hugo换成了“先跑 Python 脚本、再输出报告目录”。浏览器依赖(Chromium)在构建环境里可以通过前置脚本一次性装好,脚本跑完以后生成的 HTML 报告直接作为站点发布。构建时间窗口、系统依赖、产物托管三个问题一次全解决。
这是一个很关键的选型判断:不是所有云功能都适合跑长任务,稍微把思路从“执行函数”转向“构建产物”,反而能撬动平台的既有能力。定时触发方面,Netlify 提供了 Build Hooks(构建钩子),给它发一个 POST 请求就能触发一次新构建,配合 GitHub Actions 的 cron 定时任务,调用逻辑非常简单。整套链路没有常驻服务,没有额外计费,成本就非常低。
2. 五步落地:从零到定时运行的完整流程
2.1 第一步:本地写一个可重复执行的 RPA-Python 巡检脚本
本地脚本是整个方案的核心,其他步骤都是围绕它展开的。我的建议是先把“单人可复现”这个目标放在第一位:脚本要能在干净环境里一次跑通,要有明确的退出码,要把结果输出到独立目录,不能依赖本地乱七八糟的特定路径。
技术选型上,我更倾向 Playwright 而不是老牌的 Selenium。原因有三:第一,Playwright 的自动等待机制对页面加载时序更宽容,写出来的脚本不容易因为“元素还没出现就点击”这种问题崩掉;第二,它内置了截图和比较能力,生成报告时非常顺手;第三,无头浏览器启动时的系统依赖安装更标准化,方便后面部署到 Netlify。
下面是我实际使用过的一个基础脚本骨架。
import json import time import os from datetime import datetime from playwright.sync_api import sync_playwright BASE_URL = os.getenv("RPA_BASE_URL", "https://example.com/login") USERNAME = os.getenv("RPA_USERNAME", "demo") PASSWORD = os.getenv("RPA_PASSWORD", "demo") RESULT_DIR = "report" os.makedirs(RESULT_DIR, exist_ok=True) def check_login(page): """模拟真实用户登录,并校验登录后的关键元素""" page.goto(BASE_URL, timeout=30000) page.fill("#username", USERNAME) page.fill("#password", PASSWORD) page.click("button[type=submit]") page.wait_for_selector("text=欢迎回来", timeout=15000) def run_snapshot(): with sync_playwright() as p: browser = p.chromium.launch( headless=True, args=["--disable-dev-shm-usage"] ) page = browser.new_page(viewport={"width": 1440, "height": 900}) result = {"timestamp": datetime.now().isoformat(), "checks": []} try: check_login(page) result["checks"].append({"name": "login", "status": "pass"}) except Exception as exc: result["checks"].append({"name": "login", "status": "fail", "error": str(exc)}) page.screenshot(path=f"{RESULT_DIR}/latest.png", full_page=True) browser.close() with open(f"{RESULT_DIR}/result.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) has_fail = any(item["status"] == "fail" for item in result["checks"]) print(json.dumps(result, ensure_ascii=False, indent=2)) return 1 if has_fail else 0 if __name__ == "__main__": exit_code = run_snapshot() raise SystemExit(exit_code)这段代码做了几件关键的事。
- 通过
os.getenv读取账号密码,不把敏感信息写死在代码里。 - 断言失败时把异常记进结果,而不是让脚本直接中断,这样后面才能生成完整报告。
- 每次运行都用固定文件名
result.json和latest.png,方便集成站点时使用固定链接。 - 最终退出码非 0 即失败,后面 CI 和通知逻辑都要依赖这个退出码。
本地执行验证:
pip install playwright playwright install chromium python run_rpa.py跑通后,当前目录会出现一个report/文件夹,里面有截图和结果 JSON。这一步就完成了。
2.2 第二步:固定依赖、封装入口,让脚本能在陌生环境跑起来
本地能跑不算本事,关键是在陌生容器里也能跑。Netlify 构建环境是一个全新的 Ubuntu 容器,它预装了 Python,但你需要的 Playwright 库和 Chromium 系统依赖不会默认存在。所以第二步就是把这些安装逻辑固化下来。
我习惯把安装命令写成一个setup.sh,然后在构建配置里先执行它,再执行run_rpa.py。
#!/usr/bin/env bash set -e pip install -r requirements.txt python -m playwright install chromium python -m playwright install-deps chromium注意这里的python -m playwright install-deps chromium,它的作用是安装 Chromium 在 Ubuntu 上运行所需的系统库(比如各类libnss3、libatk等)。这一步在本地开发机上通常不需要,因为本地桌面环境已经有了这些库,但在干净的云构建容器里缺了它,浏览器一启动就会报错。我见过很多人卡在这个位置,其实加上这一行就解决了。
requirements.txt也要严格控制版本,不要用pip install playwright这种隐式安装。写死版本的好处是后面排查问题时有据可查。
playwright==1.40.0 requests==2.31.0在仓库根目录放一个.env.example,写清楚脚本运行需要哪些环境变量。
RPA_BASE_URL=https://example.com/login RPA_USERNAME=your_account RPA_PASSWORD=your_password RPA_WEBHOOK_URL=https://your-webhook-url同时必须把.env和report/加进.gitignore,避免本地测试结果被误提交进仓库。这一步看起来琐碎,但它是整个方案能否稳定迁移到线上的基础。很多自动化项目“本地跑得好好的,上云就挂”,根子往往就在依赖和配置的隐性假设没有被显式化。
2.3 第三步:接入 Netlify,把报告目录变成站点
脚本和依赖都准备好之后,就可以在 Netlify 上创建一个新站点,并把它连到代码仓库。设置里最关键的一项就是构建命令和发布目录。
我最终用的netlify.toml配置长这样:
[build] command = "bash setup.sh && python run_rpa.py" publish = "report" [build.environment] PYTHON_VERSION = "3.11"command里先跑环境准备脚本,再跑测试脚本。两条命令用&&连接,前者失败就不会继续跑后者。publish指向report目录,这意味着/latest.png、/result.json会自动变成网站的静态资源。PYTHON_VERSION显式指定 Python 版本,避免容器默认版本和你本地开发版本不一致。
第一次构建很可能会遇到一些奇怪的问题,不用慌。常见的是安装 Chromium 时网络比较慢,构建超时;解决办法是在 Netlify 的构建设置里延长构建超时时间,或者把setup.sh的输出写到日志里观察具体卡在哪一步。第一次构建通过后,你会得到一个形如https://your-site.netlify.app的地址,以后每次构建的结果都会自动发布到这个地址下。
有一个细节值得多说一句:python run_rpa.py的退出码如果非 0,Netlify 会认为“构建失败”,然后站点不会更新。这个行为一开始让我困扰,但后来反而成了好事。因为我可以在脚本里控制退出码:只有全部检查项通过才返回 0,有任何页面异常就返回 1。这样构建结果本身就成了一个“测试是否通过”的信号灯,构建日志里还能直接看到失败原因。如果你希望“失败了也把报告发布出来”,那就要在脚本外层用|| true之类的写法包一下,但我个人不建议这么做,因为失败时保留旧版本站点反而能对上一次正常状态形成对照。
2.4 第四步:用 Build Hook 加定时器,让测试自动跑
到这一步,站点已经能在 push 代码时自动构建、自动跑测试、自动发布报告。但这还只是“持续集成”,不是“定时巡检”。要让脚本每天早上自动跑一次,得靠 Build Hook 配合一个外部定时调度器。
在 Netlify 的站点管理后台找到 Build Hooks(构建钩子)入口,创建一个新 Hook。创建后会得到一个形如https://api.netlify.com/build_hooks/xxxxx的专属地址。任何时候向这个地址发一个 POST 请求,Netlify 就会触发一次新的构建。
外部调度器我选的是代码仓库自带的 Actions 定时任务,因为仓库已经存在,不用引入额外服务。下面是daily-rpa.yml的示例。
name: daily-rpa on: schedule: - cron: "0 22 * * *" workflow_dispatch: jobs: trigger: runs-on: ubuntu-latest steps: - name: Trigger Netlify Build run: | curl -X POST \ -H "Content-Type: application/json" \ -d '{}' \ "${{ secrets.NETLIFY_HOOK_URL }}"schedule使用 UTC 时区的 cron 表达式。如果你希望北京时间每天早上 8 点跑,那对应 UTC 时间就是前一天晚上 22 点,所以0 22 * * *表示每天 22:00 UTC。workflow_dispatch允许你手动触发一次,方便调试。- Hook URL 不要直接写进配置文件,应该放到 Actions 的
Secrets配置里,以NETLIFY_HOOK_URL命名。
定时任务触发后,Netlify 会立刻开始构建,脚本在云端容器里跑一次完整巡检,然后把结果发布成最新的latest.png和result.json。这一套下来,不需要任何人盯着,每天都会有新鲜出炉的测试报告挂到公网上。
2.5 第五步:加通知、留历史,真正“无人值守”
定时执行只是让测试自动跑了,但如果测试失败,你还是要第一时间知道。这就是第五步的逻辑:失败通知和结果留档。
在run_rpa.py的代码里,我加了一个简单的通知函数。脚本跑完以后,如果有失败的检查项,就调用 Webhook 地址把结果推送出来。
import requests def send_notification(result, report_url): webhook = os.getenv("RPA_WEBHOOK_URL") if not webhook: return summary = result.get("summary", {}) text = ( f"RPA巡检结果:\n" f"时间:{result['timestamp']}\n" f"通过:{summary.get('pass', 0)},失败:{summary.get('fail', 0)}\n" f"详情:{report_url}" ) requests.post(webhook, json={"text": text}, timeout=10)Webhook 的具体格式取决于你的消息接收端。我用过办公协作软件的群机器人,也用过邮件发送,本质上都是往一个固定地址 POST 一段 JSON。如果你没有现成的 Webhook,用 SMTP 发邮件也是可行的,只是要多配几个邮件服务器相关的环境变量。注意这个通知函数里requests.post必须放在哨兵异常的try外面或者也做异常保护,避免“通知程序自己挂掉导致主程序退出码异常”这种哭笑不得的问题。
结果留档方面,我建议在站点首页放一个简短的说明页,把latest.png和result.json作为固定链接嵌进去。这样你打开网站一眼就能看到“今天这个系统的登录页面快照长什么样,检查项是否全部通过”。如果你还想保留多天历史,可以把脚本里写入 JSON 的路径改成带日期的文件名,例如result-2024-06-01.json,然后在report目录下自动生成一个index.html作为索引页。Netlify 每次构建会把整个report目录发布出去,历史文件只要提交进构建产物,就会一直保留在网站上。
到这里,“本地脚本 + 云构建 + 定时触发 + 失败通知 + 网页展示”的完整闭环就形成了。后面我就不需要每天都打开电脑跑一遍脚本,只需要在通知里收到异常时,打开最新报告看一眼快照和检查详情,就能判断问题出在哪里。
3. 关键配置与避坑细节
3.1 环境变量管理:账号密码千万别写进仓库
这个坑我踩过一次,说出来都觉得自己蠢。第一次搭站点的时候图省事,直接把测试账号的密码写在run_rpa.py里提交到了仓库。虽然那个仓库是私有的,但任何不小心拿到仓库权限的人都能看到测试系统的密码。而且一旦仓库以后转公开,账号信息等于裸奔。
正确的做法是:脚本里只允许通过os.getenv读取环境变量,然后分别在 Netlify 后台和 GitHub Actions Secrets 里配置对应的变量。Netlify 站点后台有 Environment Variables(环境变量)设置项,你可以在里面添加RPA_USERNAME、RPA_PASSWORD、RPA_BASE_URL等变量,构建时这些变量会自动注入到容器的环境里。本地开发时,则在.env文件里维护一份同样的变量,配合python-dotenv之类的工具读取。
有一点容易漏:Netlify 构建环境在拉取仓库代码时,netlify.toml里的环境变量优先级低于后台配置的变量。如果你在两个地方都配置了同一个变量,以后台的为准。调试时如果发现脚本读到的值和预期不符,优先检查后台变量是否覆盖了配置文件里的默认值。
3.2 无头浏览器的系统依赖和资源限制
Playwright 的playwright install-deps chromium能解决大部分系统库缺失问题,但这不代表万事大吉。Netlify 的构建容器是共享环境,Chromium 跑起来的内存占用通常不小,如果你的页面比较重,可能会出现浏览器崩溃或者截图空白的情况。我在脚本里挂了两个参数来缓解:--disable-dev-shm-usage和--no-sandbox。第一个是让 Chromium 不用共享内存目录(容器里 /dev/shm 往往很小),第二个是避免无头模式下沙箱权限问题。
另外,脚本内部一定要设置合理的超时时间。我在goto、wait_for_selector上都给了 15 到 30 秒的上限,防止某个页面元素迟迟不出现,整个构建任务被拖到 Netlify 的超时阈值。总构建时长建议控制在 10 分钟以内,留有余量,毕竟云构建排队和网络请求本身也有额外耗时。
还有一点值得注意:Playwright 的浏览器执行文件体积不小,第一次构建时安装可能要占掉不少时间和带宽。如果你频繁触发构建,这部分成本是重复计算的。我后来用了一个技巧:把setup.sh里的安装步骤拆分,基础依赖每次都装(很快),浏览器二进制则尽量利用构建缓存。Netlify 支持依赖缓存配置,把~/.cache/ms-playwright加到缓存目录列表里,第二次构建就会明显加快。
3.3 报告链接与文件策略
我在脚本里固定使用latest.png和result.json作为文件名,这样每次构建都会覆盖旧文件,网站上的固定链接一直是“最新状态”。这种策略的好处是链接稳定,你把https://your-site.netlify.app/latest.png发到群里,任何时候点开都是最近一次执行结果。
但如果你有“保留历史、方便对比”的需求,固定文件名就不够用了。我的做法是折中:生成带时间戳的历史快照,同时保留latest系列文件。
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") page.screenshot(path=f"{RESULT_DIR}/snapshot-{timestamp}.png", full_page=True) page.screenshot(path=f"{RESULT_DIR}/latest.png", full_page=True)这样latest.png永远指向最新,而历史文件存放在站点目录中,通过一个index.html列出所有快照。注意 Netlify 并不会自动帮你清理构建产物,历史文件越多,站点体积越大,时间长了会拖慢访问速度。我一般会在脚本里加一个清理逻辑:只保留最近 30 天的快照文件。
4. 常见问题与排查记录
整套方案我跑了几个月,遇到过一些典型问题。整理成表格,方便你直接对照。
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 构建日志显示找不到 Chromium | 漏了playwright install chromium | 在setup.sh中显式安装浏览器二进制 |
| 浏览器启动时提示缺少系统库 | 容器缺少运行 Chromium 的动态依赖 | 补上python -m playwright install-deps chromium |
| 脚本提示读取不到环境变量 | 变量只配置在netlify.toml,但后台有同名变量覆盖 | 检查后台变量,保持单一处维护 |
| 定时任务到点没触发 | cron 时区与预期不一致 | 确认仓库定时任务默认 UTC 时区,换算北京时间 |
| 构建 Hook 请求返回 404 | Hook URL 被删除或站点重置 | 重新创建 Hook,并更新到 Secrets |
| 每次构建都很慢 | 浏览器二进制未缓存 | 配置 Netlify 缓存~/.cache/ms-playwright |
| Git push 后报告是旧的 | 脚本返回非 0,Netlify 视为构建失败未发布 | 查看构建日志,定位脚本断言失败的具体项 |
| 登录环节不稳定,偶发失败 | 页面响应慢或验证码干扰 | 增加等待与重试机制,失败时保存页面快照留证 |
挑几个详细说说。
第一个是定时任务时区的问题。GitHub Actions 的schedule默认按 UTC 时间执行,很多人配置0 8 * * *以为早上 8 点跑,实际跑的时候国内时间已经是下午 4 点。我的调整办法很简单:先把想要的北京时间换算成 UTC 时间,写进 cron,再用workflow_dispatch手动触发验证一次,确认确实能在预期时间附近执行。
第二个是“登录环节不稳定”。这个是 RPA 类项目最常见的烦恼。我的页面登录有验证码,验证码一出现脚本就无能为力。后来我在被测系统允许的前提下,把登录环节进行了测试专用通道改造,绕过了验证码;如果被测系统不支持这种处理,我给脚本加了重试逻辑,第一次失败后等 10 秒重试一次,同时把失败页面截图保存下来,方便人工复核。注意,你的被测系统如果对验证码有强校验,脚本的稳定性会大打折扣,要么调整业务侧策略,要么在报告里明确标注“需要人工介入”。
第三个是关于 Netlify 构建失败不回滚的问题。默认情况下,构建失败后站点会回滚到上一个成功构建,其实这个行为是合理的。但如果你误以为“构建失败等于脚本没跑”,那就看错了。日志里脚本的输出还在,只是退出码非 0 导致发布步骤中止。我排查这类问题时,第一步永远是打开 Netlify 构建日志,搜索result或error关键字,先把脚本的运行结果找出来,再判断是页面异常还是环境问题。
5. 我的体会与下一步可以怎么玩
整套方案稳定运作了小半年,我的核心体会是:RPA-Python 加 Netlify 这件事,真正的价值不是“自动化跑脚本”,而是“把脚本变成了一个谁都能访问的线上服务”。过去测试报告是工程师自己看,现在它变成站点上的一张快照,产品、运营都能打开同一个链接确认系统状态,这种信息透明带来的协作效率提升,比我预期的大得多。
期间也有一个意外收获:因为报告地址固定,我还把/result.json接入了团队内部的数据面板,每天可以自动统计巡检通过率。后期如果想进一步扩展,我觉得有几个方向很值得尝试。一是多站点并行巡检,把不同的被测环境拆成多个 Netlify 站点,或者在一个脚本里按环境循环执行,并在报告里加一个环境维度;二是引入视觉回归,对比相邻两天的页面截图,像素级差异直接标红,这样不仅能发现功能性问题,也能捕捉 UI 样式漂移;三是把结果写入简单的云数据库,做历史趋势分析和巡检通过率报表,让这套小系统慢慢长成一个有数据积累的测试看板。
最后再分享一个实用技巧。如果你不想依赖 GitHub Actions 做定时调度,也可以考虑 Netlify 官方支持的一些外部 cron 服务,或者干脆用一个轻量的边缘函数定时发请求。但无论用哪种触发方式,都建议保留一个“手动触发入口”,我的做法是保留workflow_dispatch和一个单独用来触发构建的命令行脚本,这样任何时候想立即跑一次巡检,点按钮或敲一行命令就行了。整个链路不复杂,但它把“定时、自动、展示、通知”四件事一次性打通了,对个人项目和中小团队来说,性价比非常高。