无头服务器上的浏览器自动化:Xvfb + 常驻 Chromium + CDP 实战

举报
yd_276135645 发表于 2026/10/22 10:21:05 2026/10/22
【摘要】 在无图形界面的服务器上做浏览器自动化,登录态复用与故障排查一直是两块硬骨头。本文给出一套实测组合:Xvfb 虚拟显示 + systemd 托管的常驻 Chromium + CDP 调试端口 + noVNC 人工兜底,并记录了四个坑:CDP 端口暴露风险、隐藏元素怎么点、登录态该看哪个页面、CodeMirror 正文怎么写进去。

在无图形界面的 Linux 服务器上做浏览器自动化,最难受的往往不是写代码,而是两件事:登录态没法复用,出了故障只能靠日志猜。这篇文章记录一套实测可用的组合:Xvfb 提供虚拟显示,常驻 Chromium 暴露 CDP 调试端口,noVNC 给人工兜底,Playwright 通过 connect_over_cdp 直接接管同一个浏览器实例。

一、为什么不让每次任务都新建浏览器

大多数脚本的默认写法是 browser = pw.chromium.launch(),每次跑完就销毁。这在无人值守场景里有个致命问题:需要短信验证码的站点,每次都得有人重新登录。

换一个思路:让浏览器长期活着,登录由人做一次,后续所有脚本都连上去复用同一个 profile。Cookies、localStorage、IndexedDB 全部留在磁盘上的 --user-data-dir 里,进程重启也不丢。

二、三块拼图

1. 虚拟显示。 没有物理屏也要有 DISPLAY,否则有头浏览器起不来:

Xvfb :1 -screen 0 1600x900x24 &
export DISPLAY=:1

2. 常驻 Chromium。 关键是 --remote-debugging-port 和固定的 --user-data-dir:

chromium \
  --user-data-dir=/root/.remote-chrome \
  --remote-debugging-port=9222 \
  --remote-debugging-address=127.0.0.1 \
  --no-first-run --no-default-browser-check \
  --window-size=1600,900

用 systemd 托管,Restart=always,崩了自动拉起来,不需要人守着。

3. 人看的入口。 x11vnc 抓 Xvfb 的帧缓冲,websockify 把 VNC 转成 WebSocket,前面套一层 Basic 认证,就能在浏览器里点鼠标了。这一层是"故障兜底":脚本卡住时,人可以直接看屏幕、手动点一下。

三、Playwright 侧的写法

from playwright.sync_api import sync_playwright

with sync_playwright() as pw:
    browser = pw.chromium.connect_over_cdp("http://127.0.0.1:9222")
    ctx = browser.contexts[0]        # 复用已登录的上下文
    page = ctx.pages[0]              # 复用已有标签页

connect_over_cdp 和 launch 的区别在于:它不创建新 profile,拿到的是正在跑的那个实例。所以登录态、扩展、甚至当前打开的页面都是现成的。注意 browser.close() 在 CDP 连接下只断开连接,不会杀掉浏览器进程,这一点很符合我们的需求。

四、四个踩过的坑

坑 1:CDP 端口别暴露到公网。 CDP 的权限等价于"控制整个浏览器",能读所有 Cookie、能执行任意 JS。必须显式写 --remote-debugging-address=127.0.0.1。要看界面,走 VNC 那条独立通道,别为了省事把 9222 开到 0.0.0.0。

坑 2:"element is not visible"不一定是真的看不见。 很多 Vue/React 页面会同时渲染桌面端和移动端两份 DOM,其中一份 display:none。Playwright 的自动等待会挑到隐藏的那一份然后报错。改法是在页面里用 JS 自己选:

page.evaluate("""() => {
  const els = [...document.querySelectorAll('.status-btn__text')];
  const el = els.find(e => e.offsetParent !== null);
  el.closest('button').click();
}""")

坑 3:判断登录态要挑权威页面。 首页/列表页的头部经常渲染缓存数据,看起来还登录着,实际会话早就过期。找一个"未登录必然重定向"的页面(比如控制台首页),看 URL 有没有被踢到登录页,比读页面文字可靠得多。

坑 4:内容型站点优先用原生输入。 CodeMirror、contenteditable 这类编辑器直接 fill() 有时不触发内部状态更新,标题能进、正文却是空的。稳妥做法是先点进编辑区拿到焦点,再用 keyboard.insert_text() 逐字写入,让它走完整的 beforeinput 事件链。

五、成本与收益

一次性成本大概是一个小时:两个 systemd unit、一个 VNC 反代配置。换来的收益是——登录态可以跨任务复用,定时任务不再因为一次验证码全军覆没;出错时能直接看屏幕,不用盲猜选择器;同一套环境既能给人看,又能给脚本用。

如果你的自动化里也有"必须人工登录一次"的环节,这套结构的投入产出比相当高。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。