验证码识别这块,我前后折腾了不少时间。从最开始的数字字母 OCR,到后面滑块、拼图,再到现在的点选验证码,一路走过来,最让人头疼的还真就是点选。这种验证码不像老式的歪扭数字,它不光让你看图,还让你理解语义、按顺序点击,单纯靠 OCR 那一套很难搞定。后来我换了个思路,不再自己硬抠识别算法,而是接了打码平台,整套流程才算是真正跑通了。
这篇文章,我就把自己用打码平台识别点选验证码的过程、踩过的坑、以及背后的一些原理,完整地拆开讲一遍。适合正在搞 Web 自动化、爬虫采集、以及做自动化测试的朋友参考。想说明的是,这类技术目前被广泛应用在合规的数据采集中、内部系统的自动化回归、以及账号安全风控研究中,只要你是用在正当的场景里,这套方案能帮你省掉非常多的时间。
先说结论:利用打码平台做点选识别,重活不在“写代码调用 API”,而是在于前期的验证码类型判断、图片预处理、坐标换算,以及后期的调度策略。这几点做好了,平台的识别率能发挥到九成以上;做不好,哪怕平台再强,你的任务照样失败。
1. 点选验证码:先搞清楚你到底在跟什么东西较劲
打码平台只是个替你干活的“人”,你要知道活该怎么派、派得对不对,这就需要先弄明白点选验证码本身是个啥。
1.1 点选验证码的核心逻辑
所谓点选验证码,服务端会给你一张背景图,图上有若干个元素,然后要求你按照提示,依次点击图中对应的位置。常见的提示有:
- “请按顺序点击:山、云、太阳”
- “请点击下图中所有的红绿灯”
- “请依次点击 1、2、3、4 对应的小图”
它考验的其实不是“你认不认识这个字”,而是“你在杂乱背景里能不能准确找到目标元素”。对机器来说,这个难度不在于字符识别,而在于:
- 语义理解:你得知道“山”是个什么东西,跟“山峰”是不是同一个概念。
- 目标定位:即使知道了目标,还得在图里的具体像素坐标上把它找出来。
- 点击顺序:很多点选验证码,点的先后顺序也是校验的一部分。
这就比单纯的数字字母验证码高了一个维度。普通 OCR 只能解决“这是什么字”,点选验证码还要求解决“它在哪”和“按什么顺序点”。
1.2 交互链路与技术栈
一个完整的点选验证码识别流程是这样走的:
用户打开页面 → 页面加载出验证码图片 → 用户点击“刷新”或直接出现提示词 → 用户识别图中目标 → 按顺序点击 → 前端把点击坐标点序列提交给后端 → 后端校验坐标是否在目标元素范围内、顺序是否正确。
自动化场景下,我们替换掉的是“用户”这一环。由脚本完成:截图/获取图片 → 请求打码平台识别 → 拿回坐标点 → 执行点击操作。校验逻辑、加密参数、风控检测这些,依然在目标站点的服务端,我们只能尽量模拟出人类的行为模式。
1.3 为什么说只靠开源模型不现实
也有人问,为什么不直接用本地 OCR 或目标检测模型去识别点选验证码。这个我试过,原因很简单,不现实:
- 目标种类太泛。今天验证码让你点“汽车”,明天可能让你点“和珅的画像”,目标类别是开放的,静态分类模型根本追不上。
- 图片带背景干扰。点选验证码的背景图往往极其复杂,有各种景物、噪点、扭曲,直接做目标检测,误检率高得离谱。
- 每个站的验证码风格都不一样。同一个模型在这个站点识别率 80%,到另一个站点可能直接归零。
- 训练数据难搞。标注一批点选验证码的时间,够你把打码平台接口接完十遍了。
所以,综合下来,借助打码平台的“人力+AI”混合识别,是成本最低、见效最快的路径。
2. 打码平台的识别原理与选型思路
打码平台听起来挺神秘的,其实本质就是一个“识别服务”。你给它一张图,它告诉你图里的目标都长什么样、坐标在哪。市面上的平台主要分两类:纯 AI 识别和 AI+人工兜底。
2.1 平台内部是怎么干活的
大部分平台现在都不是纯人工看图了,成本扛不住,速度也慢。实际流程是:
- 你的图片传到平台服务器。
- 平台先用自研的 AI 模型做一轮预测,模型结构通常类似目标检测网络,专门在验证码图片上做了微调训练。
- AI 输出一组候选结果,比如每个字的文本框坐标、置信度。
- 如果 AI 的置信度足够高,直接返回结果。
- 如果置信度低,或触发了人工审核规则,图片进入人工队列,由真人标注员看图选坐标。
- 识别结果通过接口返回给你。
这个链路的好处是,AI 能兜住大部分简单样本,人工只处理疑难杂症,平台成本和用户体验都兼顾了。对调用方来说,平台保证了整体识别率,不用你自己去调模型。
2.2 对接维度:找平台要看哪几个指标
选平台不是看谁便宜就选谁,要根据你的使用场景做权衡。
| 指标 | 说明 | 我的经验 |
|---|---|---|
| 识别类型支持 | 是否支持你遇到的那类点选验证码 | 优先选类型细分多的,有的平台还分“文字点选”和“图标点选” |
| 返回格式 | 坐标格式是相对坐标还是绝对坐标,是否按点击顺序排好 | 确认返回的是“图片原始坐标”还是“点选区域中心点” |
| 响应速度 | 从提交到返回结果的时间 | 纯 AI 快,约 1-3 秒;人工兜底可能 5-20 秒,需要轮询等待 |
| 准确率 | 平台宣传的识别率 | 实际要拿你自己的图去测,别只看宣传 |
| 价格 | 按次计费,充值使用 | 通常点选比纯数字贵,因为识别成本更高 |
| 并发能力 | 同时提交大量任务时是否稳定 | 大规模采集时尤其要看这个,有的平台并发一高就超时 |
我自己用的平台,点选识别一次在几分钱到一毛多之间,具体看类型和数量。这个价格对于自己搞自动化测试来说,基本可以忽略不计。但对于大量跑批的任务,成本也需要纳入考量,后面我会专门算一笔账。
2.3 为什么不要把“识别率”当成唯一标准
很多朋友上来就问“你家识别率多少”,其实这个问法不太科学。识别率跟图片质量、类型、还有场景强相关。同样一个平台,识别清晰的大图可能 99%,识别那种带干扰线、低分辨率的小图可能只有 70%。
更好的做法是:拿你自己的真实图片,先小批量测试。一般来说,测试二三十张,就能估算出真实水平。测试的时候要注意:
- 用你实际业务里的图片,不要用平台官网给的示例图。
- 覆盖不同时间段抓到的图,验证码样式有时会变。
- 看失败的类型:是坐标偏移、还是识别出无关目标、还是直接超时。
3. 实战对接:从拿到图片到返回坐标的完整链路
这一部分是文章的重点,我会把整个对接过程拆开,一步一步告诉你应该怎么做,代码直接给你参考。
3.1 第一步:抓取/保存验证码图片
点选验证码的图片获取方式各站都不一样,常见的:
- 页面直接有
<img>标签,src指向一个图片地址,直接下载即可。 - 图片通过 JavaScript 动态生成,Canvas 绘制后转 base64,需要从页面里提取。
- 部分站点的点选验证码是分块的,比如九宫格、三列图,需要切片拼图后提交给平台。
- 还有一种常见形式,就是用户点击后出现一个 JS 弹窗,验证码图片就在弹窗区域,截图可以用 Playwright 或 Selenium 对这个区域做 element screenshot。
我自己是用 Playwright 做的,优势是它自带element.screenshot()方法,可以直接截取验证码元素,这样能完美避开因页面滚动造成的图片偏移问题。如果是 Selenium,截全屏再裁剪也行,但就多了一步坐标计算。
import asyncio from playwright.async_api import async_playwright async def capture_captcha(page, selector="#captcha_img"): element = await page.query_selector(selector) if not element: raise RuntimeError("验证码元素未找到") # 截图返回 base64 编码图片 screenshot = await element.screenshot(type="png", encoding="base64") return screenshot这里有个细节,Playwright 的 element screenshot 会忽略页面的滚动位置,直接截取元素当前所在的位置区域,这点非常有用。
3.2 第二步:调用打码平台 API 的过程
调用打码平台接口,标准流程一般是两步:提交任务 + 轮询获取结果。有些平台也支持回调通知,但轮询用得最广,实现简单且稳定。
接口形式一般是 POST JSON,长这样:
POST /api/captcha/recognize Content-Type: application/json { "username": "your_account", "password": "your_password", "image": "<base64编码的图片数据>", "captcha_type": "click_word", "callback_url": "" // 留空表示轮询 }注意,image传的是 base64 编码字符串,不是你本地文件的路径。提交后会返回一个task_id,然后你需要拿着这个 id 去轮询结果:
POST /api/captcha/result { "username": "your_account", "password": "your_password", "task_id": "xxxxxxxx" }返回结果中核心的两个字段就是坐标点列表和状态码。坐标点列表一般是这样的 JSON 结构:
{ "status": 1, "result": [ {"x": 120, "y": 83}, {"x": 280, "y": 146}, {"x": 35, "y": 210} ] }坐标顺序就是平台识别出来后建议的点击顺序,你直接按这个顺序点击就行了。
3.3 第三步:参数细节——别小看这几个值
这里要特别说明几个影响成败的参数:
图片格式与压缩
打码平台对图片大小、格式有限制。有的平台要求图片不能超过 2MB,超过就要先压缩。怎么压不能瞎压,太狠了影响识别率,不压又传不上去。我通常的做法是转成 JPEG,质量设为 85,尺寸保持宽高比不变,宽度超过 500px 时等比缩放。这样能在不严重影响识别效果的前提下缩小体积。
from PIL import Image import io, base64 def compress_image(img_bytes, max_width=500, quality=85): img = Image.open(io.BytesIO(img_bytes)).convert("RGB") if img.width > max_width: ratio = max_width / img.width img = img.resize((max_width, int(img.height * ratio)), Image.LANCZOS) buf = io.BytesIO() img.save(buf, format="JPEG", quality=quality) return base64.b64encode(buf.getvalue()).decode()识别类型字段
点选验证码在打码平台里通常有细分的类型编码,比如:
- 文字点选:按提示点击图中文字
- 图标点选:按提示点击对应图标
- 数字点选:按数字顺序点击
选错类型,识别结果基本就是废的。对接前务必确认你用的是哪种。
坐标基准
坐标是平台返回的,但基准是什么?有的平台返回的是“相对于原图左上角的绝对坐标”,有的返回的是“相对于验证码容器左上角的坐标”。你需要搞清这两者是否一致。如果验证码图片在页面里做了偏移或缩放,你直接用绝对坐标去点击,点到的位置就会偏。最保险的做法是:
- 截图验证码元素的截图是干净的子图。
- 拿到平台的坐标后,再加上验证码元素在页面中的偏移量。
- 如果页面有缩放,坐标也需要等比换算。
这段代码我贴一下,它演示了一个标准的坐标换算逻辑:
def convert_to_page_coords(result_point, captcha_element_box, scale_factor=1.0): # captcha_element_box 是验证码元素在页面上的绝对位置 # result_point 是平台返回的相对于图片左上角的坐标 page_x = captcha_element_box["x"] + result_point["x"] * scale_factor page_y = captcha_element_box["y"] + result_point["y"] * scale_factor return page_x, page_y这里最容易被忽略的坑是scale_factor。如果页面设置了缩放、设备像素比不是 1,或者浏览器窗口大小变了,截图尺寸和实际页面坐标尺寸对不上,坐标就会全部偏移。
3.4 第四步:模拟点击的完整示例
拿到坐标后,就要模拟鼠标点击。我用 Playwright 的鼠标操作来实现:
async def click_points(page, points, captcha_element_box, scale_factor=1.0): for point in points: x, y = convert_to_page_coords(point, captcha_element_box, scale_factor) # 先移动鼠标,再点击,模拟人类操作 await page.mouse.move(x, y, steps=5) await page.mouse.click(x, y) # 每次点击之间随机停顿 200-500ms await asyncio.sleep(random.uniform(0.2, 0.5))注意细节:page.mouse.move的steps参数会生成平滑移动轨迹,这比直接瞬移更接近真人操作。时间间隔也要带随机性,不要太规律。
4. 实操过程中的常见问题与排查技巧
这部分是我实际跑流程时最常遇到的问题,整理成一张速查表,再逐一展开聊。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 识别返回空结果 | 图片模糊或类型选错 | 提高截图清晰度,确认类型字段 |
| 坐标整体偏移 | 页面滚动/缩放或基准不一致 | 用元素坐标偏移量换算,检查缩放因子 |
| 部分点位准确、部分不准 | 图片里目标元素过小或密集 | 适当放大局部区域再提交识别 |
| 频繁超时 | 平台并发限制或网络慢 | 减小并发量,或换平台/增加重试 |
| 识别结果顺序不对 | 平台未按顺序返回 | 提交前在参数里明确要求顺序输出 |
| 点击后提示“验证失败” | 坐标偏了几像素 | 取目标中心点而非左上角,适当做坐标微调 |
4.1 坐标偏移问题:最常见的翻车现场
这是我自己第一次完全跑通流程却反复失败的地方。平台明明返回了坐标,点击位置看上去也对,但站点就是提示验证失败。后来我发现,问题出在“图片缩放”上。
很多站点验证码图片的原始尺寸是 480x300,但页面实际显示只有 320x200。我保存下来传给打码平台的是原始图,平台返回原始图的坐标,可我点击时却用了页面显示比例去点击,这可就完全错位了。
解决办法很简单,截图直接用元素截图,让截下来的图和页面显示的效果一致。如果截图尺寸和原始图片尺寸不同,一定要先算好缩放比:
def reverse_scale(point, display_width, original_width): # 如果平台基于原始图返回坐标,页面显示已缩小 factor = display_width / original_width return point["x"] * factor, point["y"] * factor4.2 识别率上不去的元凶:图片质量
同一个验证码,截图截得清晰还是模糊,识别率差距非常大。具体来说:
- 尽量避免 JPEG 过度压缩,特别是文字边缘容易糊。
- 有些站点验证码背景特别花哨,AI 识别可能会把背景中的相似元素也识别进去。这时候可以尝试把图片转成灰度图或做简单的对比度增强,再提交平台,往往会有意想不到的效果。
- 如果验证码图片被 CSS 缩放过(比如图片原本 600px,渲染成 300px),建议把截图放大到原尺寸再提交。很多平台的模型是在原始尺寸图片上训练的,直接提交缩小图会丢特征。
我自己写过一个简单的图片增强函数,效果还不错:
import cv2 import numpy as np def enhance_image(img): # 转灰度 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # CLAHE 对比度增强 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) enhanced = clahe.apply(gray) # 适度锐化 kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]) sharpened = cv2.filter2D(enhanced, -1, kernel) return sharpened但也要注意,增强不是万能的,过度处理可能会帮倒忙。建议多做几组对比,以实际识别结果为准。
4.3 超时与重试机制:稳定性设计
点选识别本来就比普通数字验证码慢,遇到人工兜底时可能要等上十来秒。轮询时如果不注意超时设置,很容易把请求搞挂。
我的做法是:轮询总超时设为 60 秒,每 2 秒轮询一次。超过 60 秒还没出结果,就放弃这次任务并重新提交。这时要注意,不要无脑无限重试,否则并发任务一多,平台账号容易被限制。重试次数一般控制在 3 次以内。
import time def poll_result(api_client, task_id, max_wait=60, interval=2): start = time.time() while time.time() - start < max_wait: result = api_client.get_result(task_id) if result.status == 1: # 成功 return result elif result.status == 2: # 任务失败 raise RuntimeError("识别任务失败") time.sleep(interval) raise TimeoutError("识别超时")4.4 并发调度的经验:别一股脑全塞进去
如果你要同时跑几十上百个验证码,要注意平台的并发限制。有些平台对同一账号的并发请求数有限制,超了就直接拒绝。
我自己的策略是做一个简单的任务队列,把并发控制在 5 个以内。遇到高优先级任务,单独走一条队列。这样既保证速度,又不会触顶平台限制。
另外,多准备一个平台账号做备用。识别也有高峰期,一家平台可能排队严重,自动切换到备用平台能大幅提升整体成功率。我在切换时有一套简单的打分机制:近 10 次请求的成功率加权打分,低到阈值就切换另一个平台。
5. 成本优化与策略调优:让整套方案更稳更省
识别稳定性跑通之后,下一步就是考虑优化了。毕竟验证码识别是高频操作,成本和效率直接的平衡点很重要。
5.1 成本模型估算
我给自己算过一笔账,假设每天跑 5000 次点选识别,单次价格按 0.05 元算,一天就是 250 元,一个月能到 7500 元。这个数字不是小数目,所以优化空间很大。
实际优化可以从三个方向入手:
- 降低调用量。不是每次都需要打码识别。如果目标站点有验证码出现频率的控制,你可以在出错后先刷新重试,避开高峰期再识别。
- 提高识别效率。用更快的轮询策略,减少等待时间,提高单位时间吞吐量。
- 混合识别。本地先用简单规则过滤掉一部分明显不需要打码的验证码类型,比如纯数字验证码可以直接本地 OCR,不浪费点选的费用。
5.2 本地预过滤:别让打码平台干白工
有些点选验证码实际上是“伪点选”——比如图片里只有三个目标,而且页面提示词明确,不涉及复杂语义。这种可以先用本地图像处理做一个预定位,能直接识别的就不提交平台了。
不过,预过滤的准确率如果不够高,反而会降低整体成功率。我的建议是,预过滤只用于能 100% 确定的场景,其余情况全部交给平台。
5.3 命中率优化:让 AI 和人工都猜得更准
打码平台返回坐标后,直接点击的命中率会受到很多因素影响:
- 平台返回的是目标框的左上角还是中心点?这个很关键,建议取目标框中心。
- 目标元素本身很小,坐标偏差 2 像素就点到旁边了。可以请求平台返回目标框的宽高,自己算中心点再偏移一两个像素。
- 不同站点的点击允许误差不一样,有的容错很松,有的很严。对严格的站点,可以在平台返回坐标的基础上,做一个 3x3 邻域的小范围探针点击尝试。
探针点击有点像“盲点”,办法是在周围点几次看有没有触发反馈。不过这属于高阶操作,能用,但别依赖,大部分情况下中心点就行。
def probe_click(page, cx, cy, radius=3): attempts = [] for dx in [-radius, 0, radius]: for dy in [-radius, 0, radius]: attempts.append((cx + dx, cy + dy)) # 先记录当前状态,再依次尝试 return attempts5.4 业务侧的策略优化:少接触风控,比识别更重要
识别率再高,也只是成功概率的一部分。验证码出现在页面里,本身就是一个风控信号。如果你的访问行为像机器,那验证码出现的频率就会飙升,导致你不停地要打码,成本自然就上去了。
从这个角度看,优化脚本行为才是上游策略。控制访问频率、随机化操作间隔、保持合理的请求头,这些都比砸钱调用打码平台更关键。我在跑批量任务时发现,把单线程改成模拟多用户随机调度的方式之后,验证码出现频率降了差不多一半。
6. 一些零零碎碎但很重要的心得
最后说几个散装经验,不一定成体系,但都是我实打实踩坑后总结出来的。
6.1 结果缓存
同一个验证码图片不要提交第二次。如果第一次提交超时了,结果没返回,但平台实际已经识别完了,这时你再提交就浪费一次钱。我的做法是,对图片内容做一次哈希,缓存最近识别过的图片哈希和结果,命中直接复用。
6.2 平台账号安全
绝对不要在大批量任务里共用一个小号,很容易被平台限制。也别在代码里硬编码账号密码,用环境变量或配置文件管理。有些平台还支持 token 鉴权,比账号密码更安全。我是把账号信息统一放到.env文件里,用配置模块加载,代码里不出现任何明文凭据。
6.3 日志就是你的排查武器
验证码识别不稳定,每次失败都要能定位到环节。我习惯把每一步都写进结构化日志,包括:
- 图片哈希
- 提交时间、识别耗时
- 平台返回的原始结果
- 最终点击坐标
- 页面/元素位置信息
这些日志在排查问题时非常有用。有一次识别率突然下降,我翻日志发现是某次站点改版后验证码图片尺寸变了,平台返回坐标还是老基准,我的脚本却还在用旧偏移量,一下就定位到了。
6.4 风控与对抗:永远不是一锤子买卖
验证码识别说到底是一场持续的动态对抗。站点会不断调整验证码样式、增加干扰元素、调整校验逻辑,你对接的识别方案也必须跟着迭代。我的经验是,每隔一段时间就要重新测试一遍识别率,不能一个方案用到底。同时,也要给自己留一条退路:一旦主力平台挂了或效果变差,备用方案要能顶上。
我个人在实际操作中的体会是,打码平台本质上是一个高度专业化的“外包大脑”,它把识别这个难啃的骨头帮你啃掉了,但它替代不了你对业务场景的理解。图片怎么截、坐标怎么换算、任务怎么调度,这些才是每个开发者自己需要打磨的地方。把这几个环节做扎实了,识别点选验证码就不再是什么难事了。
最后再分享一个小技巧:如果你在对接时发现识别率不高,第一时间不是去换平台,而是把传给平台的图片和平台返回的结果,都保存下来看一眼。很多时候,问题出在你传过去的图本身就是残缺的、变形的或者加了莫名其妙水印的。把这个前置问题解决掉,识别率往往会有很大的提升。