用 Playwright 做多平台内容分发,我踩过的 8 个坑
最近在做一个「把文章自动发布到多个自媒体平台」的桌面工具,技术栈是 Electron + Playwright。 原以为难点在反爬,做完才发现:真正耗时间的是那些「看起来能用、实际静默出错」的细节。 把踩过的 8 个坑记下来,供同样在做 RPA 的同学避坑。
1. 编辑器在 iframe 里,你的选择器「全部失效」
第一次跑头条号,所有元素定位全 MISS,一度以为是反爬。 抓了 DOM 地图才发现:编辑器在 iframe 里,只在主框架找当然找不到。
解决办法是让解析器自动在主框架和子框架之间回退,并把「命中的框架」作为降级信号记录下来。
2. 内容不在 DOM 里(CodeMirror / Monaco)
掘金用的是 ByteMD + CodeMirror 5。它的隐藏 textarea 只是输入代理,正文内容在 CodeMirror 自己的模型里。
这意味着两件事:
- 往 textarea 的 value 里塞内容是没用的(编辑器不认)
- 校验也不能读 DOM ------ 读 textContent 永远是空的,会把「注入成功」误判成「注入失败」
正确做法是走编辑器自己的 API:向上找到持有 CodeMirror 实例的容器,调用 setValue。
3. 假成功:最危险的故障类型
有一次日志显示「发布成功」,我去平台上一看:根本没发出去。
原因是发布成功的判定复用了「草稿保存成功」的信号,而上一次草稿保存成功的提示还挂在页面上。
教训:成功判据必须高特异性。现在我的判定优先级是:
- URL 跳到明确的成功页(如 creation/success/文章ID)
- 出现明确的成功文案
- 两者都没有 → 如实报「结果未知,不得自动重试」,绝不猜
4. 坐标点击必须先滚动
为了模拟真人,我用贝塞尔曲线算鼠标轨迹,然后用原始坐标点击。 结果:页面下方的控件(分类、标签、封面)点了完全没反应。
原因:boundingBox() 对屏幕外元素返回的是相对页面的坐标,而鼠标点击用的是视口坐标。 加一行 scrollIntoViewIfNeeded() 就好了 ------ 但这一行让我排查了几个小时。
5. 隐藏的 file input 永远等不到 visible
上传图片用的 inputtype=file 几乎都是 0×0 的隐藏元素(样式美化需要)。 而自动化框架默认等元素「可见」,于是永远超时。改用等待「已挂载」(attached)即可。
顺带一提:有些平台的上传控件是点击按钮后才动态创建的,所以还得「先点按钮 → 再找 input」, 而且不能拿第一个 ------ 第一个往往是「上传封面」。
6. 单文件 input 不能一次传多张
setInputFiles 传多张时直接报错:Non-multiple file input can only accept single file。
得逐张传、逐张等(等图片真的回填成平台 CDN 地址再传下一张)。 必须校验回填的 URL 是不是平台域名 ------ 是 blob: 或本地路径就说明没传完,这种文章发出去就是「图裂」。
7. __name is not defined:page.evaluate 的隐形杀手
这个坑最隐蔽。用 tsx 跑 TypeScript 时,esbuild 为了保留函数名,会把 const clean = (s) => s.trim() 编译成 const clean = __name((s) => s.trim(), "clean")。
这个函数被 page.evaluate 序列化送进浏览器时,页面里没有 __name,整个回调直接抛 ReferenceError。 而外层通常写着 catch 兜底,于是表现为「数据莫名取不到」,极难定位。
两个解法:注入一个恒等 shim(window.__name = fn => fn),或者干脆别在 evaluate 里写具名函数表达式。 另外一条经验:evaluate 里的异常一定要带出来,不要静默吞掉。
8. 平台差异比想象的多
举几个真实例子:
| 差异 | 表现 | | --- | --- | | 草稿保存 | CSDN 有「保存草稿」按钮;掘金是自动保存,根本没有按钮 | | 登录入口 | CSDN 的密码登录藏在底部一个图标后面;掘金是明文 tab | | 发布流程 | 一步发布 vs「点发布 → 填分类标签 → 再点确定并发布」 | | 会话 Cookie | 有的平台 uuid_tt_dd 这类设备 ID 未登录也存在,拿它判登录会误判 |
所以「每个平台一个适配器 + 一份外置的选择器配置」是必须的,不能图省事写 if-else。
小结
回头看,这 8 个坑里有 4 个属于同一类:程序说成功,实际没成。
自动化系统里,假成功比失败危险得多:失败会重试,假成功会静默地污染数据、让用户以为事情办好了。 所以我现在给自己定了条规矩:
任何「成功」都必须有高特异性的证据;拿不到证据就如实报「未知」,绝不猜。
如果这篇对你有用,后面我会继续写「选择器热更新」和「审核状态回查」的工程实现。