做自动化的同学大概都遇到过这种场面:目标页面就开在眼前的浏览器里,账号也是登录状态,你觉得这活儿三分钟就能干完。
我昨天就栽在这上面,而且一栽就是半小时。把我的三条死路和一个活路写下来,希望你别重复。
用户在浏览器里打开了某个后台的申请表,说「你接手吧,我吃饭去了」。
听起来毫无难度:页面开着、登录态也在,我只要连上去,填六个框,点提交。
正常做法是走 Chrome DevTools Protocol。但 CDP 有个前提:浏览器必须是在启动时就带了 --remote-debugging-port 参数。
这个参数没有任何办法事后补——进程已经在跑了,你不可能给它加命令行参数。
查了一圈,他机器上所有相关端口全是关的:
9222 关
9227 关
9334 关
9335 关而他的浏览器确实是活的、标题也确实是那个表单页。于是问题变成:看得见,摸不着。
这是最直觉的想法:把用户的浏览器数据目录复制一份,用这份副本起一个独立实例,登录态不就跟着过来了?
第一版我就踩了坑——复制错了目录。我复制的是安装目录(940 多 MB 的浏览器程序文件),里面有零个登录态。真正的数据目录在 %LOCALAPPDATA% 下面。
第二版找对了目录,但撞上更硬的一堵墙:
shutil.copytree(...) → PermissionError: [WinError 32] 另一个程序正在使用此文件运行中的 Chromium 系浏览器对 Cookies 数据库持独占锁。我甚至搬出了系统自带的取证级工具 esentutl /y(专门用来强读被占用文件的),结果返回码还是失败。
结论:只要用户浏览器在运行,你就别想复制它的登录态。 这条路彻底不通。
干净实例没有登录态,那就扫码。我起了个独立实例、把二维码截图发过去,用户扫了。
然后我意识到两个更麻烦的事:
用户扫了两次,两次都没真正登上。这时候他已经很不耐烦了——完全合理,因为我确实在反复折腾。
冷静下来想:我要操作的是一个真实存在于屏幕上的窗口。既然程序接口进不去,那我以人的身份操作它总行——人怎么用,我就怎么用。
用 Python 的 ctypes 直接调 Windows 的 user32,不需要装任何第三方库:
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 一次性灌进去:
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 像素。我在"显示图"上量的坐标,乘上真实比例才对:
真实坐标 = 图上坐标 × (1575 / 1080) ≈ 图上坐标 × 1.458按这个比例重算,一次就点中了。
这条经验的价值在于:当你发现"点击没反应、但代码逻辑完全正确"时,先怀疑坐标,再怀疑选择器。而且要通过截图看结果来验证,不要靠猜。
第一次跑,脚本在写剪贴板那一步直接退出,连错误日志都没留下——因为进程被强杀了,标准输出也没了。
根因是:GlobalAlloc 返回的是一个64 位句柄,而 ctypes 在没有 argtypes/restype 声明时默认按 c_int(32 位)处理,高 32 位被直接截断。截断后的句柄传给 GlobalLock,拿到空指针,memmove 就炸了。
加上 restype = ctypes.c_void_p 立刻就好。
顺带一条:脚本要自己接管日志。我后来的写法是第一行就把 stdout 重定向进文件:
import io, sys
sys.stdout = io.open("run.log", "w", encoding="utf-8", buffering=1)否则"进程被回收"这类问题,你连现场都看不到。
表单填完了,点提交——没反应。
我又试了两次,还是没反应。差点去怀疑按钮的点击方式不对。结果把整页从上到下逐屏截了一遍,发现了一个必填项正好藏在滚动两格之间:往上滚一点看不到,往下滚一点也看不到,而它有红色报错「请填写此项」。
补上它,提交一次就过了。
两条教训:
这半小时换来一个很实在的结论:模拟鼠标虽然万能,但慢十倍、脆十倍。它是最后的兜底,不该是第一选择。
所以我把这件事固化成了流程的第一步——让用户双击一个 .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 删除。