前几天我干了一件挺有意思的事:让 AI 自己发布了一篇头条文章。具体来说,就是用 Cursor 当“编程大脑”,用 Playwright 当“机械手”,控制一个真实的 Chrome 浏览器打开头条创作平台,登录、写标题、填正文、点发布,一气呵成。中途我只做了一件事——扫码登录的时候掏了一下手机。
这套组合其实值得每个做测试、做运营、做内容的人关注:Playwright 是当前最主流的 web 浏览器自动化框架,Cursor 是能读懂整个工程上下文、直接帮你生成和修改代码的 AI 编程工具。两者配合起来,相当于你有了一个能自己看页面、自己写脚本、自己修报错的“实习生”。这篇文章就把整个实战过程拆开讲清楚:从为什么这样选型,到环境怎么搭、脚本怎么让 Cursor 写、完整发布流程怎么落地,再到我实际踩过的坑和对应解法,尽量做到你能按着文章思路复刻一个自己的版本。
1. 项目拆解:先搞清这个自动化到底要做什么
1.1 标题里藏着三个关键词
很多人看到“让 AI 自动发布文章”会以为难点在“AI 写文章”,但真正动起手来你会发现,文章内容反而是最简单的一环,瓶颈全在“浏览器操作”这一层。
拆开来看,这个标题里有三个关键技术点:
- Playwright:微软开源的浏览器自动化框架,支持 Chromium、Firefox、WebKit。它通过一套统一的 API 去控制真实浏览器内核,能模拟点击、输入、截图、读取页面内容、处理多个标签页,几乎是目前做 web 端自动化测试和 RPA 类工具的首选。
- Cursor:一款 AI 编程 IDE,本质上是 VS Code 的“魔改版”,核心能力是能读取你整个项目目录,结合当前打开的代码文件来回答问题、成段生成代码、在指定位置修改代码。你可以把它理解成“离代码最近的那档 AI 助手”。
- web 浏览器操作:这里不是指打开浏览器看网页,而是指让程序替代人完成一系列页面上的真实操作,包括登录、跳转、填写表单、点击按钮、等待反馈。
这个项目的本质,是让大模型通过 Cursor 帮你写出“一段控制真实浏览器的程序”,然后这段程序替代人工完成“在头条创作后台上发布一篇自带标题和正文的文章”。AI 既参与了写脚本,也参与了写内容。
1.2 为什么第一个场景选“发布头条文章”
我见过不少人拿到 Playwright 后的第一个想法是“我要写个抢票脚本”或者“我要批量注册”。这类需求在合规上天然有问题,而且网站的反自动化策略往往非常激进,根本不适合新手用来练手。相比之下,“发布自己账号下的文章”是个边界清晰、价值明显、风险可控的场景:
- 流程固定:登录 → 进入创作中心 → 新建文章 → 填标题 → 填正文 → 提交发布。整条链路每一步都明确,适合写成脚本。
- 结果可校验:发布成功后页面会出现明确的成功提示,或者能从文章列表里看到新记录。这对自动化脚本非常重要——有没有成功是可以用代码断言出来的。
- 价值可见:如果你是一个每天要在多个平台同步内容的创作者,这玩意儿能直接省下十几分钟重复劳动,比写一百个“hello world”练习都让人有动力。
- 风险可控:发布的是自己原创内容,使用的是自己账号,频率不高,属于正常用户行为。拿它学习浏览器自动化,心态上踏实很多。
1.3 方案选型:为什么是 Playwright + Cursor,而不是其他组合
也许你会问:做浏览器自动化不是还有很多工具吗,像 Selenium、Puppeteer、Pyppeteer,为什么偏偏选 Playwright?而写脚本为什么不纯手写,非得拉上 Cursor?
先对比 Selenium。Selenium 确实老牌,但它的底层是 WebDriver 协议,和浏览器通信要经过一个中间服务,很多版本在元素定位、页面等待、多标签处理上的体验都比较“古老”。Playwright 走的是 Chrome DevTools Protocol 这一条更贴近浏览器内核的通道,最让我受用的三点是:
- 自动等待做得很好。Playwright 的大多数操作会内置等待机制,比如点击元素之前会等它稳定、可见、可被点击,而不是像 Selenium 那样没等到就报错。
- 上下文和登录态管理方便。一个
BrowserContext相当于一个独立的浏览器会话,天然隔离 Cookie、缓存,还能一键保存和恢复登录状态。 - 自带 codegen 录制器。可以直接录一段人工操作生成基础代码,省掉最反人类的“手动查选择器”环节。
再说不纯手写的问题。手写一个发布脚本的技术难度确实不高,但真正耗时间的地方在于:你得反复打开开发者工具看 DOM、试各种选择器、处理页面加载慢、判断“到底执行到哪一步了”。这些恰恰是 Cursor 这类 AI 工具擅长的——你只要给它一个明确的页面目标和你录出来的代码片段,它能很快整理出可用的主流程。人负责判断方向和最终把关,AI 负责快速生成和调错,这套分工我实测下来效率非常高。
也有人问:“为什么不直接用现成的 RPA 工具?” 因为现成工具通常绑定特定平台,只能选择已有的动作组件,灵活性不足。用 Playwright 写,本质上是在写一套完全属于自己的“浏览器遥控器”,换一个网站、换一种操作,改代码就行。
2. 环境准备:让 Playwright 在本地跑起来的完整过程
2.1 用 Python 还是 Node.js
Playwright 官方提供 JavaScript/TypeScript 和 Python 两套主要 SDK,另外也有 Java 和 .NET。我自己选的是 Python,理由很朴素:我后续想把发布结果写进 Excel 做记录,还想接一些数据分析和 AI 相关的库,Python 这边生态最顺。如果你本身就是前端开发者,用 TypeScript 也完全没问题,核心 API 的设计是一致的。
安装 Python 版本只需要两步:
pip install playwright playwright install chromium第一条命令装的是核心库,第二条命令是下载浏览器内核。默认会下载 Chromium 三个发行渠道中的统一版本,实际跑的时候用p.chromium.launch()就能拉起它。
如果你用 Node 生态,对应的命令是:
npm init -y npm i -D playwright npx playwright install chromium这里有个非常容易踩的坑:playwright install下载浏览器依赖的是国外的 CDN,在国内网络环境下经常下到一半卡死,或者报ETIMEDOUT。解决办法是用国内镜像,比如设置环境变量指向 npmmirror 的浏览器下载地址:
# Windows PowerShell $env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright" playwright install chromium # macOS / Linux export PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright" playwright install chromium如果你是在 Linux 服务器上跑,还需要装一批系统依赖库,最好直接用官方参数装:
playwright install --with-deps chromium不然大概率会遇到启动浏览器时缺.so动态库的报错,这类报错信息往往很吓人,其实只是系统依赖没装齐。
2.2 第一个脚本:先让浏览器自己打开
环境装好之后,我建议你先不要急着写业务代码,花两分钟写一个最小脚本,验证整条链路通不通:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://mp.toutiao.com") page.wait_for_timeout(3000) browser.close()这里有两个细节值得说。
第一,headless=False表示有头模式,也就是你会肉眼看到一个 Chrome 窗口打开并自动跳转。第一次跑自动化务必用有头模式,因为你得确认浏览器真的启动了、页面真的加载了。等到脚本稳定之后,再改成headless=True做无人值守。
第二,page.wait_for_timeout(3000)只是我用来“肉眼观察”的临时等待,不是常规手段。正式代码里不要频繁用这种固定sleep,优先用条件等待,也就是等待某个元素出现或某个操作完成。关于这一点,后面第五节会展开讲。
2.3 录制脚本:用 codegen 先把人工操作变成代码
Playwright 最让我上头的功能就是录制定向,叫codegen。它相当于一个“浏览器录像机”,你正常在浏览器里操作,它在一旁把每一步翻译成代码输出。
启动命令是:
python -m playwright codegen https://mp.toutiao.com如果是 Node 项目:
npx playwright codegen https://mp.toutiao.com启动后你会看到两个窗口:一个是真实浏览器,一个是实时同步生成的代码面板。这时候你就手动走一遍发布文章的全流程:登录、进创作中心、点发布文章、编辑器的位置先点一下再随便写几个字、把标题框也点一下。等到操作结束后,你已经拿到了一份能跑通基础路径的脚本。
但这里必须提醒一句:codegen 生成的代码质量比较“粗糙”。它的优势是选择器抓得准,因为那些选择器是它从真实页面 DOM 里录出来的;缺点是逻辑完全是直铺的,没有函数封装,没有异常处理,没有登录态复用,很多时候还夹着一大堆wait_for_timeout。我的习惯是:把 codegen 当成“翻译工具”,生成完代码后直接丢给 Cursor 做结构化重构,而不是当成最终答案。
3. 用 Cursor 生成自动化脚本:提示词与分层设计
3.1 Cursor 能做什么、不能做什么
Cursor 的强项是能理解你的项目上下文,也就是说它不像网页版 ChatGPT 那样只能看到聊天框里的内容,而是能直接读你当前工作区里的文件、记住你的目录结构、在指定文件里插入或修改代码。写 Playwright 脚本这种任务,天然适合它:你需要反复根据真实页面的 DOM 结构生成定位代码,而这些结构信息可以通过你的描述、报错信息、甚至完整的 HTML 片段喂给它。
但它也不是万能的。最重要的一点是:它没有“亲眼”看过你的页面,也不会替你跑代码。所以你不能只丢一句“帮我写个自动发布头条文章的脚本”,然后指望一次成功。你得给它足够的信息:用哪个框架、目标页面地址、操作顺序、登录方式、内容从哪里来、发布成功的标志是什么。
另外要提醒一个很多人忽略的点:Cursor 本质是云端的模型服务,对话内容会被发送给模型提供商。所以不要把各种密钥、Token、Cookie、账号密码直接粘进对话里。脚本要用的密钥放本地环境变量或.env文件,让代码去读取即可。这个习惯要从第一天就养成。
3.2 把需求讲清楚:一段可复用的提示词结构
我实际操作下来,最顺手的提示词组织方式是“背景 + 原料 + 目标 + 限制”。下面是我这次用的一段提示词的结构,你可以直接套:
“我准备用 Playwright(Python 版)写一个自动发布文章到头条创作平台的脚本。当前页面入口是 https://mp.toutiao.com,登录后进入创作者后台。我已经用 codegen 录了一段基础操作,代码贴在下面:……(粘贴录制代码)。请把这段代码重构为清晰的分层结构:config.py 存 URL 和路径,publisher.py 写核心发布流程,main.py 负责串联。要求:1)所有定位器都带超时时间;2)标题和正文从本地 article.md 读取;3)发布前点击发布按钮后,等待“发布成功”的提示出现再返回;4)发布完成后截图保存到当前目录。代码先解释你的设计,再给完整文件。”
这里面的“目标”和“限制”尤其重要。你如果不告诉它要等“发布成功提示”,它很可能就只会机械地“点完发布就结束”,那脚本等于没有结果校验。你如果不告诉它“标题从本地文章读取”,它就默认写死一条示例内容,后面你每次发布都得改代码。
3.3 生成代码的分层设计
经过 Cursor 重构后,我的工程目录长这样:
auto-publish/ ├── config.py # 全局配置:URL、超时时间、文件路径 ├── login.py # 首次登录,保存登录态到 state.json ├── publisher.py # 核心发布流程:读取文章、填写、发布 ├── main.py # 入口:加载登录态,调用 publisher ├── article.md # 待发布的文章(标题+正文) ├── state.json # 登录态文件,首次登录后自动生成 └── requirements.txt为什么要分层?因为你不分层的话,登录逻辑、页面逻辑、内容读取逻辑全搅在一个文件里,后续改任何一处都要在大几百行的代码里找。分层之后的好处很直接:如果某天头条后台的“发布按钮”选择器变了,我只需要改publisher.py,根本碰不到登录逻辑和内容读取逻辑。
publisher.py的核心骨架大概是这样的:
from playwright.sync_api import Page, expect class ArticlePublisher: def __init__(self, page: Page, config: dict): self.page = page self.config = config def go_to_editor(self): # 进入创作中心后,打开新建文章页面 self.page.goto(self.config["editor_url"]) self.page.wait_for_load_state("networkidle") def fill_title(self, title: str): title_input = self.page.locator( 'input[placeholder*="请输入标题"]' ) expect(title_input).to_be_visible(timeout=10_000) title_input.fill(title) def fill_content(self, content: str): editor = self.page.locator(".ProseMirror") editor.click() self.page.keyboard.insert_text(content) def publish(self): publish_btn = self.page.get_by_role("button", name="发布") publish_btn.click() success = self.page.locator("text=发布成功") expect(success).to_be_visible(timeout=30_000)注意这里我用的选择器是.ProseMirror,这只是我这次录制到的编辑器 class,不同时期头条后台的 DOM 可能不一样。凡是页面结构相关的地方,都应该以你自己codegen录制出来的为准。这也是为什么第一步一定是“录一遍再重构”,凭空很难猜出真实的 DOM。
3.4 第一次跑通后必须补齐的“经验型代码”
Cursor 第一次生成的代码能跑通主流程,但离“可放心使用”还差三块很重要的东西,我第一次跑的时候没加,结果翻车了。这三块分别是:
登录态复用。自动化脚本每次启动都重新扫码登录很折腾,而且头条的扫码登录二维码有时效,流程可能因为等你扫码而中断。正确做法是第一次手动登录后,把BrowserContext里的登录状态保存成文件,后续脚本直接加载。代码就两行:
# 首次登录时保存 page.context.storage_state(path="state.json") # 后续启动时复用 context = browser.new_context(storage_state="state.json")幂等控制。自动化脚本最怕重复执行。如果发布按钮点了一次但页面响应比较慢,你可能以为失败了又跑一次,结果就发了两篇一模一样的文章。我的解决方式是在publisher.publish()之前检查:从本地的published.jsonl文件读取历史发布记录,如果article.md的第一行标题已经在记录里,就直接中止发布。
失败现场留证。页面操作不可能永远不出问题,出问题的时候你得知道当时发生了什么。我的做法是在任何异常分支里都加一行截图:
try: publish_btn.click() except Exception: page.screenshot(path=f"error_{timestamp}.png") raise不要小看这三块,它们决定了一个自动化脚本是“能跑”还是“能长期用”。
4. 完整发布流程实现:登录、填内容、点发布的代码细节
4.1 登录后保持会话:storageState 的正确玩法
先聊登录。头条的创作者后台登录方式主要是扫码,偶尔有手机验证码。自动化脚本直接走验证码会非常麻烦,因为收码环节绕不开手机验证。所以我最终采用的是“人工登录一次 + 保存登录态”的方案,这是目前最稳、也最合规的做法:
第一步,用一个有头模式的脚本打开登录页,人工扫码,等页面跳转进创作者后台后,执行:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() page = context.new_page() page.goto("https://mp.toutiao.com") input("扫码登录完成后按回车继续...") context.storage_state(path="state.json") browser.close()第二步,之后所有自动化脚本都这样加载登录态:
context = browser.new_context(storage_state="state.json") page = context.new_page() page.goto("https://mp.toutiao.com")这里有个很容易忽略的坑:storage_state里存的是 Cookie 和本地存储,但很多平台的后台会校验 User-Agent 和浏览器环境指纹。如果你第一次保存登录态用的是有头模式,后面加载时却用了完全不同的浏览器启动参数,登录态有可能失效。所以我建议正式使用后所有脚本尽量保持一致的浏览器启动参数,不要今天有头明天无头混着来。
4.2 页面元素定位:创作平台的关键节点
头条创作平台的页面结构在不同的登录态、不同时间点会有差别,但大体上是固定的几个步骤:进入创作中心 → 左侧“发布”菜单 → 选择“文章” → 进入文章编辑页。
文章编辑页最关键的两个元素是标题输入框和正文编辑器。
标题输入框通常是input,我用的是 placeholder 模糊匹配:
title_input = page.locator('input[placeholder*="请输入标题"]') title_input.fill(title)注意这里用fill而不是press_sequentially,因为fill会一次性设置值,速度极快且稳定。press_sequentially是为了模拟真人逐字输入,但它在富文本场景容易因为焦点问题漏字符,能不用就不用。
正文编辑器要麻烦一点。它往往是contenteditable的 div,而不是传统的<textarea>。对于 Playwright 而言,fill()方法对 contenteditable 的支持时好时坏,具体取决于页面的实现方式。我这次遇到的情况是必须点击编辑器、获得焦点,然后通过keyboard.insert_text()模拟输入:
editor = page.locator(".ProseMirror") editor.click() page.keyboard.insert_text(body_text)这里还有一个小知识点:keyboard.insert_text()插入中文没有问题,但如果正文里包含很多换行和特殊字符,建议先进行一次格式预处理,把 Markdown 的#、**等符号转成目标平台能理解的纯文本或 HTML。头条的富文本编辑器支持一定程度的粘贴排版,如果直接插入带 Markdown 符号的原文,发布出去的文章可读性会很差。
4.3 文章内容从哪里来:AI 生成与数据文件
很多人听说“AI 自动发布文章”,下意识会想“是不是让 AI 现场写一篇然后直接发”。技术上确实可以做,但我更推荐“内容准备与发布执行分离”的方案:发布动作由 Playwright 完成,而文章内容平时已经准备好,存在本地article.md里。这样做有个巨大的好处——你可以在发布前人工检查文章内容和格式,而不是把一个 AI 临时生成的偏偏文本直接丢给平台。
内容生成这一步我用的是 Cursor。具体做法是在 Cursor 的对话里给定一个写作主题和字数要求,让它先写一版草稿,我再手动改一遍确认没毛病,保存成article.md。脚本读取这个文件的逻辑很简单:
import re with open("article.md", "r", encoding="utf-8") as f: lines = f.read().strip().split("\n") title = lines[0].lstrip("# ").strip() body_text = "\n".join(lines[1:]).strip()当然,如果你就是想全自动,也可以让 Cursor 帮你写一段调用大模型 API 生成文章的代码。大致思路是:把主题写入一个topic.txt,脚本启动后请求大模型接口拿到文章内容,再直接填入页面。但我要泼一盆冷水——内容质量是自动化的上限,而不是代码本身的功劳。平台不缺文章,缺的是至少对读者负责的内容。自动化帮你节省的是操作时间,不是思考时间。
4.4 发布动作和结果校验:点完按钮之后怎么办
发布按钮在整个流程里是最需要谨慎处理的位置。我遇到的页面结构里,右上角有个“发布”按钮,点击后会再弹出一个确认框或下拉菜单,需要再次点击“发布”才算最终提交。
完整发布动作建议写成这样:
def publish_article(page): publish_btn = page.get_by_role("button", name="发布").first publish_btn.click() # 有些情况下会弹出二次确认,尝试处理但不强制 confirm_btn = page.get_by_role("button", name="发布,确认提交").first try: confirm_btn.click(timeout=3_000) except Exception: pass # 等待成功提示出现 success = page.locator("text=发布成功") expect(success).to_be_visible(timeout=30_000)用get_by_role("button", name="发布")来定位按钮,是我特别推荐的做法。它基于无障碍语义来查找元素,通常比直接抄 class 名稳定得多。哪怕按钮的长相换了、class 版本换了,只要还是同一个按钮,名字没变,就能找到。
expect(...).to_be_visible(timeout=30_000)则是官方推荐的等待方式,它的含义是“在 30 秒内,这个元素必须是可见的才通过”。如果发布没有成功,这里就会抛超时异常。加上我前面提到的截图逻辑,跑挂了也能看清现场。
4.5 安全边界:频率、合规与人工兜底
把发布自动化跑通之后,有两件事我特别想强调,因为它们比技术更影响你长期使用这套东西的安稳程度。
第一,合规边界。自动化脚本能做的事很多,但“能不能做”和“该不该做”是两码事。我这次的做法是只发布自己账号下的原创内容,不搞批量注册小号,不刷阅读、不刷评论、不买量,也不去试图绕过平台的风控和验证机制。这类事情轻则封号,重则给自己惹上一堆不必要的麻烦,完全不值得。
第二,频率控制。哪怕只是正常发布,也不要一下子上线一个“每分钟发一篇”的脚本。平台对非常规频率是有感知的。我自己会在关键操作之间加一些随机间隔,把这个也写进了流程里:
import random import time time.sleep(random.uniform(2, 5))间隔别写死,随机一点更像真人操作,也更不容易触发无谓的风控提醒。自动化追求的是高效,但没必要高到“看起来不像人”。
第三,人工兜底。我最后的发布会做成半自动模式:脚本把标题和正文填好之后,会先弹出一个确认提示,等人工看一眼草稿再决定是否真正点发布。这种“半自动”的模式听着比“全自动”笨,实际上是我最推荐的姿势。因为文章一旦发出去了,撤回一文成本可不算低。
5. 我踩过的坑:Playwright + Cursor 常见问题排查
5.1 浏览器下载失败与安装报错
这几乎是每个新手都会遇到的第一道坎。我在 2.1 节已经提到了镜像环境变量,这里补充一个更容易被忽略的情况:有时候playwright install显示下载成功,但运行时仍然报错找不到浏览器可执行文件。原因通常是版本不一致——你pip install的 Playwright 版本和之前playwright install下载浏览器时的版本不是同一套。
解决办法是每次升级库版本后都要重新执行一次安装:
pip install -U playwright playwright install chromium或者干脆在项目里锁定版本:
pip install playwright==1.44.0千万不要用“最新版安装失败就降级再装”这种思路反复折腾。先敲playwright --version查看版本,再对照官方文档确认浏览器版本要求,比瞎试靠谱得多。
5.2 选择器不稳定:今天能跑明天报错
页面自动化最大的敌人就是 DOM 变化。今天还能定位到的选择器,明天后台前端一改版就失效了。这是 Playwright 项目维护中无法完全避免的问题,只能尽量降低影响。
我总结了几个让选择器更抗造的技巧:
- 优先用
get_by_role或get_by_text,它们基于元素的语义和可见文本,而不是一串可能随时变化的 class。 - 不要复制一长串
div > div > div路径。这种绝对路径是最脆的,页面层级稍微一变就全崩。 - 对会变的 ID/class 做模糊匹配,比如
locator('[class*="editor"]'),少用精确匹配。 - 顺手给关键页面元素加注释,标明“这个元素是标题输入框”,下次选择器失效时你能快速知道要修哪里。
选择器崩了不要慌,先用 codegen 重新录一遍,拿到新的选择器,再对比旧代码找出变化点。这比对着浏览器按 F12 瞎猜效率高多了。
5.3 验证码、登录风控与合规处理
自动化登录时,平台会弹出验证码或要求滑块验证。很多教程会教你用第三方打码平台或者模拟拖拽,但我要明确说:这类“绕过风控”的操作我不建议做,也不展开讲。原因很简单,验证码本身是平台安全策略的一部分,尝试绕过它既违反平台规则,也容易把自己的账号搭进去。
我的处理方式就是前面说的:第一次登录人工完成,把登录态保存下来,后续脚本只在会话有效期内执行。如果哪一天脚本跑起来发现登录失效了,那就停下来,重新跑一次“人工扫码登录”的流程,再继续。这中间损失的不过是几十秒,换来的是账号安全和心理安稳。
注意:凡是脚本里涉及登录态失效的情况,不要偷偷摸摸自动重试太多次。设置一个失败中止逻辑,提示人工介入,反而更省心。
5.4 跑一半卡住:超时和等待策略
这是自动化脚本最常见的一种“死法”:页面加载很慢,脚本等不到目标元素,卡在一个wait_for上直到超时报错。
排查思路是分层的:
- 先看报错超时的元素是哪个,它到底“是否出现过”但“不稳定”,还是“完全没出现”。
- 用截图确认脚本卡住时页面长什么样。很多时候你会发现,页面弹了一个“是否确认离开”的浏览器原生对话框,或者有个遮罩层挡住了按钮,脚本是点击到了遮罩层上。
- 然后用
page.pause()一键进入调试模式。Playwright 支持在代码里插一行page.pause(),运行到这里的时候脚本会暂停,并打开 Playwright Inspector,你可以在这时候手动操作浏览器、断点续跑,效果比传统print调试好很多。
关于等待,我的一条经验是:尽量用操作自带的等待,而不是手动写长超时。比如点击按钮前它自己会等元素可点;填写前可以用expect(...).to_be_visible确保元素真的渲染出来了。真正的网络请求等待才需要page.wait_for_load_state("networkidle")。固定sleep是最后手段,能不用就不用。
5.5 Cursor 生成的代码报错:问题出在哪
Cursor 生成 Playwright 代码的报错,大多数时候不是 Playwright 的问题,而是信息不足。我遇到过的典型案例:
- 它假设了一个不存在的
editor_url,结果goto到一个错误页面。 - 它使用了
fill()填充 contenteditable 编辑器,但运行时无声失败。 - 它生成的等待条件太严苛,等待某个只在有头模式才出现的元素,导致无头运行必挂。
处理思路很明确:不要急着让 Cursor 盲修,先自己从报错信息里定位是“定位、等待、还是流程”哪一类问题,然后再贴足够的信息给它。我推荐的提问格式是:
“下面这行代码报错了:…… 完整报错信息是:…… 当前页面的相关 HTML 片段是:…… 请帮我判断是选择器问题还是等待问题,并给出修改后的代码。”
把报错和现场信息喂进去之后,Cursor 的有效率会显著提升。它偶尔会过度自信地给出一个看似合理但实际跑不通的方案,这种时候别信口开河,直接拿代码去跑一遍验证。和 AI 协作的正确姿势,是把它当“手速很快但对环境半生不熟的同事”,而不是“全知全能的神”。
6. 后续还能怎么玩:扩展方向与我的体会
6.1 扩展方向:多平台分发、定时发布、数据回传
这套“Playwright + Cursor”的组合能力边界非常大,发布头条文章只是起点。顺着同样的技术栈,你可以做非常多的事情:
- 多平台分发。把
publisher.py抽象成接口,每个平台写一个实现类,比如ToutiaoPublisher、WeixinPublisher、ZhihuPublisher。配置文件里存各平台的登录态和编辑页 URL,一份内容发布到 N 个平台。 - 定时发布。如果脚本运行环境是 Linux 服务器,配合 cron 就能实现在指定时间自动执行发布任务。Windows 上也可以用计划任务。但记住前面说的,频率别太夸张。
- 数据回传。发布完不等于结束,你完全可以让 Playwright 再回到文章列表页,抓取初始阅读量、评论数,写进一个 CSV 或数据库,长期积累自己内容的数据曲线。
- 作为 UI 回归测试。如果你在一个内容团队工作,发布流程本身就是一个典型的高频操作流程,完全可以把它组织成 Playwright Test 用例,每次后台改版后跑一遍,验证主流程没有回归。
在测试工程化的语境下,这套东西的价值就更直接了。Playwright 本身就是为了测试而生的,codegen 录制的操作、expect断言的模式、page.screenshot的现场留存,都是标准的测试方法论。与其把它当成一次性的自动发布脚本,不如当成一个可以长期沉淀的“UI 自动化测试资产”。
6.2 我的体会:全自动不如可控的半自动
最后说一句个人体会。这套项目做完之后,我最大的收获反而不是“省了几分钟发布文章的时间”,而是形成了一种思维:任何重复的网页操作,都能被脚本化。看到一个新的后台系统,我第一反应是“它的操作路径是什么、状态返回值是什么、哪些环节能被自动化”,而不是“这个太复杂了不搞了”。
但同时我也越来越确定:所谓的“全自动”往往是伪命题。真正常态的自动化流程里,总有那么几个节点需要人工介入,比如登录扫码、内容审核、异常处理。把这些节点识别出来,把自动化用在重复度最高、出错影响最小的环节,才是最舒服、最长久的使用姿势。
Cursor 在这个流程里的角色,是一个能快速把你的想法变成代码、又能基于报错迭代修改的协作者。Playwright 的角色,则是一双对网页操作极度精准的手。两者结合,真正替代掉的不是“思考”,而是“重复”。我建议你也找一个自己工作中最烦、最重复的网页操作练练手,别贪多,就从“回车就能发布一篇文章”这种小事开始。跑通的那一瞬间,那种“浏览器自己动起来了”的感觉,确实挺上头的。