🌟 为什么我决定扔掉 Selenium?——一个被 selector 失败、隐式等待和 CI 崩溃折磨三年的自白
凌晨2:47,手机震了第七次。
钉钉弹出告警:“【大促价格监控】任务 #JD-HEADPHONES-03 —— FAILED(StaleElementReferenceException)”。我抓了把头发,盯着终端里那行熟悉的红字:Message: stale element reference: element is not attached to the page document。再往下翻,是另一个幽灵:TimeoutException: Message: timeout: Timed out receiving message from renderer。
这不是演习。这是双十一大促前夜的真实战况。我们用 Selenium 写的 12 个核心电商页面价格巡检脚本,在 Chrome v125 升级后的首波流量高峰中集体“诈尸”——不是全挂,而是间歇性抽风:有时能跑通,有时卡在搜索框输入后不点搜索按钮,有时点了却把“加入购物车”误点成页脚的“联系我们”。排查三天,发现根源竟是:某平台首页悄悄把 <button class="btn-buy"> 改成了 <div class="action-btn js-buy-btn">,而我们的 WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.CLASS_NAME, "btn-buy"))) 直接哑火。
不是 Selenium 不好。它稳、成熟、生态全。但我的需求变了。老板甩来一句话:“帮我点开京东首页→搜‘无线耳机’→拉到第3个商品→截图价格”,我却要花 42 行代码:
- 启动 ChromeOptions 加一堆规避检测参数
- 等首页加载完再等搜索框可点击
- 输入后显式等搜索按钮出现再 click
- 解析商品列表时得用 XPath 定位“第3个含‘自营’且非广告”的节点
- 滚动到该元素再截图……
直到公司内部 Hackathon 上,隔壁组小哥用 Claude Code + Chrome MCP,10 分钟做完同一件事。更魔幻的是——产品同学现场语音说:“把刚才脚本改成去小红书搜‘降噪耳机测评’,只取笔记标题和点赞数”,他敲了三行指令,回车,跑通。页面结构早因灰度改版变了个样,但脚本没崩,因为 LLM 理解了“标题”和“点赞数”的语义,而不是死磕 class="note-title"。那一刻我关掉了 PyCharm,打开了终端。
🔧 我的零代码自动化工作流长啥样?(附真实终端录屏思维导图)
现在我的自动化流程像呼吸一样简单:我说人话 → 机器听懂 → 浏览器执行 → 我拿结果。
整个链路极简到只有四层:
自然语言指令
↓
Claude Code(本地运行的 LLM,带 browser_tool 插件)
↓
Chrome MCP Server(独立进程,通过 WebSocket 接管 Chrome)
↓
Chrome DevTools Protocol(原生协议,直接操作 DOM/Network/Runtime)
重点划掉一个常见误解:MCP 不是插件,不是浏览器扩展,更不是 WebDriver 的包装壳。它是站在 Chrome 肩膀上的“新协议层”——绕过 Selenium 那套抽象又易碎的 WebElement 对象模型,直接走 CDP 发送 Runtime.evaluate 或 DOM.scrollIntoViewIfNeeded。这意味着:没有 StaleElementReferenceException(元素不存在于当前 DOM 树?CDP 会实时查),没有 NoSuchElementException(找不到?LLM 会尝试语义匹配而非硬 selector)。
我把它封装成一行命令(macOS zsh alias):
alias mcp-start='chrome-mcp --port 3000 --headless=false & sleep 1 && claude-code --mcp-url ws://localhost:3000 --max-steps 8'
启动后,终端就安静了。接着我在飞书发条语音:“把知乎今日热榜前5条标题存成 CSV”,Claude Code 自动拆解为:
navigate("https://www.zhihu.com/hot")wait_for_element("section.hot-list")extract_text("div.List-item h2", limit=5)save_csv(["title"], extracted_data)
整个过程无 selector、无显式等待、无 driver.quit()。
兴奋劲儿还没过,现实就给了我四记重拳:
坑1:Chrome 调试端口被 macOS 默默封印chrome-mcp 启动时报错 Connection refused。查日志发现它默认连 http://127.0.0.1:9222/json,但 brew 安装的 Chrome 根本不开放这个端口。解决方案:不用 brew 启动,改用命令行强制开启:
open -a "Google Chrome" --args --remote-debugging-port=9222 --no-sandbox --disable-gpu
(加 --no-sandbox 是为了本地开发免权限纠缠,生产环境请用 Docker)
坑2:LLM 工具太“老实”,不会高亮元素
我要实现“滑动到商品卡片并加红色边框高亮”,但 browser_tool.click() 和 .type() 根本不支持。翻源码发现 browser_tool 底层调用 execute_js(),于是手动 patch(~/.local/lib/python3.11/site-packages/browser_tool/tool.py):
# 在 execute_js 方法末尾加这三行:
if "highlight" in kwargs:
js_code = f"arguments[0].style.outline='3px solid red';"
self._execute_cdp("DOM.highlightNode", {"nodeId": node_id})
现在指令里写 “高亮第一个商品卡片” 就自动生效。
坑3:中文页面的 :contains() 是个伪命题document.querySelector("button:contains('立即购买')") 在 Chrome 控制台直接报错——CSS 选择器根本没这语法!解决:让 Claude 自动生成兼容代码:
Array.from(document.querySelectorAll('button'))
.find(el => el.textContent?.trim().includes('立即购买'))
并在 MCP 的 JS 执行层自动注入此逻辑,无需我手写。
坑4:MCP Server 日志干净得诡异,但 Chrome 没反应
查了一小时网络、防火墙、端口占用,最后发现是 Chrome 版本太高(v126+)。降级到 v124.0.6367.91 后秒通。我写了版本锁定脚本防踩坑:
# macos-chrome-lock.sh
curl -L https://edgedl.me.gvt1.com/edgedl/chrome/mac/stable/ChromeV124.dmg -o /tmp/chrome.dmg
hdiutil attach /tmp/chrome.dmg && cp -R "/Volumes/Google Chrome/Google Chrome.app" /Applications/
🛠️ 我的“自然语言指令”写作心法(比写正则还简单)
别把 LLM 当 AI,当一个较真的实习生——你给模糊需求,它就给你模糊结果。我的黄金模板:
“在 [页面描述] 上,[动作],[目标元素特征],[后续操作]”
✅ 好例子:
“在京东商品搜索页上,输入‘AirPods Pro 二代’,点击第一个带‘自营’字样的商品卡片,截图右上角价格区域”
❌ 糟糕例子:
“点那个蓝色的购买按钮” → 它真会去找所有蓝色文字,包括页脚“©2024 京ICP备XXX号”里的“蓝”字
实战技巧:主动定义失败路径。加一句:
“如果找不到包含‘立即抢购’的按钮,请返回错误信息:‘未定位到立即抢购按钮,请检查页面是否已跳转至商品详情页’”
这样脚本不会静默失败,而是抛出可读错误,方便快速定位是页面没加载完,还是文案真改了。
📊 效果对比:同一任务,Selenium vs Claude+MCP(真·表格)
| 维度 | Selenium(Python) | Claude+Chrome MCP |
|---|---|---|
| 编写耗时 | 28 分钟(含 selector 调试) | 3 分钟(语音转文字+微调) |
| 页面改版后维护 | 修改 5 处 selector + 等待逻辑 | 指令重写 1 句(“把‘加入购物车’改成‘立即抢购’”) |
| 错误可读性 | NoSuchElementException: Message: no such element: Unable to locate element... | “找不到包含‘立即抢购’的按钮,请确认页面已跳转至商品详情页” |
| 团队协作成本 | 需 Python 基础 + 元素定位知识 | 产品用飞书发语音:“把昨天的脚本改成抓小红书笔记的点赞数” |
最震撼的是维护成本。上周拼多多首页改版,Selenium 脚本崩了 3 处;而 MCP 版本,我只改了指令里一个词:“把‘百亿补贴’换成‘限时秒杀’”,重跑即通。
🚀 我的生产环境落地清单(已验证,非纸上谈兵)
别光顾着激动,生产环境必须稳如老狗:
必装三件套(版本锁死):
npm install -g [email protected] pip install claude-code==0.12.0 # Chrome v124 二进制包已打包好,见 internal-nexus/chrome-v124/安全红线(血泪教训):
MCP Server 绝不暴露公网!本地开发用127.0.0.1:3000;CI 环境用 Docker network 隔离:# docker-compose.yml services: mcp-server: image: ghcr.io/mozilla/chrome-mcp:0.4.2 ports: ["127.0.0.1:3000:3000"] network_mode: "bridge" claude-runner: build: . depends_on: [mcp-server] # 不暴露任何端口,仅容器内通信性能兜底:
Claude 加--max-steps 8防止无限循环;Chrome 启动加--disable-gpu --no-sandbox --disable-dev-shm-usage(尤其 Docker 内)。最后的真心话:
它不是银弹。遇到 Canvas 渲染的滑块验证码、WebGL 动画页、iframe 嵌套超过 4 层的银行系统,我依然切回 Selenium——老将出马,一个顶俩。但日常 80% 的场景:竞品价格监控、SEO 标题抓取、表单回归测试、运营活动页巡检……真的可以做到:张嘴就来,说完就跑,改完就用。
扔掉 Selenium 不是叛逃,而是升级武器库。当老板说“把昨天脚本改成抓抖音直播间在线人数”,我不再打开 IDE,而是打开飞书,按住说话键:“在抖音直播间页,提取右上角‘在线X万人’的数字,每分钟存一次”。
世界终于回到了它该有的样子:人指挥机器,而不是人迁就机器的语法。