Playwright + CDP:接管已登录的浏览器,别再跟 Cookie 死磕

举报
yd_219023509 发表于 2026/10/28 14:39:34 2026/10/28
【摘要】 会话 Cookie 一关浏览器就没了,脚本第二次必挂。这篇讲清楚为什么会丢、怎么用持久化 profile 和 CDP 接管把它稳住,附带跨域、CSRF、fetch 劫持几个坑的解法。

做浏览器自动化,最烦的不是定位元素,是登录态。脚本跑一次好好的,第二次就 403 not login,翻半天日志才发现 Cookie 过期了,或者压根没落盘。

这篇把我踩过的坑和最后跑通的方案写清楚,代码可以直接抄。

一、先搞清楚登录态为什么丢了

浏览器里的 Cookie 分两种:

  • 持久 Cookie:带 expires / max-age,写进磁盘,重启还在。
  • 会话 Cookie:只有 session,进程一退就没了。

华为云、大多数 SSO 系统用的恰恰是后者。你手动登录完,关掉窗口,那个 IAMIdentity 之类的东西就一起蒸发了。

Playwright 的 browser.close() 和 context.close() 都会走正常的浏览器退出流程,会话 Cookie 同样被清掉。所以「第一次能跑,第二次不行」几乎是必然。

二、方案 A:持久化 user data dir

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    ctx = p.chromium.launch_persistent_context(
        r"D:\auto\profile",
        channel="chrome",
        headless=False,
        locale="zh-CN",
    )
    page = ctx.pages[0]
    page.goto("https://example.com/login")
    input("登录完成后回车…")
    # 后续操作都在这个 ctx 里做

launch_persistent_context 会把整个 profile 写到磁盘,重启进程后 Cookie、localStorage、IndexedDB 都在。

注意两点:

1. channel="chrome" 用系统装的 Chrome,比 Playwright 自带的 Chromium 兼容性好,很多站点的风控认这个 UA。

2. 同一个 user_data_dir 不能同时被两个 Chrome 实例占用,跑之前先把窗口关干净。

三、方案 B:CDP 接管,进程不关就不会丢

方案 A 有个前提:你得在一个进程里把活干完。如果要做多步、跨很多次调用,更稳的是让 Chrome 常驻,用 CDP 连上去。

先启动带调试端口的 Chrome:

"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
  --remote-debugging-port=9222 ^
  --user-data-dir="D:\auto\profile" ^
  --no-first-run ^
  --no-default-browser-check

然后任何脚本、任何时候都能连:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp("http://127.0.0.1:9222")
    ctx = browser.contexts[0]
    page = ctx.pages[0] if ctx.pages else ctx.new_page()
    print(page.url)
    # 这里拿到的就是你手动登录过的那个会话

connect_over_cdp 不会新建浏览器,只是接管已有的。你手动点了登录,脚本立刻就能用;脚本跑完 Browser 也不关,人还能接着操作。两边交替干活,这是最舒服的形态。

Chrome 156 之后有个变化:默认 profile 加 --remote-debugging-port 会被拒绝,必须显式给一个 --user-data-dir。上面那条命令已经带上了。

四、页面里的 fetch 被劫持了怎么办

有些站点会注入 SDK 劫持 window.fetch,做一些埋点或者代理转发。这时候在 page.evaluate 里直接 await fetch(...),可能直接抛异常,堆栈还指向一堆压缩过的 sourcemap,很难查。

换成 XMLHttpRequest 最省事:

new Promise(res => {
  const x = new XMLHttpRequest();
  x.open('POST', url, true);
  x.withCredentials = true;
  x.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
  x.onload = () => res([x.status, x.responseText]);
  x.onerror = () => res([-1, 'network error']);
  x.send(JSON.stringify(payload));
})

XHR 不走 window.fetch 那条链,劫持不到。

五、跨域请求的两个必要条件

在页面上下文里请求另一个域名,服务端必须同时给出:

Access-Control-Allow-Origin: https://当前站点
Access-Control-Allow-Credentials: true

并且脚本里要带 withCredentials: true / credentials: 'include',否则 Cookie 不会带过去。

另外,服务端返回的自定义响应头(比如 CSRF token)默认对前端不可见,必须在 Access-Control-Expose-Headers 里列出来,否则 x.getResponseHeader('csrf') 拿到的是 null。

六、几个实战细节

CSRF 要先取再带。一般是先 GET 一个接口,从响应头里拿 token,再把它塞进后续每个请求的同名请求头。token 会过期,401/403 之后要重新取一次。

别用 wait_until="networkidle" 等 SPA。很多站点有长连接或者轮询,永远等不到 idle。用 domcontentloaded 加显式 wait_for_selector,稳得多。

登录检测别只看 URL。页面跳到首页不等于登录成功,直接打一个需要鉴权的接口,看返回是不是 not login,才是准的。

WAF 会拦裸请求。curl 能过的接口,浏览器里未必能过,反之亦然。带齐 Origin、Referer、User-Agent 再试。

七、最后

自动化这件事,九成的坑都在状态管理上。元素定位、等待、重试这些都是小问题,真正让人卡一天的是「我明明登录了,为什么它说没登录」。

把会话这东西当成资源来管:要么持久化到磁盘,要么让进程常驻通过 CDP 接管。想明白这一层,剩下的都是体力活。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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