验证码识别这块我前后折腾了不少时间。从最开始的数字字母 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(typepng, encodingbase64) 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_width500, quality85): 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, formatJPEG, qualityquality) return base64.b64encode(buf.getvalue()).decode()识别类型字段点选验证码在打码平台里通常有细分的类型编码比如文字点选按提示点击图中文字图标点选按提示点击对应图标数字点选按数字顺序点击选错类型识别结果基本就是废的。对接前务必确认你用的是哪种。坐标基准坐标是平台返回的但基准是什么有的平台返回的是“相对于原图左上角的绝对坐标”有的返回的是“相对于验证码容器左上角的坐标”。你需要搞清这两者是否一致。如果验证码图片在页面里做了偏移或缩放你直接用绝对坐标去点击点到的位置就会偏。最保险的做法是截图验证码元素的截图是干净的子图。拿到平台的坐标后再加上验证码元素在页面中的偏移量。如果页面有缩放坐标也需要等比换算。这段代码我贴一下它演示了一个标准的坐标换算逻辑def convert_to_page_coords(result_point, captcha_element_box, scale_factor1.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_factor1.0): for point in points: x, y convert_to_page_coords(point, captcha_element_box, scale_factor) # 先移动鼠标再点击模拟人类操作 await page.mouse.move(x, y, steps5) 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(clipLimit2.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_wait60, interval2): 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, radius3): 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 风控与对抗永远不是一锤子买卖验证码识别说到底是一场持续的动态对抗。站点会不断调整验证码样式、增加干扰元素、调整校验逻辑你对接的识别方案也必须跟着迭代。我的经验是每隔一段时间就要重新测试一遍识别率不能一个方案用到底。同时也要给自己留一条退路一旦主力平台挂了或效果变差备用方案要能顶上。我个人在实际操作中的体会是打码平台本质上是一个高度专业化的“外包大脑”它把识别这个难啃的骨头帮你啃掉了但它替代不了你对业务场景的理解。图片怎么截、坐标怎么换算、任务怎么调度这些才是每个开发者自己需要打磨的地方。把这几个环节做扎实了识别点选验证码就不再是什么难事了。最后再分享一个小技巧如果你在对接时发现识别率不高第一时间不是去换平台而是把传给平台的图片和平台返回的结果都保存下来看一眼。很多时候问题出在你传过去的图本身就是残缺的、变形的或者加了莫名其妙水印的。把这个前置问题解决掉识别率往往会有很大的提升。
阅读完成 · 觉得有帮助?