
点选式验证码这东西干过网页自动化的人应该都不陌生。它看起来只是让用户对着图片点几个字背后却是一整套图形语义识别引擎如果只靠浏览器自动化框架的“找到元素再点击”那一套根本过不了。这个项目我前后调了两天最终跑通的核心路线很清晰用浏览器自动化框架负责定位、截图和模拟操作用第三方打码平台负责识别图片内容剩下最关键的工作其实就是坐标换算和异常重试。这篇文章把整条链路拆开讲适合做数据采集、自动化测试、以及日常需要重复性登录验证的开发者参考尤其是第一次接触点选类验证码自动化的人。整个项目的难点不是“会不会调库”而是“验证码识别出来后怎么把识别坐标准确映射回页面上的物理位置”。这里有个很容易踩的坑网页里的验证码容器、屏幕截图、图片原始尺寸三者坐标体系并不一致直接拿识别结果去点击十有八九会点偏。这也是为什么很多人代码写得没错却一直跑不通——问题基本都出在坐标换算和浏览器缩放比例上。1. 项目整体设计与思路拆解1.1 为什么要做验证码自动化处理我最初接到这个需求是因为某B站点选验证码挡住了批量采集的入口。这个页面本身没有太强的对抗策略验证码的形态就是“展示一张包含字符或物品的大图要求用户按提示点选指定文字或图案”。对普通用户来说这种验证码只是多点几下鼠标但对自动化程序来说它构成了一个“语义理解”层面的拦截。如果只是做一个临时脚本完全可以通过慢速人工输入绕过但批量操作时这种方式完全不可行效率太低。于是只能走自动化程序模拟用户鼠标动作让验证码服务先识别图片内容再把识别结果转成鼠标点击坐标。抛开伦理边界不谈单论技术难度这件事的核心就是三块页面交互定位、图片识别、坐标映射。需要明确一点这篇文章涉及的技术只应该用在你拥有合法权限的账号或测试环境里比如自己做自动化测试、给自己账号做数据采集。未经授权去绕过验证码规则属于违规行为这一点必须心里有数。1.2 两条技术路线怎么选自训练模型 vs 专业识别服务验证码识别一般有两条路线。第一是自己在本地训练一个图像识别模型针对验证码的字体、颜色、背景干扰做定制优化。这条路听起来很酷但实际做起来会发现数据标注成本非常高。点选验证码要求的不是简单识别“图里有什么”而是输出“这些文字在图中的哪个坐标”要自己做出高精度模型至少需要几万张标注样本还要不断跟随验证码样式更新做迭代个人开发者很难扛得住。第二条路线就是接入专业打码平台。这类平台的价值在于它底层有一套庞大的样本库和模型对常见的文字点点选、滑块、方形字符都有现成方案而且能以极低延迟返回结构化坐标结果。在大多数中小型自动化项目里直接调用打码平台是性价比最高的选择。这里有一点经验上的建议如果是很低频的脚本比如一天跑几十次平台按次计费的费用几乎可以忽略如果一天几万次那就值得考虑自训练模型来降低长期成本。方案初期成本准确率维护成本适合场景人工处理无高无极低频偶尔一次采集自训练模型非常高中等高验证码固定、量大且长期商业打码平台低高低中小规模自动化项目我最终选择的是“浏览器自动化框架 打码平台”的组合。一方面是项目时间有限自建模型不现实另一方面点选验证码的图片内容变化频繁打码平台后端对干扰线的抗性强很多短期跑起来更稳。1.3 整体工作流设计整个自动化流程拆成六步我用文字描述一下链路定位验证码容器 - 获取容器在页面中的矩形区域 - 对页面截图并裁剪出验证码图片 - 将图片上传到识别平台 - 解析识别结果中的多个坐标点 - 按顺序在页面中模拟点击最后提交验证。这里最容易被忽视的是“定位矩形区域”和“页面截图”之间的关系。验证码容器在网页里通常是一个div或img元素它有浏览器视口坐标。而页面截图是整张图片的像素数据像素坐标和视口坐标之间还隔着缩放比。如果直接用元素坐标去裁剪截图截出来的图要么多出边框要么位置偏移。处理好这一层整个项目就成功了六成。另外验证码的识别结果不是一次就能通过的尤其是网络波动导致图片上传不完整时识别坐标会乱。所以工作流里必须加入验证失败后的重试循环避免程序中途卡死。这个设计思路和爬虫的请求重试机制是一模一样的都属于“默认会失败失败后再处理”的防御式编程。2. 点选验证码的机制与自动化核心细节2.1 这类验证码到底在验证什么点选验证码的核心机制是让用户在图里找到特定对象并点击从而证明操作者是“真实人类”。它并不要求用户输入复杂字符而是利用人类的语义理解能力。理论上机器也可以做到但需要视觉模型同时完成目标检测和坐标回归这对传统程序来说不是一件容易的事。网页端的实现方式通常是这样的验证码背景是一张带干扰线的图片图片上有若干汉字或物品用户根据提示点击完所有目标之后页面会把每个点击点的相对坐标组合一起提交给服务器。服务器校验这些坐标是否落在正确区域如果全部命中则验证通过。因此自动化方案的关键就在于替程序生成这批“正确的坐标集合”。这里有一个细节值得注意验证码服务器校验的不是像素级精确坐标而是容差范围内的区域命中。所以哪怕识别结果偏移几个像素只要仍在目标区域内验证也能通过。这给了我一个缓冲空间。但相反的如果偏移超过几十像素那点击就会落到错误区域直接被判定失败。2.2 坐标换算是整个方案的决定性环节我在调试中最痛苦的就是坐标这部分。浏览器自动化框架获取到的元素位置是“视口逻辑坐标”而用截图工具拿到的截图是“物理像素坐标”两者不一定相等。如果页面设置了缩放比例比如 Windows 下常见的 125%、150%逻辑坐标和物理像素坐标之间就差了一个缩放系数。举个具体例子验证码容器在页面里位于 x200, y300容器宽度 300px。在 150% 缩放的屏幕上截图中对应区域的实际物理像素位置就变成了 x300, y450。如果我用逻辑坐标直接裁剪图片会向左上偏移识别结果自然就不准。更麻烦的是识别平台返回的坐标是基于它收到的验证码图片来定位的我要把这个坐标还原回页面视口坐标才能让程序点击正确位置。换算公式可以这样理解目标点击点相对验证码左上角的偏移除以缩放比例再加上验证码元素本身在页面上的逻辑坐标就是程序应该点击的视口位置。这个公式看起来简单但实际代码里因为有多种坐标体系交错很容易在某一环节漏掉系数。提示写代码前一定要先确认截图的像素尺寸和浏览器视口尺寸的比例。不要让程序假设二者相等否则换一台高 DPI 的机器就全盘崩溃。2.3 提取验证码图片的正确方式很多初学者习惯直接用自动化框架自带的元素截图方法去抓验证码图片但这个方法在部分浏览器里会返回一个空白图或者图片尺寸不对。原因在于元素截图方法在不同版本和不同浏览器上对“可见区域”的定义不一致。所以我在这个项目里改用了一种更稳的办法截取整个浏览器窗口然后用预处理方式裁剪出验证码区域。裁剪时要注意边界条件。如果验证码容器使用了border-radius、阴影等装饰属性直接按照rect区域截图会把装饰也截进去。实际处理时可以适当向内收缩几个像素比如取出rect后对四个边缘各缩小 23 像素减少干扰。这个细节在识别准确率上能带来肉眼可见的提升。图片格式方面统一转成 PNG 或 JPEG 都可以关键在于图片字节大小。平台接口一般有文件大小上限如果验证码图片分辨率高、背景复杂直接用 PIL 保存可能会超限。解决办法是先将图片转为 RGB 模式再适当调整压缩质量。我一般把长边控制在 500 像素以内既保证识别精度又控制上传体积。2.4 点击执行与点击之后的反馈判断写最终点击操作时我建议用模拟鼠标动作而不是直接执行 JavaScript 触发点击。很多点选验证码页面会监听mousedown、mouseup事件链直接调用.click()会让页面判定为非法交互。浏览器自动化框架本身提供的ActionChains可以构造完整的鼠标移动序列让行为更接近真人。另外点击顺序必须严格按照平台返回的序号来执行。点选验证码的提示往往要求“先点 A再点 B”顺序错了哪怕坐标全对也会被判定失败。所以代码里要对识别结果按seq或order字段排序后再执行。验证是否成功的标准通常是验证码区域消失、出现成功状态或者页面能跳转到下一个环节。我会在点击完成后等待一段时间检查验证码容器是否还存在于页面中。如果还在说明可能失败了程序自动重新截图再次识别。注意第二次点击之前要清空之前的点击痕迹否则旧坐标会叠加到新验证码上干扰判断。3. 实操完整链路与核心代码实现3.1 环境准备与基础配置这个项目主要依赖四样东西浏览器自动化框架、浏览器驱动、图像处理库、以及一个打码平台账号。浏览器驱动要选择与浏览器版本严格匹配的版本否则启动时会直接报错。这里有一个实际操作经验如果电脑里有多个浏览器版本务必确认webdriver.Chrome()指向的浏览器路径和驱动版本是配套的否则后续定位元素时会出现莫名其妙的超时。Python 环境的依赖很少pip install selenium pillow requests打码平台的接入方式一般有两种一种是 HTTP 接口直传另一种是平台提供的 SDK 包。能用 HTTP 接口就用 HTTP 接口因为请求过程更透明出问题容易排查。我在代码里用了比较通用的 POST 上传方式具体域名和字段名已经做了抽象替换大家对接时换成自己申请的真实接口参数即可。3.2 定位验证码容器并截图先定义一个函数传入浏览器驱动对象和验证码元素返回裁剪后的验证码图片字节流、元素矩形区域、以及当前页面的缩放比例。import io from PIL import Image def capture_captcha(driver, captcha_ele): # 1. 获取元素在视口中的矩形区域逻辑坐标 rect captcha_ele.rect x rect[x] y rect[y] width rect[width] height rect[height] # 2. 获取当前页面的设备像素比 dpr driver.execute_script(return window.devicePixelRatio || 1;) # 3. 截取整个浏览器窗口 png driver.get_screenshot_as_png() im Image.open(io.BytesIO(png)) # 4. 根据 dpr 计算物理像素裁剪区域 box ( int(x * dpr), int(y * dpr), int((x width) * dpr), int((y height) * dpr), ) captcha im.crop(box) # 5. 转成字节流方便后续直接上传 buf io.BytesIO() captcha.save(buf, formatPNG) return buf.getvalue(), rect, dpr这里值得留意的是我使用window.devicePixelRatio作为缩放系数。在大多数浏览器中get_screenshot_as_png()返回的图片宽度就是视口宽度乘以设备像素比。如果你发现截图尺寸和视口尺寸完全一致说明没有缩放那dpr取 1 即可。3.3 上传识别平台并解析坐标打码平台的返回结构不同但核心信息都是一组坐标点每个点带序号。下面是一个模拟的请求函数平台接口地址和鉴权信息需要替换成你自己的。import requests def recognize_captcha(img_bytes, username, password, soft_id): resp requests.post( https://identify.example.com/api/upload, files{file: (captcha.png, img_bytes, image/png)}, data{ user: username, pass: password, soft_id: soft_id, }, ) data resp.json() # 假设返回格式为 # {code: 0, result: [{seq: 1, x: 55, y: 31}, {seq: 2, x: 98, y: 77}]} if data.get(code) ! 0: raise RuntimeError(识别失败: {}.format(data.get(error))) points data[result] # 按序号排序确保点击顺序正确 points.sort(keylambda p: p[seq]) return points平台返回的x、y是相对于裁剪图片左上角的像素坐标。也就是说如果验证码图片左上角是 0,0那么坐标 55,31 就是从该点向右 55 像素、向下 31 像素的位置。这个坐标体系纯粹基于图片和网页没有关系所以下一步要转回页面坐标。3.4 模拟鼠标点击目标位置现在到了最关键的一步把图片坐标转换为页面视口坐标再用鼠标动作点击。根据上一节的换算逻辑目标点击点在页面中的位置应该等于验证码容器左上角坐标加上偏移。但注意识别结果里的坐标是物理像素需要除以 dpr 才能变成视口逻辑偏移量。from selenium.webdriver.common.action_chains import ActionChains def click_points(driver, captcha_ele, rect, dpr, points): actions ActionChains(driver) # 先将鼠标移动到验证码左上角 actions.move_to_element_with_offset(captcha_ele, 0, 0) actions.pause(0.3) for pt in points: offset_x pt[x] / dpr offset_y pt[y] / dpr actions.move_by_offset(offset_x, offset_y) actions.pause(0.1) actions.click() actions.pause(0.1) # 点击完成后回到左上角方便计算下一次偏移 actions.move_by_offset(-offset_x, -offset_y) actions.pause(0.1) actions.perform()这里有一个动作链的细节move_by_offset是以上一个鼠标位置为基准进行移动的。所以每次点击后必须把自己移动到的位置作为下一次移动的起点。如果直接把多个坐标都写进动作链而不考虑当前位置第二个点的坐标就会累积偏移导致点击位置完全错误。我在每次点击完成之后把鼠标重新移回左上角这样每个坐标都是独立的思路更清晰。注意如果你的自动化框架版本里move_to_element_with_offset方法不是以左上角为基准而是以元素中心为基准那你就不能把偏移量直接等于pt[x] / dpr。需要用偏移量减去元素宽高的一半来修正。具体实现时可以先用一个已知坐标测试一次确定基准方式。3.5 结果验证与整体循环点击完成后页面通常会有明确的状态反馈。我要做的是等待验证码区域消失或者出现某种成功标志。这里我用了一个等待元素消失的方式from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_captcha_success(driver, captcha_selector, timeout5): try: WebDriverWait(driver, timeout).until( EC.invisibility_of_element_located(captcha_selector) ) return True except Exception: return False如果等待超时大概率是点击失败或者识别坐标不准。这时候不要立刻重新点击因为页面上可能残留了上一次的点击标记直接再点容易叠加错误。最佳做法是先刷新验证码然后重新执行完整流程。刷新验证码的方式通常是点击验证码区域本身或者调用页面上暴露的刷新接口。整体逻辑串联起来就是这样一个循环定位验证码 - 截图 - 识别 - 点击 - 判断成功失败则刷新验证码最多重试三次。三次仍然失败就停止并告警避免无限循环浪费资源。4. 实操中的常见问题与排查技巧4.1 点击坐标偏移几十像素这是最常见的问题。点击点整体向右或向下偏移几乎每次识别结果都是“差一点”。出现这种情况优先检查设备像素比dpr是否被正确读取。尤其是 Windows 远程桌面里缩放比例经常被系统单独控制浏览器里的devicePixelRatio可能和截图的物理像素比例不一致。还有一个隐蔽原因验证码图片加载完成后容器尺寸发生了变化。某些页面在图片加载过程中会有一个从默认高度到位真实高度的过程。如果我的程序在高度变化之前就读取了rect那么获取到的坐标就是错的。解决办法是在截图前增加一个等待确认验证码图片完全加载或者通过多次读取rect并判断其稳定性来避免。4.2 截图裁剪区域包含多余边框或缺少内容如果裁剪出的验证码图左右有白边或者内容被裁掉多半是验证码容器做了圆角或阴影。圆角会让边缘部分透明阴影会让容器视觉区域大于实际图片。处理方式有两种一种是缩小裁剪区域比如四边各收缩 5 像素另一种是不用容器rect直接去找内部真正的img元素用其图片本身的坐标来定位。如果页面里验证码是背景图没有子元素那就只能用rect。这种情况下我建议在裁剪后先做一次可视化检查把裁剪结果保存到本地文件夹人工看一眼图片是否居中且完整。允许程序在调试阶段自动保存错误样本能大幅缩短排查时间。4.3 验证码位于 iframe 内部如果目标页面的验证码是在 iframe 里展示的直接用主页面里的定位器会找不到元素。因为 iframe 是一个独立的文档上下文。必须先切换到 iframe再去定位验证码容器。这种情况下坐标换算还有一个额外的坑iframe 在页面中的位置会叠加到主容器坐标上但自动化框架切换上下文后rect默认是相对于 iframe 的窗口坐标而不是主窗口坐标所以换算时要把 iframe 的外层矩形同步加入计算。我一般在进入 iframe 后先执行 JavaScript 获取 iframe 元素的位置再用两者的偏移关系修正验证码坐标。切换到 iframe 后截整页图也不太现实最好先退出 iframe 截主页面再按主页面坐标裁剪或者直接用 iframe 的rect叠加主页面内的 iframe 位置。4.4 打码平台偶尔返回乱码坐标平台识别不可能达到 100% 准确偶尔会把相近字符认错导致返回的坐标不在任何有效区域。乱码坐标的特征是没有规律地飘在图片边缘或者超出图片尺寸。代码里可以加一个基本校验坐标值必须落在图片宽度和高度范围内越界的直接丢弃然后重新识别。另外上传图片的质量对识别率影响极大。不要直接上传原始大图也不要压缩得太小。实践中我发现长边在 400-500 像素附近时平台识别的综合准确率最高。图片中如果有明显的噪点可以先做一次灰度处理再上传某些平台对灰度图反而更友好。这个不是绝对的需要根据平台情况做测试。4.5 合规与使用边界最后一定要强调验证码自动化用于未经授权的批量注册、刷接口、爬取非公开数据都是违规甚至违法的行为。这篇文章里的技术方案只建议用于自动化测试、你自己账号的辅助操作或者你在完全授权的情况下做数据采集工作。网站投入资源部署验证码目的就是保护平台的数据和用户安全任何技术都应当用来解决问题而不是单纯绕过规则。我个人在实际操作中还有一个体会不管方案跑得多顺都要给自动化流程加上频率控制和日志记录。频率控制避免对目标服务器造成压力日志记录方便事后回溯操作过程。这些看似小细节在自动化项目里其实是比“识别准确率”更重要的工程能力。最后分享一个适合扩展的改进方向本文方案里的打码平台是按次调用的如果验证码出现频率很高成本会累积。后续可以在我自己跑取的样本基础上用现成的图像分类模型做二次校验只有平台置信度低时才发给打码平台这样既保证速度又降低费用。这个思路不复杂但很实用适合已经有基础自动化框架的人继续深入。