首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >WorkBuddy 自动化卡在登录墙:浏览器明明开着、账号明明登着,CDP 却连不上,我用 ctypes 绕了过去

WorkBuddy 自动化卡在登录墙:浏览器明明开着、账号明明登着,CDP 却连不上,我用 ctypes 绕了过去

原创
作者头像
用户12800671
发布于 2026-10-09 23:02:38
发布于 2026-10-09 23:02:38
50
举报

做自动化的同学大概都遇到过这种场面:目标页面就开在眼前的浏览器里,账号也是登录状态,你觉得这活儿三分钟就能干完。

我昨天就栽在这上面,而且一栽就是半小时。把我的三条死路和一个活路写下来,希望你别重复。

需求很简单:代填一张已经打开的表单

用户在浏览器里打开了某个后台的申请表,说「你接手吧,我吃饭去了」。

听起来毫无难度:页面开着、登录态也在,我只要连上去,填六个框,点提交。

第一条路:CDP。三分钟就否掉了

正常做法是走 Chrome DevTools Protocol。但 CDP 有个前提:浏览器必须是在启动时就带了 --remote-debugging-port 参数。

这个参数没有任何办法事后补——进程已经在跑了,你不可能给它加命令行参数。

查了一圈,他机器上所有相关端口全是关的:

代码语言:bash
复制
9222 关
9227 关
9334 关
9335 关

而他的浏览器确实是活的、标题也确实是那个表单页。于是问题变成:看得见,摸不着。

第二条路:复制用户数据目录,自己起一个实例

这是最直觉的想法:把用户的浏览器数据目录复制一份,用这份副本起一个独立实例,登录态不就跟着过来了?

第一版我就踩了坑——复制错了目录。我复制的是安装目录(940 多 MB 的浏览器程序文件),里面有零个登录态。真正的数据目录在 %LOCALAPPDATA% 下面。

第二版找对了目录,但撞上更硬的一堵墙:

代码语言:bash
复制
shutil.copytree(...) → PermissionError: [WinError 32] 另一个程序正在使用此文件

运行中的 Chromium 系浏览器对 Cookies 数据库持独占锁。我甚至搬出了系统自带的取证级工具 esentutl /y(专门用来强读被占用文件的),结果返回码还是失败。

结论:只要用户浏览器在运行,你就别想复制它的登录态。 这条路彻底不通。

第三条路:起干净实例,让用户扫码

干净实例没有登录态,那就扫码。我起了个独立实例、把二维码截图发过去,用户扫了。

然后我意识到两个更麻烦的事:

  1. 二维码有有效期。从"生成"到"用户看到并掏出手机"之间隔了几分钟,码就过期了。
  2. 扫码写入的 cookie 落盘时机不可控。实例一旦被回收,登录态就没了,等于白扫。

用户扫了两次,两次都没真正登上。这时候他已经很不耐烦了——完全合理,因为我确实在反复折腾。

活路:绕开浏览器接口,直接模拟人

冷静下来想:我要操作的是一个真实存在于屏幕上的窗口。既然程序接口进不去,那我以人的身份操作它总行——人怎么用,我就怎么用。

用 Python 的 ctypes 直接调 Windows 的 user32,不需要装任何第三方库:

代码语言:python
复制
import ctypes, time
u32 = ctypes.windll.user32

# 关键第一步:声明 DPI 感知。少这一行,后面所有坐标都是错的
try:
    ctypes.windll.shcore.SetProcessDpiAwareness(2)   # 每显示器 v2
except Exception:
    u32.SetProcessDPIAware()

# 把目标窗口切到前台(被遮挡的窗口渲染会冻结,必须切前台)
u32.ShowWindow(hwnd, 9)          # SW_RESTORE
u32.SetForegroundWindow(hwnd)

# 点一下
u32.SetCursorPos(x, y)
u32.mouse_event(0x0002, 0, 0, 0, None)   # 左键按下
time.sleep(0.07)
u32.mouse_event(0x0004, 0, 0, 0, None)   # 左键抬起

# 滚轮(第四参数是滚动量,单位 120 的倍数)
u32.mouse_event(0x0800, 0, 0, ctypes.c_long(-480), None)

文字输入别一个字一个字敲,中文很容易乱。用剪贴板 + Ctrl+V 一次性灌进去:

代码语言:python
复制
CF_UNICODETEXT, GMEM_MOVEABLE = 13, 0x0002
k32 = ctypes.windll.kernel32

# 这两行是保命的:句柄是 64 位,不声明 restype,ctypes 会按 32 位截断
k32.GlobalAlloc.restype  = ctypes.c_void_p
k32.GlobalLock.restype   = ctypes.c_void_p

u32.OpenClipboard(None)
u32.EmptyClipboard()
data = text.encode("utf-16-le") + b"\x00\x00"
h = k32.GlobalAlloc(GMEM_MOVEABLE, len(data))
p = k32.GlobalLock(h)
ctypes.memmove(p, data, len(data))
k32.GlobalUnlock(h)
u32.SetClipboardData(CF_UNICODETEXT, h)
u32.CloseClipboard()
# 然后发 Ctrl+V

两个把我坑掉半小时的细节

坑一:你在图上量到的坐标,不是屏幕坐标

我截了窗口图,在图上量出输入框在 (555, 449),直接把这两个数当屏幕坐标用了。点了七八次,每次都点空。

原因很隐蔽:我用来看图的工具会把大图缩小显示。窗口实际宽 1575 像素,但显示出来只有约 1080 像素。我在"显示图"上量的坐标,乘上真实比例才对:

代码语言:bash
复制
真实坐标 = 图上坐标 × (1575 / 1080) ≈ 图上坐标 × 1.458

按这个比例重算,一次就点中了。

这条经验的价值在于:当你发现"点击没反应、但代码逻辑完全正确"时,先怀疑坐标,再怀疑选择器。而且要通过截图看结果来验证,不要靠猜。

坑二:剪贴板那步让脚本无声崩掉

第一次跑,脚本在写剪贴板那一步直接退出,连错误日志都没留下——因为进程被强杀了,标准输出也没了。

根因是:GlobalAlloc 返回的是一个64 位句柄,而 ctypes 在没有 argtypes/restype 声明时默认按 c_int(32 位)处理,高 32 位被直接截断。截断后的句柄传给 GlobalLock,拿到空指针,memmove 就炸了。

加上 restype = ctypes.c_void_p 立刻就好。

顺带一条:脚本要自己接管日志。我后来的写法是第一行就把 stdout 重定向进文件:

代码语言:python
复制
import io, sys
sys.stdout = io.open("run.log", "w", encoding="utf-8", buffering=1)

否则"进程被回收"这类问题,你连现场都看不到。

最难的一关其实不是技术

表单填完了,点提交——没反应。

我又试了两次,还是没反应。差点去怀疑按钮的点击方式不对。结果把整页从上到下逐屏截了一遍,发现了一个必填项正好藏在滚动两格之间:往上滚一点看不到,往下滚一点也看不到,而它有红色报错「请填写此项」。

补上它,提交一次就过了。

两条教训:

  • "点了没反应"先查必填项,别急着怀疑选择器或点击方式。尤其是长表单,滚动会让你漏掉夹在中间的项目。
  • 长表单要逐屏扫一遍,不要跳着滚。跳着滚一定会漏。

最后:能用接口就别用鼠标

这半小时换来一个很实在的结论:模拟鼠标虽然万能,但慢十倍、脆十倍。它是最后的兜底,不该是第一选择。

所以我把这件事固化成了流程的第一步——让用户双击一个 .bat,用调试端口启动浏览器:

代码语言:bat
复制
@echo off
taskkill /F /IM 浏览器.exe >nul 2>&1
timeout /t 4 >nul
start "" "路径\浏览器.exe" --remote-debugging-port=9222 --remote-allow-origins=* https://目标网址

用户花 30 秒双击一下,我就能全程走 CDP 后台完成,快且稳。注意一个细节:调试端口只在浏览器未运行时启动才生效,所以 bat 里要先杀掉已有进程。

一个 30 秒的配合,换掉半小时的瞎点。这笔账很划算。


如果你也在做浏览器自动化,欢迎说说你遇到过的"看得见摸不着"的情况——我猜不止我一个人在坐标上栽过。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 需求很简单:代填一张已经打开的表单
  • 第一条路:CDP。三分钟就否掉了
  • 第二条路:复制用户数据目录,自己起一个实例
  • 第三条路:起干净实例,让用户扫码
  • 活路:绕开浏览器接口,直接模拟人
  • 两个把我坑掉半小时的细节
    • 坑一:你在图上量到的坐标,不是屏幕坐标
    • 坑二:剪贴板那步让脚本无声崩掉
  • 最难的一关其实不是技术
  • 最后:能用接口就别用鼠标
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档