先聊个背景。前阵子有个运营团队找到我他们每天要往内容中台发布好几拨素材每拨还得分发到不同栏目和账号下发完之后还要截图确认发布状态是否正确。整个链路看起来特别常规登录、填标题、传附件、点提交、刷新列表确认单次不到十分钟。但放大到每周几十次操作、多个账号轮换就变成了一个非常磨人的重复劳动而且越到后面越容易出错——不是标题贴错了就是附件版本传旧了。这个痛点直接催生了 Py-RPA 项目用 Python 把“自动发布”和“发布后的测试”串成一条自动化流水线让脚本替人完成登录、编辑、上传、提交、验证的全过程并在发布结束后自动核对结果、留存截图、输出报告。这篇文章不是泛泛讲 RPA 概念而是完整复盘我从需求分析、技术选型、代码实现到稳定性排错的整个过程包含具体的代码写法、踩坑经过和排查思路给正准备做类似自动化项目的朋友一份可以“抄作业”的实战参考。1. 这个项目到底想解决什么手动发布的隐性成本我们不说“降本增效”这种空话先把现实里的浪费拆开看。手动发布这件事最大的成本不是点那几下鼠标而是每一步都依赖人的注意力而注意力是会波动的。1.1 发布加测试这套流程原本长什么样那个团队的日常工作是这样循环的登录内容中台的后台个别账号还会碰上短信二次验证。进入“新建发布”页面逐项填写标题、分类、摘要、关键字。上传附件包盯着页面上传进度条走完。点击提交发布等系统弹出“发布成功”的提示。切到前台列表页或详情页核对这条内容是否真的可见、字段是否一致、附件能否下载。看起来只有五步但每一步都有隐藏成本。第二步骤里如果一次要发十来篇不同文章的关键字段不一样人在高密度复制粘贴时太容易串行第三步最折磨人上传走得很慢时你必须干等着一旦走神就可能没注意到失败提示第五步是最容易被省略的很多人点完提交就算完了根本不回头看。结果就是“我以为发出去了”和“系统里真发成功了”之间经常出现落差。所以我在项目定义阶段就给这次自动化定了一个核心目标自动化的终点不是帮人点下按钮而是能证明这次发布是完整且正确的。如果没有后面这半句RPA 做得再顺也只是把“人工点鼠标”变成了“机器点鼠标”出错的概率并没有本质变化。1.2 两条路线之争RPA 还是 API 对接不少朋友听到自动化第一反应是为什么不直接调接口我专门纠结过这个问题结论是接口路由能不能走通取决于目标系统给不给你机会。那个内容中台是第三方定制开发的系统操作界面一应俱全但并没有对外开放正式 API也没有官方接口文档。想走接口路线要么等供应商排期开发要么自己对内部网络请求做逆向解析。前者周期不可控后者风险高系统一升级接口字段一变全链路可能直接翻车。于是 RPA 成了这个场景下最务实的选择它不依赖系统方配合从用户视角模拟真实操作只要界面还能用脚本就能继续跑。但“选了 RPA”不等于“可以随便写”。我在项目开始时给自己立了两条架构原则对外表现是 RPA对内逻辑要分层。数据准备、界面操作、结果验证、通知记录必须分开不能揉在一个函数里。所有发布数据从外部文件驱动绝不允许把内容硬编码进脚本。后续要增加数量、调整内容只改数据文件不改流程代码。这两条原则在后面的实现里帮了大忙尤其是多账号扩展阶段基本没动流程代码。2. 技术选型复盘为什么我没栽在工具选择上RPA 工具不算少但“用 Python 做 RPA”具体走哪条技术路线选择起来有讲究。我当时把候选方案过了一遍最后定下的组合在后续稳定运行中经受住了验证。2.1 方案对比PyAutoGUI、Selenium 与 Playwright方案原理优势劣势适合场景PyAutoGUI模拟鼠标键盘基于屏幕坐标和图像识别不挑环境能操作任意桌面程序坐标偏移敏感分辨率一变就废无法读取页面状态无 DOM 可用的老系统、兜底操作Selenium通过浏览器 Driver 操作 DOM 元素生态成熟、资料多、团队熟悉等待策略要自己写元素状态同步靠经验标准 Web 表单老团队维护成本低Playwright基于浏览器协议控制页面内置自动等待自动等待完善跨浏览器内置断言、截图、下载处理对浏览器版本有一定要求需要学新 API现代 Web 系统需要稳定验证的长流程这个对比里最关键的分界线不是“谁更快”而是“能不能验证结果”。PyAutoGUI 点完以后脚本不知道页面发生了什么只能靠截图去猜Selenium 能拿到 DOM但定位、等待、同步都需要大量手工代码Playwright 则把可交互状态检查内建在操作里它会等元素可见、稳定、能接收事件之后再执行点击这正好匹配发布流程里“上传完成”“提交按钮变成可点状态”这类充满动态变化的场景。2.2 我最终敲定的组合与理由我的最终组合是主流程用 Playwright 的同步 API 做浏览器自动化PyAutoGUI 作为兜底手段处理极少数浏览器自动化碰不到的特殊控件。选 Playwright 的首要原因是它对“用户能看到的页面”和“页面真正渲染完成”之间的时间差处理得非常好。发布流程中最怕的就是元素刚出现在 DOM 里但还不能点击固定 sleep 又总是不够智能。Playwright 的 actionability 检查会反复轮询元素状态满足条件才继续这让我在写流程时能少操心大量“等多久”的问题。PyAutoGUI 我留了两个使用场景。一个是文件上传的兜底虽然大部分上传控件能用set_input_files直接给 input 标签指定路径但遇到页面用自研 Upload 组件、没有暴露真实 input 节点时就得靠系统级文件对话框加 PyAutoGUI 键入路径。另一个是极端的验证码兜底虽然我尽量靠 Cookie 和登录态绕过校验但偶尔会话过期重登时难免遇到此时弹出一个对话框提示人工处理几秒钟比脚本硬刚验证码合理得多。这套组合还有一个隐藏优点万一目标系统某天变得不稳定需要临时退回半自动操作PyAutoGUI 兜底脚本可以在不动主代码的情况下分担一部分紧急工作。3. 自动发布主流程拆解代码不是最难的部分进入正题自动发布主流程怎么写。我在这个项目里最大的感受是如果只写“打开页面—输入文字—点按钮”脚本能跑通一次但大概率撑不过十次。真正决定成败的是登录态管理、数据驱动、等待策略和异常兜底这些细节。3.1 登录前置处理Cookie 与登录态发布流程的第一步是登录后台。如果每次都走用户名密码登录会有两个潜在的麻烦一是频繁触发验证码或二次认证二是脚本在深夜批量运行时登录动作太反常容易被系统判定异常。所以我设计了“首登录 状态持久化 状态自检”的组合。首次配置阶段会手动登录一次然后把 Cookie 和 localStorage 的关键状态保存到本地状态文件。脚本每次启动时加载这个文件注入浏览器上下文先访问首页探测当前登录态是否有效如果发现跳回了登录页才进入账号密码登录兜底。下面是加载登录态的示例from playwright.sync_api import sync_playwright import json def load_storage_state(context, storage_path): with open(storage_path, r, encodingutf-8) as f: storage_state json.load(f) context.add_cookies(storage_state.get(cookies, [])) # localStorage 需要先访问同源页面后再注入 page context.new_page() page.goto(BASE_URL, wait_untildomcontentloaded) for key, value in storage_state.get(origins, []): for k, v in value.get(localStorage, []): page.evaluate( (k, v) localStorage.setItem(k, v), k, v )这里有个很容易踩的细节保存登录状态时不能只存 Cookie一定要连 localStorage 一起保存。很多后台系统的权限标识是放在 localStorage 里的只存 Cookie 就会出现“看着登录了实际没权限”的诡异现象。不少 RPA 项目反复出现登录失效排查半天最后发现就是漏了这一步。3.2 内容组装与待发布清单管理发布内容我设计成 CSV 清单驱动每条记录包含标题、分类、摘要、关键字、附件路径、目标账号、期望发布状态等字段。脚本启动时先读取清单做一次完整性的前置校验缺标题的、分类不在白名单里的、附件路径不存在的一律拦截掉绝不能带到发布流程里。import csv import os def load_publish_items(csv_path): items [] with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: if not row.get(title, ).strip(): raise ValueError(存在未填写标题的记录请检查清单) if not os.path.isfile(row.get(attachment, )): raise FileNotFoundError(f附件不存在: {row[attachment]}) items.append(row) return items这个前置校验帮我挡掉了大量“发到一半发现内容不对”的尴尬。有一次脚本跑到第三条记录时附件路径指向了一个已经被清理的临时目录发布流程卡在上传环节因为前置校验没拦住白白浪费了十几分钟去排查。后来在校验逻辑里增加了“文件存在且大小大于 0”的检查这类问题再没出现过。清单文件同时承担增量管理的职责。每条记录带一个 status 字段发布成功后就标记为 published下次运行自动跳过已完成记录也可以用--force参数强制重跑任意一条。这样一套机制既支持批量补量也支持单条重发不用专门写一堆重复脚本。3.3 发布动作的可控执行链路发布动作本身我封装成一个函数所有页面操作都收敛在里面并在外层套上重试控制。核心思路是每次执行前都从当前页面状态出发而不是假设自己还停留在预期的页面执行到任何一步失败时先记录现场再决定是否重试。from playwright.sync_api import sync_playwright PUBLISH_URL https://example.com/publish def publish_item(page, item): page.goto(PUBLISH_URL, wait_untildomcontentloaded) page.get_by_label(标题).fill(item[title]) page.get_by_label(分类).select_option(item[category]) page.get_by_label(正文摘要).fill(item[abstract]) page.set_input_files(#uploadInput, item[attachment]) page.get_by_role(button, name提交发布).click() page.locator(.success-tip).wait_for(statevisible, timeout15000) item_id extract_item_id(page.url) return {status: published, item_id: item_id}这里我坚持用get_by_label、get_by_role而不是写死 CSS 选择器是吃过亏之后形成的习惯。后台系统升级版本时CSS 类名和 DOM 结构说变就变但表单的 label 文本、按钮的可访问名称一般会保持稳定。用语义化定位脚本对页面改版的抵抗力会强很多。提交后等待成功提示我也不是死等固定时间而是等待“成功提示可见”这个状态出现。这样页面响应慢的时段会被自动覆盖不需要人工去反复调大或调小 sleep。3.4 定位与等待策略给脚本装上眼睛和耐心定位和等待是 RPA 自动化的核心功力我把它们压缩成几条硬性规则写进了项目注释里能用文本定位的不用结构性定位。优先get_by_text、get_by_label、get_by_roleCSS 选择器只作为补充。等待状态变化不固定 sleep。等“按钮可点击”优于等“三秒”等“成功提示出现”优于等“五秒”。每个长操作前先做页面元素自检。比如上传区域是否已就绪、附件列表是否已清空。第三条是我踩坑后补上的。有次上传组件在页面上初始化为隐藏状态脚本没检查就直接执行set_input_files文件确实传上去了但后续提交时系统认为附件模块未完成初始化弹了个错误提示。加了“上传区域可交互”的自检步骤后这类问题就再没出现过。4. 发布后的自动测试解决“发出去了吗”和“发对了吗”项目名称里的“测试”二字是我专门强调的。如果只发不验证那 RPA 只是把手动重复劳动换成了半自动重复劳动脚本照样会发错只是发错得更快而已。4.1 测试目标的分层设计我把发布后的自动测试拆成四个层级每一层解决一个级别的问题层级检查内容典型断言第一层操作成功提交过程无异常系统返回成功状态成功提示元素可见、无错误对话框第二层数据存在新发布记录出现在列表页和详情页列表页包含目标标题、详情页状态为已发布第三层数据一致发布后的内容与源数据一致标题、分类、摘要字段与 CSV 完全匹配第四层结果可用附件可下载、详情页可正常打开附件下载成功且文件非空、详情页无白屏前两层回答“发出去了吗”后两层回答“发对了吗”。很多自动化项目只停留在第一层看到成功提示就认为任务结束。实际上那个内容中台偶尔会出现“提示发布成功、前台列表却查不到”的脏数据状况如果脚本不主动跨到前台去验证这类问题要等真实用户反馈才会暴露那就晚了。4.2 断言体系与状态机校验在实现上发布后的验证脚本会重新打开前台的模拟用户视角页面而不是停留在后台页面里。原因很简单后台列表页和前台用户看到的状态之间存在缓存和权限过滤只有从前台侧验证才真正模拟了用户看到的内容体验。import os def verify_published(page, item): page.goto(FRONT_LIST_URL, wait_untildomcontentloaded) target page.get_by_text(item[title]) assert target.is_visible(), 前台列表未找到目标内容标题 target.click() detail page.locator(.detail-content) assert detail.get_by_text(item[abstract]).is_visible(), 摘要内容不匹配 assert detail.get_by_text(item[category]).is_visible(), 分类字段不匹配 download_btn page.get_by_role(link, name下载附件) with page.expect_download() as dl_info: download_btn.click() download dl_info.value assert os.path.getsize(download.path()) 0, 下载附件为空 return {verified: True, file_size: os.path.getsize(download.path())}这段代码里每个断言都带了中文描述一旦失败日志里能一眼看出是哪个环节出了问题。配合最后一步的附件下载验证相当于把“内容能点开、附件能拉下来”真实跑了一遍。验证全部通过后脚本会把当前详情页截一张图连同执行时间、记录 ID、断言明细一起写入执行报告。这些证据在后续问题跟进时特别有用能够直接证明这次发布不仅是“发出去了”而且经过了完整的多层验证。4.3 证据留存、异常告警与人工介入边界自动验证失败的时候脚本不能闷头重试几次然后放弃必须分层处理第一层断言失败说明发布动作本身失败比如按钮没点下去、上传中断这类错误可以直接重试整个发布流程。第二层第三层断言失败说明数据异常这类问题绝不自动重发立即留存现场证据并通知人工。因为重复发布可能掩盖系统侧真正的脏数据问题。第四层附件下载失败先检查附件是否在系统侧真正生成再决定重新验证还是通知人工。通知我用了独立的通知脚本将执行结果摘要、失败截图和报告链接推送到运营团队的工作群。整体逻辑是能自动恢复的尽量自动恢复比如登录态过期就重新登录后再跑涉及数据一致性问题时坚决不自动重发始终把问题暴露在明面上。这套“自动发布 自动测试 证据留存 分级告警”的闭环让项目真正从一个“模拟人点鼠标”的脚本进化成“带验收流程的数字员工”。5. 稳定性真坑记录三个问题与完整排查链路项目上线前我以为最大的难点是写流程真正上线之后才发现稳定性才是 RPA 的生死线。脚本连续跑一个月总会在某个时间点遇到奇奇怪怪的问题。下面三个坑是我印象最深的我把完整的排查链路写出来比直接扔一个“加等待”之类的结论有价值得多。5.1 动态 ID 引发的“幽灵报错”现象发布流程刚开始几天执行得非常顺某天突然开始第一条记录的提交点击报“元素不存在”。排查过程我先看错误截图和 DOM 快照页面结构看起来完全正常目标按钮也在。于是把脚本定位用的选择器翻出来发现是后台系统自动生成的动态 ID类似content_12345每次新建发布时都会重新生成。问题就出在这里脚本第一次定位这个按钮时用的是初始页面里出现过的 ID提交时页面局部刷新后 ID 变了定位器自然就指向空。这类定位器最迷惑人的地方在于它不是每次都失败而是当 ID 恰好相同的一次能过、不同的一次就挂像幽灵一样难以复现。根因页面中按钮外层容器绑定的动态样式类在每次页面加载时会重新生成随机后缀我的选择器依赖了这个会变化的后缀。修复把所有依赖动态 ID 和动态样式类的定位一律替换为 label 文本、按钮名称等语义化定位同时把按钮可点击性的判断改为等待按钮自身 enabled 状态而不是等待某个样式类出现。改完以后这类幽灵报错再也没出现过。5.2 弹窗遮罩层拦截点击现象连续发布第二条记录时页面出现一个“操作成功是否继续编辑”的弹窗脚本点击下一步按钮时报“元素被遮挡不可操作”。排查过程Playwright 报元素不可点击时我第一反应是元素没出现但实际查 DOM目标按钮明明在。于是打开页面截图看层叠关系发现弹窗遮罩层把整个页面盖住了点击动作的落点全在遮罩层上。再往前翻这个弹窗只在前一条发布成功后出现属于典型的“跨步骤状态残留”。根因后台系统在一次发布结束后会保留一个轻量级模态框默认“否不继续编辑”。脚本没有处理这个模态框它以隐藏状态留在 DOM 里下一次发布操作时又被系统重新显示出来正好挡住了核心操作区域。修复在每次发布流程开始时统一做一次“模态框清扫”动作把可能存在的模态框关闭后再开始新建页面操作。具体实现上用page.on(dialog)监听原生对话框事件同时把弹窗关闭按钮写成一个带超时的可选等待出现就点没出现就跳过。这一个修复让后续所有批处理流程的稳定性直接提升了一个量级。5.3 首次成功第二次失败的会话状态问题现象批量发布三条记录第一条成功第二条登录校验失败被踢回登录页第三条也跟着失败但单独跑每一条却又全部正常。排查过程这个现象非常迷惑。一开始怀疑是发布节奏太快触发了风控机制于是刻意调慢操作速度但问题依旧。后来用调试模式逐步跟踪发现第一条发布成功后后台会执行一次“会话刷新”把认证令牌从 Cookie 迁移到 localStorage。脚本在第一条发布之后没有重新同步存储状态后续操作拿着旧的令牌去请求自然被判定为未认证。这种问题最坑的地方在于单条重跑时系统已经完成了状态刷新所以永远复现不了。根因目标系统有关键操作后更新令牌存储位置的机制。RPA 脚本只在启动时加载了一次认证状态没有在长流程中途感知状态变化。修复在每条记录执行前增加一个轻量级的会话探测步骤访问一个需要权限的页面检查是否被重定向到登录页如果被重定向就重新加载存储状态或走登录兜底流程。同时把状态保存时机从“只启动时存”改成“每发布成功一条后存一次”。这样脚本永远不会持有超过一个会话周期的过期状态。5.4 排查思路的一次总结这三个坑让我体会到RPA 排错最值钱的不是“知道该等待”而是“能构建出证据链”。我逐步养成了一套方法现象记录 → 最小复现 → 分层排查 → 修改验证 → 回归用例。每遇到一个新问题先截全屏、抓 DOM 快照、保存控制台日志然后用单条记录复现把范围压缩到最小确认根因后只做一处修改改完再跑回归。这套流程听起来基础但用在长流程自动化场景里真的能把两小时的排查压缩到二十分钟。6. 从 demo 到长期跑扩展方向与实操体会项目稳定之后我又陆续给它加了几个面向无人值守场景的能力。这里不展开全部细节重点说扩展思路和写代码之外的经验。6.1 定时调度与无人值守发布流程一旦跑顺剩下的问题是什么时候跑。我用的方案是系统任务计划程序做每日定时执行外加锁文件机制避免重复运行。锁文件逻辑很朴素启动时检查锁文件是否存在存在说明上一轮还没跑完直接退出正常结束时删除锁文件。这个看似简单的机制挡住了大量“定时任务重叠执行”造成的脏数据因为发布这种流程最怕两个实例同时操作同一个账号容易互相覆盖或触发异常。6.2 多账号、多流程的任务编排后来需求扩展成多账号、多平台的后台轮流发布我在数据层加了一张账号映射表把每条发布记录和目标账号关联起来。主流程代码基本没动只是加了一层账号维度的并发控制同一账号下的记录严格串行不同账号之间可以并行。并行度压到两个浏览器实例以内避免服务器负载过高导致页面响应变慢。这个扩展恰恰验证了最开始“数据和流程分离”设计的分量——没有改流程代码只加了数据配置就撑起了多账号场景。6.3 我个人最有价值的三条经验最后分享三条带血的实操体会。第一条RPA 脚本一定要有现场记录能力。跑失败的时候截图、DOM 快照、控制台日志这三样缺一不可否则排查就是盲人摸象。我在项目里把所有执行记录和失败输出统一归到一个日志目录现在回看任何一个历史问题都像回放录像一样能还原当时现场。第二条代码要写好“退路”。RPA 不可能跑得完美每一步都要想好失败后做什么重试、跳过、还是通知人工。我按错误类型定义了三种级别可重试错误登录过期、网络超时、不可重试错误字段校验失败、数据不一致、可跳过错误该条已发布过。这套分级让运维决策清晰也给了脚本一定的自愈能力。第三条不要迷信自动化率。有些环节人工点一下其实更稳比如录入验证码、选择有歧义的附件版本。强行走全自动换来的往往是更高的重试成本和更复杂的技术方案。我在项目里坚持“能达到目的的最小自动化”原则这让精力能集中在真正适合自动化的发布主流程和验证流程上。这套 Py-RPA 项目从最初一个单纯点击脚本逐步长成承载多账号自动发布、四层验证、证据留存和定时调度的完整流水线过程里反复调整过很多次。如果你也在规划类似的自动化项目希望这篇复盘能帮你少走一些弯路。关键不在于工具多高级而在于把流程想清楚、把验证做扎实让它真正变成可靠省心的数字助手。