点选式验证码这东西,干过网页自动化的人应该都不陌生。它看起来只是让用户对着图片点几个字,背后却是一整套图形语义识别引擎,如果只靠浏览器自动化框架的“找到元素再点击”那一套,根本过不了。这个项目我前后调了两天,最终跑通的核心路线很清晰:用浏览器自动化框架负责定位、截图和模拟操作,用第三方打码平台负责识别图片内容,剩下最关键的工作其实就是坐标换算和异常重试。这篇文章把整条链路拆开讲,适合做数据采集、自动化测试、以及日常需要重复性登录验证的开发者参考,尤其是第一次接触点选类验证码自动化的人。整个项目的难点不是“会不会调库”,而是“验证码识别出来后,怎么把识别坐标准确映射回页面上的物理位置”。这里有个很容易踩的坑:网页里的验证码容器、屏幕截图、图片原始尺寸,三者坐标体系并不一致,直接拿识别结果去点击,十有八九会点偏。这也是为什么很多人代码写得没错,却一直跑不通——问题基本都出在坐标换算和浏览器缩放比例上。1. 项目整体设计与思路拆解1.1 为什么要做验证码自动化处理我最初接到这个需求,是因为某B站点选验证码挡住了批量采集的入口。这个页面本身没有太强的对抗策略,验证码的形态就是“展示一张包含字符或物品的大图,要求用户按提示点选指定文字或图案”。对普通用户来说,这种验证码只是多点几下鼠标,但对自动化程序来说,它构成了一个“语义理解”层面的拦截。如果只是做一个临时脚本,完全可以通过慢速人工输入绕过,但批量操作时这种方式完全不可行,效率太低。于是只能走自动化:程序模拟用户鼠标动作,让验证码服务先识别图片内容,再把识别结果转成鼠标点击坐标。抛开伦理边界不谈,单论技术难度,这件事的核心就是三块:页面交互定位、图片识别、坐标映射。需要明确一点,这篇文章涉及的技术,只应该用在你拥有合法权限的账号或测试环境里,比如自己做自动化测试、给自己账号做数据采集。未经授权去绕过验证码规则,属于违规行为,这一点必须心里有数。1.2 两条技术路线怎么选:自训练模型 vs 专业识别服务验证码识别一般有两条路线。第一是自己在本地训练一个图像识别模型,针对验证码的字体、颜色、背景干扰做定制优化。这条路听起来很酷,但实际做起来会发现数据标注成本非常高。点选验证码要求的不是简单识别“图里有什么”,而是输出“这些文字在图中的哪个坐标”,要自己做出高精度模型,至少需要几万张标注样本,还要不断跟随验证码样式更新做迭代,个人开发者很难扛得住。第二条路线就是接入专业打码平台。这类平台的价值在于,它底层有一套庞大的样本库和模型,对常见的文字点点选、滑块、方形字符都有现成方案,而且能以极低延迟返回结构化坐标结果。在大多数中小型自动化项目里,直接调用打码平台是性价比最高的选择。这里有一点经验上的建议:如果是很低频的脚本,比如一天跑几十次,平台按次计费的费用几乎可以忽略;如果一天几万次,那就值得考虑自训练模型来降低长期成本。方案初期成本准确率维护成本适合场景人工处理无高无极低频,偶尔一次采集自训练模型非常高中等高验证码固定、量大且长期商业打码平台低高低中小规模自动化项目我最终选择的是“浏览器自动化框架 + 打码平台”的组合。一方面是项目时间有限,自建模型不现实;另一方面,点选验证码的图片内容变化频繁,打码平台后端对干扰线的抗性强很多,短期跑起来更稳。1.3 整体工作流设计整个自动化流程拆成六步,我用文字描述一下链路:定位验证码容器 -> 获取容器在页面中的矩形区域 -> 对页面截图并裁剪出验证码图片 -> 将图片上传到识别平台 -> 解析识别结果中的多个坐标点 -> 按顺序在页面中模拟点击,最后提交验证。这里最容易被忽视的是“定位矩形区域”和“页面截图”之间的关系。验证码容器在网页里通常是一个<div>或<img>元素,它有浏览器视口坐标。而页面截图是整张图片的像素数据,像素坐标和视口坐标之间还隔着缩放比。如果直接用元素坐标去裁剪截图,截出来的图要么多出边框,要么位置偏移。处理好这一层,整个项目就成功了六成。另外,验证码的识别结果不是一次就能通过的,尤其是网络波动导致图片上传不完整时,识别坐标会乱。所以工作流里必须加入验证失败后的重试循环,避免程序中途卡死。这个设计思路和爬虫的请求重试机制是一模一样的,都属于“默认会失败,失败后再处理”的防御式编程。2. 点选验证码的机制与自动化核心细节2.1 这类验证码到底在验证什么点选验证码的核心机制,是让用户在图里找到特定对象并点击,从而证明操作者是“真实人类”。它并不要求用户输入复杂字符,而是利用人类的语义理解能力。理论上,机器也可以做到,但需要视觉模型同时完成目标检测和坐标回归,这对传统程序来说不是一件容易的事。网页端的实现方式通常是这样的:验证码背景是一张带干扰线的图片,图片上有若干汉字或物品,用户根据提示点击完所有目标之后,页面会把每个点击点的相对坐标组合,一起提交给服务器。服务器校验这些坐标是否落在正确区域,如果全部命中,则验证通过。因此,自动化方案的关键就在于,替程序生成这批“正确的坐标集合”。这里有一个细节值得注意:验证码服务器校验的不是像素级精确坐标,而是容差范围内的区域命中。所以哪怕识别结果偏移几个像素,只要仍在目标区域内,验证也能通过。这给了我一个缓冲空间。但相反的,如果偏移超过几十像素,那点击就会落到错误区域,直接被判定失败。2.2 坐标换算是整个方案的决定性环节我在调试中最痛苦的就是坐标这部分。浏览器自动化框架获取到的元素位置是“视口逻辑坐标”,而用截图工具拿到的截图是“物理像素坐标”,两者不一定相等。如果页面设置了缩放比例,比如 Windows 下常见的 125%、150%,逻辑坐标和物理像素坐标之间就差了一个缩放系数。举个具体例子,验证码容器在页面里位于 x=200, y=300,容器宽度 300px。在 150% 缩放的屏幕上,截图中对应区域的实际物理像素位置就变成了 x=300, y=450。如果我用逻辑坐标直接裁剪,图片会向左上偏移,识别结果自然就不准。更麻烦的是,识别平台返回的坐标是基于它收到的验证码图片来定位的,我要把这个坐标还原回页面视口坐标,才能让程序点击正确位置。换算公式可以这样理解:目标点击点相对验证码左上角的偏移,除以缩放比例,再加上验证码元素本身在页面上的逻辑坐标,就是程序应该点击的视口位置。这个公式看起来简单,但实际代码里因为有多种坐标体系交错,很容易在某一环节漏掉系数。提示:写代码前一定要先确认截图的像素尺寸和浏览器视口尺寸的比例。不要让程序假设二者相等,否则换一台高 DPI 的机器就全盘崩溃。2.3 提取验证码图片的正确方式很多初学者习惯直接用自动化框架自带的元素截图方法去抓验证码图片,但这个方法在部分浏览器里会返回一个空白图,或者图片尺寸不对。原因在于,元素截图方法在不同版本和不同浏览器上对“可见区域”的定义不一致。所以我在这个项目里改用了一种更稳的办法:截取整个浏览器窗口,然后用预处理方式裁剪出验证码区域。裁剪时要注意边界条件。如果验证码容器使用了border-radius、阴影等装饰属性,直接按照rect区域截图会把装饰也截进去。实际处理时可以适当向内收缩几个像素,比如取出rect后,对四个边缘各缩小 2~3 像素,减少干扰。这个细节在识别准确率上能带来肉眼可见的提升。图片格式方面,统一转成 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, format="PNG") 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(key=lambda 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, timeout=5): 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 合规与使用边界最后一定要强调:验证码自动化用于未经授权的批量注册、刷接口、爬取非公开数据,都是违规甚至违法的行为。这篇文章里的技术方案,只建议用于自动化测试、你自己账号的辅助操作,或者你在完全授权的情况下做数据采集工作。网站投入资源部署验证码,目的就是保护平台的数据和用户安全,任何技术都应当用来解决问题,而不是单纯绕过规则。我个人在实际操作中还有一个体会:不管方案跑得多顺,都要给自动化流程加上频率控制和日志记录。频率控制避免对目标服务器造成压力,日志记录方便事后回溯操作过程。这些看似小细节,在自动化项目里其实是比“识别准确率”更重要的工程能力。最后分享一个适合扩展的改进方向:本文方案里的打码平台是按次调用的,如果验证码出现频率很高,成本会累积。后续可以在我自己跑取的样本基础上,用现成的图像分类模型做二次校验,只有平台置信度低时才发给打码平台,这样既保证速度又降低费用。这个思路不复杂,但很实用,适合已经有基础自动化框架的人继续深入。
点选式验证码这东西,干过网页自动化的人应该都不陌生。它看起来只是让用户对着图片点几个字,背后却是一整套图形语义识别引擎,如果只靠浏览器自动化框架的“找到元素再点击”那一套,根本过不了。这个项目我前后调了两天,最终跑通的核心路线很清晰:用浏览器自动化框架负责定位、截图和模拟操作,用第三方打码平台负责识别图片内容,剩下最关键的工作其实就是坐标换算和异常重试。这篇文章把整条链路拆开讲,适合做数据采集、自动化测试、以及日常需要重复性登录验证的开发者参考,尤其是第一次接触点选类验证码自动化的人。整个项目的难点不是“会不会调库”,而是“验证码识别出来后,怎么把识别坐标准确映射回页面上的物理位置”。这里有个很容易踩的坑:网页里的验证码容器、屏幕截图、图片原始尺寸,三者坐标体系并不一致,直接拿识别结果去点击,十有八九会点偏。这也是为什么很多人代码写得没错,却一直跑不通——问题基本都出在坐标换算和浏览器缩放比例上。1. 项目整体设计与思路拆解1.1 为什么要做验证码自动化处理我最初接到这个需求,是因为某B站点选验证码挡住了批量采集的入口。这个页面本身没有太强的对抗策略,验证码的形态就是“展示一张包含字符或物品的大图,要求用户按提示点选指定文字或图案”。对普通用户来说,这种验证码只是多点几下鼠标,但对自动化程序来说,它构成了一个“语义理解”层面的拦截。如果只是做一个临时脚本,完全可以通过慢速人工输入绕过,但批量操作时这种方式完全不可行,效率太低。于是只能走自动化:程序模拟用户鼠标动作,让验证码服务先识别图片内容,再把识别结果转成鼠标点击坐标。抛开伦理边界不谈,单论技术难度,这件事的核心就是三块:页面交互定位、图片识别、坐标映射。需要明确一点,这篇文章涉及的技术,只应该用在你拥有合法权限的账号或测试环境里,比如自己做自动化测试、给自己账号做数据采集。未经授权去绕过验证码规则,属于违规行为,这一点必须心里有数。1.2 两条技术路线怎么选:自训练模型 vs 专业识别服务验证码识别一般有两条路线。第一是自己在本地训练一个图像识别模型,针对验证码的字体、颜色、背景干扰做定制优化。这条路听起来很酷,但实际做起来会发现数据标注成本非常高。点选验证码要求的不是简单识别“图里有什么”,而是输出“这些文字在图中的哪个坐标”,要自己做出高精度模型,至少需要几万张标注样本,还要不断跟随验证码样式更新做迭代,个人开发者很难扛得住。第二条路线就是接入专业打码平台。这类平台的价值在于,它底层有一套庞大的样本库和模型,对常见的文字点点选、滑块、方形字符都有现成方案,而且能以极低延迟返回结构化坐标结果。在大多数中小型自动化项目里,直接调用打码平台是性价比最高的选择。这里有一点经验上的建议:如果是很低频的脚本,比如一天跑几十次,平台按次计费的费用几乎可以忽略;如果一天几万次,那就值得考虑自训练模型来降低长期成本。方案初期成本准确率维护成本适合场景人工处理无高无极低频,偶尔一次采集自训练模型非常高中等高验证码固定、量大且长期商业打码平台低高低中小规模自动化项目我最终选择的是“浏览器自动化框架 + 打码平台”的组合。一方面是项目时间有限,自建模型不现实;另一方面,点选验证码的图片内容变化频繁,打码平台后端对干扰线的抗性强很多,短期跑起来更稳。1.3 整体工作流设计整个自动化流程拆成六步,我用文字描述一下链路:定位验证码容器 -> 获取容器在页面中的矩形区域 -> 对页面截图并裁剪出验证码图片 -> 将图片上传到识别平台 -> 解析识别结果中的多个坐标点 -> 按顺序在页面中模拟点击,最后提交验证。这里最容易被忽视的是“定位矩形区域”和“页面截图”之间的关系。验证码容器在网页里通常是一个<div>或<img>元素,它有浏览器视口坐标。而页面截图是整张图片的像素数据,像素坐标和视口坐标之间还隔着缩放比。如果直接用元素坐标去裁剪截图,截出来的图要么多出边框,要么位置偏移。处理好这一层,整个项目就成功了六成。另外,验证码的识别结果不是一次就能通过的,尤其是网络波动导致图片上传不完整时,识别坐标会乱。所以工作流里必须加入验证失败后的重试循环,避免程序中途卡死。这个设计思路和爬虫的请求重试机制是一模一样的,都属于“默认会失败,失败后再处理”的防御式编程。2. 点选验证码的机制与自动化核心细节2.1 这类验证码到底在验证什么点选验证码的核心机制,是让用户在图里找到特定对象并点击,从而证明操作者是“真实人类”。它并不要求用户输入复杂字符,而是利用人类的语义理解能力。理论上,机器也可以做到,但需要视觉模型同时完成目标检测和坐标回归,这对传统程序来说不是一件容易的事。网页端的实现方式通常是这样的:验证码背景是一张带干扰线的图片,图片上有若干汉字或物品,用户根据提示点击完所有目标之后,页面会把每个点击点的相对坐标组合,一起提交给服务器。服务器校验这些坐标是否落在正确区域,如果全部命中,则验证通过。因此,自动化方案的关键就在于,替程序生成这批“正确的坐标集合”。这里有一个细节值得注意:验证码服务器校验的不是像素级精确坐标,而是容差范围内的区域命中。所以哪怕识别结果偏移几个像素,只要仍在目标区域内,验证也能通过。这给了我一个缓冲空间。但相反的,如果偏移超过几十像素,那点击就会落到错误区域,直接被判定失败。2.2 坐标换算是整个方案的决定性环节我在调试中最痛苦的就是坐标这部分。浏览器自动化框架获取到的元素位置是“视口逻辑坐标”,而用截图工具拿到的截图是“物理像素坐标”,两者不一定相等。如果页面设置了缩放比例,比如 Windows 下常见的 125%、150%,逻辑坐标和物理像素坐标之间就差了一个缩放系数。举个具体例子,验证码容器在页面里位于 x=200, y=300,容器宽度 300px。在 150% 缩放的屏幕上,截图中对应区域的实际物理像素位置就变成了 x=300, y=450。如果我用逻辑坐标直接裁剪,图片会向左上偏移,识别结果自然就不准。更麻烦的是,识别平台返回的坐标是基于它收到的验证码图片来定位的,我要把这个坐标还原回页面视口坐标,才能让程序点击正确位置。换算公式可以这样理解:目标点击点相对验证码左上角的偏移,除以缩放比例,再加上验证码元素本身在页面上的逻辑坐标,就是程序应该点击的视口位置。这个公式看起来简单,但实际代码里因为有多种坐标体系交错,很容易在某一环节漏掉系数。提示:写代码前一定要先确认截图的像素尺寸和浏览器视口尺寸的比例。不要让程序假设二者相等,否则换一台高 DPI 的机器就全盘崩溃。2.3 提取验证码图片的正确方式很多初学者习惯直接用自动化框架自带的元素截图方法去抓验证码图片,但这个方法在部分浏览器里会返回一个空白图,或者图片尺寸不对。原因在于,元素截图方法在不同版本和不同浏览器上对“可见区域”的定义不一致。所以我在这个项目里改用了一种更稳的办法:截取整个浏览器窗口,然后用预处理方式裁剪出验证码区域。裁剪时要注意边界条件。如果验证码容器使用了border-radius、阴影等装饰属性,直接按照rect区域截图会把装饰也截进去。实际处理时可以适当向内收缩几个像素,比如取出rect后,对四个边缘各缩小 2~3 像素,减少干扰。这个细节在识别准确率上能带来肉眼可见的提升。图片格式方面,统一转成 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, format="PNG") 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(key=lambda 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, timeout=5): 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 合规与使用边界最后一定要强调:验证码自动化用于未经授权的批量注册、刷接口、爬取非公开数据,都是违规甚至违法的行为。这篇文章里的技术方案,只建议用于自动化测试、你自己账号的辅助操作,或者你在完全授权的情况下做数据采集工作。网站投入资源部署验证码,目的就是保护平台的数据和用户安全,任何技术都应当用来解决问题,而不是单纯绕过规则。我个人在实际操作中还有一个体会:不管方案跑得多顺,都要给自动化流程加上频率控制和日志记录。频率控制避免对目标服务器造成压力,日志记录方便事后回溯操作过程。这些看似小细节,在自动化项目里其实是比“识别准确率”更重要的工程能力。最后分享一个适合扩展的改进方向:本文方案里的打码平台是按次调用的,如果验证码出现频率很高,成本会累积。后续可以在我自己跑取的样本基础上,用现成的图像分类模型做二次校验,只有平台置信度低时才发给打码平台,这样既保证速度又降低费用。这个思路不复杂,但很实用,适合已经有基础自动化框架的人继续深入。