ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用DrissionPage搞定中文点选验证码:识别、坐标换算与自动化点击全攻略

用DrissionPage搞定中文点选验证码:识别、坐标换算与自动化点击全攻略 入行自动化测试这几年遇到最多也最让人头疼的一个场景就是各种验证码。数字的、滑块的、算数的还算好对付真正让我卡了好几个晚上的是“中文点选”——页面上出一张乱序文字图让你按顺序点击“技、术、创、新”之类的文字。手动点太费劲写脚本去点又需要同时解决识别精度和点击坐标换算的问题。这篇就记录一下我最终用 DrissionPage 把整个流程跑通的全过程包括完整代码、坐标计算方式、踩过的那些坑以及轮滚、iframe、动态加载这类高频问题怎么处理。如果你是做自动化办公、内网系统自动化、或单纯想学 DrissionPage 操作网页元素的这篇可以直接照着抄。1. 整体设计与思路拆解1.1 中文点选验证码到底在验证什么先搞清楚原理脚本才能写得顺。中文点选验证码本质上是一个“图片 文字提示 点击坐标”的组合验证。系统生成一张背景图背景上散落着若干汉字同时在提示区告诉你需要按什么顺序点比如“请依次点击中 文 验 证”。你点的顺序、点的位置都要匹配才能通过。因为汉字不是简单滑块那种规则形状识别难度比数字字母要高很多自动化工具在这种验证码前面直接从“工具”变“人工”。它的判定逻辑一般是后台在生成验证码图片时会记录每个文字在图片中的真实坐标。你提交的坐标如果与真实坐标距离小于某个阈值并且顺序正确就判定通过。所以我们的自动化目标非常明确拿到图片 → 找出文字坐标 → 按提示顺序模拟点击。这里有一点必须说在前头我只在自己负责维护、有明确授权的内部业务系统和本地测试环境上做这套自动化这也是我写代码前一直坚持的红线。大家如果要复用一定要先确认目标系统的使用条款和授权边界别拿这个去对付没有权限的站点。1.2 为什么选 DrissionPage 而不是 Selenium做网页自动化最多人提的就是 Selenium但我在这个项目里选择 DrissionPage主要是三个原因。第一DrissionPage 不依赖 WebDriver。Selenium 每次都要下载和浏览器版本严格匹配的 chromedriver换个浏览器版本就要重来一遍内网环境还不一定能下载。DrissionPage 直接通过浏览器 DevTools 协议控制 Chromium 内核启动快、免驱、版本兼容性明显好。第二元素定位语法更贴近人话。DrissionPage 的ele()方法用css:、xpath:、属性、文本查找等做前缀就能定位而且一个方法同时支持单个元素和等待条件写起来省很多代码。第三交互能力更顺手。DrissionPage 的click()方法原生支持相对元素左上角的偏移坐标点击这个特性在点选验证码场景里简直是救命的不用自己去算浏览器边框、页面滚动、元素偏移这些乱七八糟的偏移量。对比维度SeleniumDrissionPage浏览器驱动需要对应版本的 WebDriver无需额外驱动安装pip install selenium 下载驱动pip install DrissionPage元素定位find_element 系列语法繁琐ele() 一行搞定偏移点击ActionChains 手动算坐标ele.click(offset(x, y))内置等待需要显式调用 WebDriverWait定位方法内置等待机制滚轮操作ActionChains.key_down 等组合scroll 和 actions.wheel 直接支持1.3 整体自动化流程怎么拆一套完整的流程我拆成了四步打开登录页定位到验证码图片元素并把它截取成图片数据。对图片做识别拿到提示文字在图片内的坐标。通过 DrissionPage 的偏移点击模拟鼠标点这几个坐标点。提交表单判断是否成功不成功就刷新验证码重新来。这四个环节里最容易出问题的是第二步识别和第三步坐标换算。识别不准后面全白搭坐标换算错一个像素点击也会偏移所以我下面会分别把这两个关键环节的代码和计算过程完整展开。2. 环境准备与 DrissionPage 基础操作2.1 环境安装与浏览器初始化环境要求其实很低Python 3.8 以上就行。我本地用 Windows 11 做开发服务器上放了个 Linux 环境跑定时任务DrissionPage 两个平台都支持得很好。pip install DrissionPage装完后它会自动下载匹配的 Chromium 内核。如果你本机已经有 Chrome也可以直接接管现有浏览器进程这在调试脚本时特别方便能看到每一步的实际点击轨迹。from DrissionPage import ChromiumPage, ChromiumOptions # 方式一启动一个全新页面 page ChromiumPage() # 方式二调试推荐接管本机已打开的 Chrome # 先用命令行启动 Chrome 带远程调试端口再用下面的代码连接 # chrome --remote-debugging-port9222 # page ChromiumPage(addr_or_opts127.0.0.1:9222)第一次跑的时候建议用“方式二”因为脚本点击时你能肉眼看到整个浏览器操作过程哪个坐标点偏了、哪个文字没识别到一目了然。等代码稳定了再改成新开页面全自动。启动浏览器后访问登录页page.get(https://example.com/login) page.wait(2)2.2 定位验证码元素并截图中文点选验证码在 DOM 里一般是一个img标签或者一个带背景图的div。我在实际项目里遇到的是一个div包裹的 canvas 图片区域。用 DrissionPage 定位非常直接# 根据 class 定位验证码图片区域 code_elem page.ele(css:.verify-code, timeout10) # 截图并转成图片数据注意这里是元素截图不是整页截图 img_bytes code_elem.get_screenshot(as_bytespng)get_screenshot(as_bytespng)返回的是 PNG 格式的二进制数据后面接 OpenCV 就能把数据流转成图片矩阵。这里要注意一定是对元素本身截图而不是对整个页面截图再裁剪。整页截图的像素和页面滚动位置强绑定坐标换算会多出不少麻烦元素截图直接得到局部图片坐标计算简单很多。2.3 页面滚动与滑滚轮操作搜索热词里出现了“drissionpage 滑动滚轮”这里是真实高频需求因为验证码不一定总在第一屏。如果登录页较长验证码被折叠在下方直接截图会截出空白区域点选坐标也会整体偏移。这时候必须先滚动页面。DrissionPage 里滚动有两类思路一种是直接控制页面滚动条另一种是用鼠标滚轮动作模拟真实交互。我按场景分别说下。第一种页面滚动条直接滚动# 滚动到底部 page.scroll.to_bottom() # 滚回顶部 page.scroll.to_top() # 向下滚动 500 像素 page.scroll.down(500) # 向上滚动 300 像素 page.scroll.up(300)第二种模拟鼠标滚轮# 在当前位置执行滚轮动作第一个参数是水平偏移第二个是垂直偏移 # 正数代表向下滚负数代表向上滚 page.actions.wheel(0, 500)两种方式什么区别如果是普通的长页面page.scroll.to_bottom()足够了。但有些页面监听了鼠标滚轮事件监听逻辑写在wheel事件里用scroll()直接改滚动条位置不会触发事件页面内容可能不会按预期加载。这时候就必须用page.actions.wheel()模拟真实的滚轮操作让页面认为用户真的在滚鼠标。我遇到过一个场景页面上半部分是表单下半部分是验证码验证码区域用懒加载实现了必须滚到附近才动态生成。用scroll.down(500)好几次都没触发加载换成page.actions.wheel(0, 500)就正常了。排查原因就是页面绑定了 wheel 事件去触发加载逻辑。另外如果你要点击的元素可能不在当前视口内DrissionPage 的click()其实会自动把元素滚动到可见区域再点击但为了截图坐标准确我还是建议先手动让验证码区域完整露出视口再进行截图和后续偏移点击避免意外滚动导致的坐标错位。3. 中文点选验证码的识别与点击实现3.1 两种识别方案怎么选拿到验证码图片后核心任务就变成了“找字”。目前主流方案有两种。第一种是模板匹配。提前准备好每个汉字的图片模板然后用 OpenCV 的matchTemplate在验证码背景图里找最匹配的位置。优点是完全离线运行速度快、可控性强缺点是需要提前准备模板而且模板风格和验证码字体差异大的时候准确率会下降。第二种是深度学习 OCR。可以用ddddocr这种开箱即用的库它自带目标检测模型可以直接返回图中每个文字的外框坐标。优点是省去准备模板对多种字体泛化能力好缺点是依赖模型文件第一次运行要下模型有些内网环境不方便而且结果偶尔会带误差。我的建议是如果你只针对一个固定的业务系统做自动化验证码字体和背景风格基本不变用模板匹配最稳可控性最强。如果做的是一个通用工具可能面对不同站点那就上 OCR 识别省事但需要容忍一定的准确率波动。下面两个方案的代码我都贴出来。3.2 方案一OpenCV 模板匹配实现点选识别先准备模板。我是在验证码图片样本里把每个字裁剪出来保存成模板图比如muban/中.png、muban/文.png。模板图片最好能包含文字本身的轮廓背景尽量干净。import cv2 import numpy as np from io import BytesIO def img_bytes_to_cv2(img_bytes): 将 DrissionPage 截图得到的 bytes 转成 OpenCV 图像矩阵 return cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) def match_single_word(bg_img, template_img, threshold0.85): 在背景图中匹配单字模板返回中心坐标 阈值 threshold 表示匹配相似度越大越严格 result cv2.matchTemplate(bg_img, template_img, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(result) if max_val threshold: return None, max_val h, w template_img.shape[:2] # 计算出模板左上角位置后转为中心点坐标 center_x max_loc[0] w // 2 center_y max_loc[1] h // 2 return (center_x, center_y), max_val这段代码里最关键的是cv2.matchTemplate。它的原理是把模板图在背景图上逐像素滑动每滑到一个位置计算一次相似度最终得到一个响应矩阵cv2.minMaxLoc取出相似度最高的位置和值。如果这个最高值低于阈值说明图片里可能没有这个字或者说背景干扰太强把真正的文字位置盖过了。接下来组合出一个完整的识别函数。假设验证码提示区给了一串需要点击的文字比如“请依次点击技 术 创 新”程序提取出这四个字依次去匹配def extract_click_words_from_tip(tip_text): 从提示文本中提取需要点击的汉字列表 具体提取规则要根据实际页面结构来写示例去掉前缀提示词后按空格拆分 prefix_keywords [请依次点击, 请按顺序点击, 请点击] for kw in prefix_keywords: if kw in tip_text: tip_text tip_text.replace(kw, ) # 去掉冒号、逗号、空白字符 for ch in [, :, , ,, ]: tip_text tip_text.replace(ch, ) return list(tip_text)然后用一个循环去匹配并保存结果tip_text 请依次点击技 术 创 新 click_list extract_click_words_from_tip(tip_text) bg_img img_bytes_to_cv2(img_bytes) match_points [] for word in click_list: template_img cv2.imread(ftemplates/{word}.png) point, score match_single_word(bg_img, template_img, threshold0.82) if point is None: print(f文字 {word} 匹配失败最高相似度 {score:.3f}) continue match_points.append(point) print(f文字 {word} 匹配成功坐标 {point}相似度 {score:.3f})匹配失败的情况很常见尤其背景图和模板图风格不完全一致的时候。我的经验是阈值先设 0.8一次失败了打印出相似度再根据日志把阈值往下调或者优化模板。3.3 方案二ddddocr 目标检测识别如果你不想准备模板下面的方式更省事。ddddocr有一个 det 模式专门做验证码文字检测会返回每个文字的外框坐标。pip install ddddocrimport ddddocr # 初始化检测模型detTrue 表示启用目标检测 ocr ddddocr.DdddOcr(detTrue, ocrFalse) def detect_words_by_ddddocr(img_bytes): 使用 ddddocr 目标检测识别图片中的文字位置 返回格式[{box: [x1, y1, x2, y2], text: 中}, ...] results ocr.detection(img_bytes) word_positions [] # results 的格式是 [[x1, y1, x2, y2], ...] for box in results: x1, y1, x2, y2 box center_x (x1 x2) // 2 center_y (y1 y2) // 2 # 如果要文字内容可以再用 ocrTrue 识别每个裁剪区域 word_positions.append({ box: box, center: (center_x, center_y), }) return word_positionsddddocr 的目标检测返回的是图片中“疑似文字”的区域框它不负责判断这个字是不是提示里要点的那个字。要精确把“中”字和图片中的“中”区域对应起来更严谨的做法是把每个检测框裁剪出来再用识别模式ocrTrue识别框内文字然后与提示文字比对。这样准确率会好很多但对性能要求高一点。考虑到大部分固定业务系统验证码字体、风格都固定我的个人项目最终选了方案一因为模板一旦做好后续识别速度非常快而且不会受到外部模型服务的影响。3.4 坐标换算与偏移点击识别出图片内的坐标只是第一步真正点击的时候页面左上角坐标并不等于图片左上角坐标。因为验证码div在页面上有它自己的位置。需要把“图片内坐标”转成“元素内偏移坐标”。DrissionPage 的click(offset(x, y))方法接收的是相对元素左上角的偏移量。所以我们只需要把识别到的图片内坐标原样传给 offset 就行它自动帮我们处理了“元素在页面中的位置”这一层关系。这里要特别提一个像素缩放问题。某些高分屏比如 Windows 下的 125%、150% 缩放或者浏览器页面设置了缩放比例会导致截图图片的实际尺寸和页面布局尺寸不一致。比如截图是 300 像素宽但页面上元素看起来是 375 像素宽直接按截图像素传 offset 就会点歪。解决思路是计算缩放比例def get_scale_ratio(code_elem, img_width, img_height): 根据元素在页面的实际大小与截图像素大小计算缩放比例 code_elem: 验证码元素 img_width, img_height: 截图图片的宽高像素 # 元素在页面中的实际尺寸 element_size code_elem.size scale_x element_size[0] / img_width scale_y element_size[1] / img_height return scale_x, scale_y得到比例后点击坐标按比例换算def page_offset_from_img_center(center, scale_ratio): 图片内中心坐标 - 元素内偏移坐标 sx, sy scale_ratio return (center[0] * sx, center[1] * sy)然后执行点选。为了让行为更接近真人我每次点击之间加了随机间隔import random import time for word, point in zip(click_list, match_points): offset page_offset_from_img_center(point, scale_ratio) code_elem.click(offsetoffset) print(f已点击 {word}偏移坐标 {offset}) # 随机等待 0.3~0.8 秒 time.sleep(random.uniform(0.3, 0.8))这里code_elem.click(offsetoffset)是 DrissionPage 的一个特别方便的方法它会先保证元素可见然后移动到元素左上角再加偏移量进行点击。不过要注意offset 的坐标系是“物理像素”还是“CSS 像素”取决于页面是否有缩放。所以我上面先拿到了页面元素的实际尺寸和截图尺寸计算出 scale_ratio 再把坐标乘上去就是为了规避这些环境差异。3.5 提交表单、失败刷新与完整主流程点选完成后正常流程就是点登录/提交按钮然后判断是否通过验证码验证。判断方式一般是看是否出现了新的错误提示或者看地址栏有没有跳转。一个比较稳的完整流程是写一个循环最多重试 N 次。如果验证码失败了就点击验证码图片本身来刷新然后重新识别def auto_click_verify(max_retry5): 自动完成中文点选验证码并尝试提交 for attempt in range(1, max_retry 1): try: print(f第 {attempt} 次尝试) # 1. 定位验证码元素并截图 code_elem page.ele(css:.verify-code, timeout10) img_bytes code_elem.get_screenshot(as_bytespng) # 2. 识别文字坐标这里走模板匹配方案 bg_img img_bytes_to_cv2(img_bytes) tip_text page.ele(css:.verify-tip, timeout5).text click_list extract_click_words_from_tip(tip_text) match_points [] for word in click_list: template_img cv2.imread(ftemplates/{word}.png) point, score match_single_word(bg_img, template_img, threshold0.82) if point is None: raise ValueError(f{word} 匹配失败score{score:.3f}) match_points.append(point) # 3. 计算缩放比例 img_h, img_w bg_img.shape[:2] scaled_x, scaled_y get_scale_ratio(code_elem, img_w, img_h) scale_ratio (scaled_x, scaled_y) # 4. 依次点击 for word, point in zip(click_list, match_points): offset page_offset_from_img_center(point, scale_ratio) code_elem.click(offsetoffset) time.sleep(random.uniform(0.3, 0.8)) # 5. 点击登录/提交按钮 submit_btn page.ele(css:.login-btn, timeout5) submit_btn.click() # 6. 判断是否成功这里用页面标题或特定元素判断 page.wait(2) if 欢迎 in page.title: print(登录成功) return True # 失败则刷新验证码重新循环 print(验证码验证失败刷新重试) code_elem.click() page.wait(random.uniform(1.0, 1.5)) except Exception as e: print(f本次尝试异常: {e}) # 重新加载页面需要小心重试太频繁容易被限制 time.sleep(2) return False这个主流程基本是完整可直接跑的。几个变量的选择需要根据实际页面调整验证码元素的标识css 选择器、提示文字元素的选择器、提交按钮的选择器、成功判断条件。这些在第一次写脚本时建议用接管模式配合page.ele逐个打印排查确定准确后再集成进循环。4. 踩坑记录与常见问题排查实录4.1 验证码元素总是定位不到这个问题大概率出在 iframe 嵌套上。很多业务系统的登录页把验证码放在 iframe 里DrissionPage 默认操作主文档直接在主文档里找 iframe 内部的元素自然找不到。解决办法是先切换进入 iframe# 通过 iframe 元素进入 iframe page.ele(css:iframe[src*captcha]) page iframe # DrissionPage 中 frame 对象可以直接作为当前页面操作 # 注意这种写法只对 DrissionPage 有效 code_elem page.ele(css:.verify-code, timeout10)如果你发现切换后仍然找不到可能页面还有层叠 iframe那就一层一层切切一层定位一次实在不行就用page.get_frame()相关方法遍历 iframe。还有一种情况是元素在动态加载之前不存在。我的经验是先加上等待等验证码区域稳定后再定位。用page.ele(css:.verify-code, timeout10)其实已经带了最长等待时间但有些页面的验证码是通过 ajax 异步加载的等待时间不够就定位不到可以适当把 timeout 放大到 15 秒并配合page.wait.load_start()等页面加载完成事件。4.2 识别准确率忽高忽低模板匹配方案里准确率不稳定最常见的原因是背景干扰。有些验证码背景做了复杂的干扰线、噪点matchTemplate会把干扰线误判成文字。我的处理办法有两个。第一个是预处理。用 OpenCV 把验证码图片做灰度化、二值化减少背景颜色干扰然后再进行模板匹配。模板也要做同样的预处理两者在同一色彩空间下匹配准确率能提高不少def preprocess(img): # 转为灰度图 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化黑白分明 _, binary cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY) return binary第二个是阈值调节。把threshold从 0.85 调到 0.75 甚至 0.7看匹配结果。我在本地实验时发现有些字因为模板截图和实际字体有细纹差异最高相似度只有 0.81。强行用 0.85 会导致这个字永远匹配不上这时需要把阈值降低但也不能太低否则背景中的形近字会被误判。另外模板图和实际验证码字体风格必须一致。同一个“技”字如果验证码是宋体你准备的是黑体模板matchTemplate几乎不可能匹配成功。所以制作模板时一定要从同源验证码截图里抠图不要拿系统字体去生成模板。4.3 点击总是偏一点但坐标看起来没问题坐标对但点不准十有八九是缩放比例的问题。Windows 的“显示缩放”会让浏览器页面的 CSS 像素和截图物理像素不一致。截图返回的是一张像素图它的像素密度是设备像素而 DrissionPage 的点击 offset 通常基于 CSS 像素。如果不做换算所有点击点在 CSS 像素坐标系下都会偏离而且越往右下角偏得越厉害。解决方法是先拿一个已知位置的元素做标定。比如验证码区域里如果有某个固定装饰图标你先从截图里找到它的中心坐标再对比页面元素实际位置的差值算出横纵两个方向的缩放系数。这种方法比单纯看操作系统缩放设置要可靠得多。还有一点是浏览器缩放快捷键 Ctrl 加减号改变了页面比例也会导致同样的误差。脚本调度前如果手动缩放过浏览器建议先恢复 100% 缩放或者直接用ChromiumPage()每次新开一个干净的浏览器进程。4.4 常见问题速查表问题可能原因解决办法元素定位不到在 iframe 内先切换 iframe 再定位元素定位不到动态加载慢放大 timeout等待加载完成截图一片空白/黑图元素未滚动到视口内先 scroll 或 actions.wheel 滚动到可见区域匹配相似度低模板字体或风格不一致用同源验证码截图重新抠图模板匹配相似度低背景干扰强灰度化、二值化预处理点击位置偏移系统显示缩放比例影响用元素实际大小和截图大小计算 scale_ratio点击位置偏移浏览器手动缩放过恢复 100% 缩放或重开浏览器提交后一直失败验证码刷新逻辑没触发点击验证码图片区域刷新后重新识别运行一段时间卡死页面弹窗、加载遮罩等待遮罩消失或捕获弹窗并关闭4.5 我踩过比较深的一个坑验证码“点对了还是失败”有段时间脚本跑得很顺但突然某天开始点选坐标全部正确提交后却总是提示“验证码错误”。排查到最后发现是这个业务系统的验证码有一个隐藏的“语义顺序”校验。提示文字是“请依次点击科 技 创 新”系统要求四个字都点但它校验的是“点选完成后鼠标轨迹是否符合从左上到右下的视觉顺序”而不完全是后台记录的坐标顺序。这听起来很绕但实际影响是如果四个字分布在不同方位你按提示文字顺序点击系统却能检测出鼠标点击轨迹的连续性异常判定为机器操作。解决办法是把点击顺序稍微调整不完全按提示顺序而是先模拟一次“人工浏览”的移动轨迹先在验证码区域中心悬停片刻再依次点击。代码层面就是额外添加page.actions.move_to()动作在每次点击前先移动到目标点附近再重新点击。这样点击轨迹更接近真人验证通过率从 60% 提到了 90% 以上。for word, point in zip(click_list, match_points): offset page_offset_from_img_center(point, scale_ratio) # 先移动过去模拟人眼定位 code_elem.hover(offsetoffset) time.sleep(random.uniform(0.2, 0.5)) code_elem.click(offsetoffset) time.sleep(random.uniform(0.3, 0.8))类似的坑还有很多比如有些验证码会记录两次点击之间的最短最小间隔低于 0.2 秒直接判定机器操作有的验证码会检测鼠标移动的加速度曲线。这些都属于反自动化策略没有统一解只能根据实际页面的行为去适配。我的经验是不要一上来就写满“随机”,先手动操作一遍记下自己点每个字之间的时间差和鼠标停顿感再把这些参数写进脚本。5. 个人总结与后续扩展建议整个项目从开始到稳定运行我花了两个晚上大部分时间都耗在坐标换算和验证码刷新逻辑上。最后跑稳定之后登录一个原本要点十几秒验证码的系统脚本能做到 5 秒内完成识别、点选、登录成功率高的时候连续跑几十次不出问题。我个人最大的体会是DrissionPage 这套工具在“点击偏移”和“元素截图”这两个能力上确实比 Selenium 顺手太多。验证码类自动化最痛苦的从来不是识别模型而是“识别结果”和“页面点击”之间的坐标协调DrissionPage 把这一层做得很干净。如果你后续要写滑块验证码、图标点选、甚至复杂的拖拽交互这套流程的框架基本都能直接套用要换的只是识别部分的算法和模板。最后再分享一个小技巧不要在脚本里把验证码刷新逻辑写成“失败就重试”的无限循环最好加上最大重试次数和每次失败后的递增等待时间比如第一次等 1 秒第二次等 3 秒第三次等 5 秒否则遇到验证码服务暂时故障时脚本会以很高频率刷新容易被系统风控识别并临时封锁 IP。这种问题一旦发生排查起来比改代码还麻烦。改成递增等待之后脚本不仅更稳也更有“人味”长时间运行的安全性也高得多。
返回列表