CaptchaAI 识别 reCAPTCHA v2 图片网格后,只返回一串单元格编号,比如 [1, 3, 6, 9],点击动作要自己换算成像素坐标。grid_size 判断错、坐标偏移算错,是最常见的两个坑。本文按排查顺序讲清楚:怎么截图、怎么转坐标、怎么点对图块,以及图块被替换后怎么处理,对接跨境站点的同类挑战时这套逻辑基本通用。
国内站点很少见这种网格图片挑战——常见的是 GeeTest(极验)或网易易盾滑块,reCAPTCHA v2 图片网格多出现在跨境电商、海外表单这类面向国际用户的页面上,脚本由 Google 托管,这也是下文单独提示网络排查的原因。
九宫格的编号规则
reCAPTCHA v2 图片挑战常见 3×3(俗称九宫格)和 4×4 两种网格,编号规则都一样:
- 起点:左上角是第 1 格
- 顺序:从左到右、从上到下依次递增
- 终点:右下角是最后一格(3×3 是第 9 格,4×4 是第 16 格)
3×3 Grid: 4×4 Grid:
1 2 3 1 2 3 4
4 5 6 5 6 7 8
7 8 9 9 10 11 12
13 14 15 16
第三步做坐标换算时会直接用到这套行列公式。
第一步:截取网格图并读取提示语
开始前先确认三件事:
- CaptchaAI API Key 已拿到,去 captchaai.com 注册即可
- 本地 Python 或 Node.js 环境能跑通 Selenium/Puppeteer
- 目标页面确实弹出 reCAPTCHA v2 图片网格,而非 v3 隐形验证
Python(Selenium)
import base64
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example.com/form")
# Wait for reCAPTCHA iframe
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "iframe[src*='recaptcha']"))
)
# Switch to challenge iframe
iframes = driver.find_elements(By.CSS_SELECTOR, "iframe[src*='recaptcha']")
challenge_iframe = iframes[-1] # Challenge iframe is typically the last one
driver.switch_to.frame(challenge_iframe)
# Get the grid image
grid_img = driver.find_element(By.CSS_SELECTOR, "img.rc-image-tile-33, img.rc-image-tile-44")
img_src = grid_img.get_attribute("src")
# Get instruction text
instruction = driver.find_element(
By.CSS_SELECTOR, ".rc-imageselect-desc-wrapper"
).text
print(f"Instruction: {instruction}")
# Screenshot the grid as base64
img_b64 = grid_img.screenshot_as_base64
# Determine grid size
classes = grid_img.get_attribute("class")
grid_size = "4x4" if "44" in classes else "3x3"
print(f"Grid size: {grid_size}")
driver.switch_to.default_content()
JavaScript(Puppeteer)
const puppeteer = require('puppeteer');
const fs = require('fs');
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com/form');
// Find the challenge iframe
const frames = page.frames();
const challengeFrame = frames.find(f => f.url().includes('recaptcha'));
// Get instruction
const instruction = await challengeFrame.$eval(
'.rc-imageselect-desc-wrapper',
el => el.textContent.trim()
);
// Screenshot the grid image
const gridImg = await challengeFrame.$('img.rc-image-tile-33, img.rc-image-tile-44');
const imgBuffer = await gridImg.screenshot();
const imgBase64 = imgBuffer.toString('base64');
// Determine grid size
const className = await challengeFrame.$eval(
'img.rc-image-tile-33, img.rc-image-tile-44',
el => el.className
);
const gridSize = className.includes('44') ? '4x4' : '3x3';
console.log(`Grid: ${gridSize}, Instruction: ${instruction}`);
第二步:把图片提交给 CaptchaAI 识别
这一步分两段:先把截图和提示语一起 POST 到 in.php 拿任务 ID,再轮询 res.php 等结果。两个接口都要带上:
key:你的 API Keygrid_size:3x3 或 4x4,跟第一步读到的网格大小对应instructions:从提示语里提取出的关键词,比如"红绿灯"对应 traffic lights
import requests
import time
import json
API_KEY = "YOUR_API_KEY"
# Parse the instruction to a simple keyword
# "Select all images with traffic lights" → "traffic lights"
import re
keyword_match = re.search(r'(?:with|of|containing)\s+(.+?)\.?$', instruction, re.I)
keyword = keyword_match.group(1) if keyword_match else instruction
# Submit
with open("/tmp/grid.png", "wb") as f:
f.write(base64.b64decode(img_b64))
with open("/tmp/grid.png", "rb") as f:
resp = requests.post("https://ocr.captchaai.com/in.php",
files={"file": f},
data={
"key": API_KEY,
"method": "post",
"grid_size": grid_size,
"img_type": "recaptcha",
"instructions": keyword,
"json": "1",
}
).json()
if resp["status"] != 1:
raise Exception(f"Submit error: {resp['request']}")
task_id = resp["request"]
# Poll
for _ in range(20):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": "1"
}).json()
if result["status"] == 1:
cells = json.loads(result["request"])
print(f"Cells to click: {cells}") # e.g., [1, 3, 6, 9]
break
if result["request"] != "CAPCHA_NOT_READY":
raise Exception(f"Error: {result['request']}")
第三步:把单元格编号换算成点击坐标
CaptchaAI 返回从 1 开始的编号,浏览器要的是像素坐标,这一步把编号拆成行列,再算出每格中心点,公式很直接:
- 列号 = (编号 − 1) % 列数
- 行号 = (编号 − 1) ÷ 列数(取整)
- 中心点 x = 列号 × 格宽 + 格宽 ÷ 2,y 同理用格高算
def cell_to_coordinates(cell_index, grid_size, grid_width, grid_height):
"""Convert a 1-based cell index to (x, y) center coordinates."""
if grid_size == "3x3":
cols, rows = 3, 3
else:
cols, rows = 4, 4
cell_w = grid_width / cols
cell_h = grid_height / rows
# Convert 1-based index to 0-based row/col
idx = cell_index - 1
col = idx % cols
row = idx // cols
# Center of the cell
x = col * cell_w + cell_w / 2
y = row * cell_h + cell_h / 2
return int(x), int(y)
# Example: grid is 300×300
for cell in cells:
x, y = cell_to_coordinates(cell, grid_size, 300, 300)
print(f"Cell {cell} → ({x}, {y})")
以 300×300 的 3×3 网格为例,实际输出是:
Cell 1 → (50, 50)
Cell 3 → (250, 50)
Cell 6 → (250, 150)
Cell 9 → (250, 250)
第四步:点击对应的图块
两种工具思路一样:拿网格元素的实际宽高,代入第三步的公式算出目标坐标,再触发点击、按验证按钮。
Selenium
from selenium.webdriver.common.action_chains import ActionChains
driver.switch_to.frame(challenge_iframe)
# Get grid element position and size
grid_el = driver.find_element(By.CSS_SELECTOR, ".rc-imageselect-target")
grid_rect = grid_el.rect
grid_w = grid_rect["width"]
grid_h = grid_rect["height"]
actions = ActionChains(driver)
for cell in cells:
x, y = cell_to_coordinates(cell, grid_size, grid_w, grid_h)
# Click relative to grid element's top-left corner
actions.move_to_element_with_offset(
grid_el,
x - grid_w / 2, # offset from center
y - grid_h / 2
).click()
actions.perform()
# Click verify
verify_btn = driver.find_element(By.ID, "recaptcha-verify-button")
verify_btn.click()
driver.switch_to.default_content()
Puppeteer
// Click each cell by index
const tableRows = await challengeFrame.$$('table.rc-imageselect-table tr');
for (const cellIdx of cells) {
const row = Math.floor((cellIdx - 1) / (gridSize === '4x4' ? 4 : 3));
const col = (cellIdx - 1) % (gridSize === '4x4' ? 4 : 3);
const cell = (await tableRows[row].$$('td'))[col];
await cell.click();
await new Promise(r => setTimeout(r, 200));
}
await challengeFrame.click('#recaptcha-verify-button');
图块点击后又刷新了怎么办
部分 reCAPTCHA v2 网格会在点击后用新图片替换刚才那一格,这是设计好的机制,不是脚本出错。用重试循环处理,每一轮做四件事,直到通过或达到最大轮次:
- 重新截图当前网格
- 提交给 CaptchaAI 识别,拿新一批单元格编号
- 点击这些单元格
- 点验证按钮,检查挑战是否已经消失
def solve_with_dynamic_tiles(driver, api_key, max_rounds=3):
for round_num in range(max_rounds):
driver.switch_to.frame(challenge_iframe)
# Re-capture grid and instruction
img_b64 = driver.find_element(
By.CSS_SELECTOR, "img.rc-image-tile-33"
).screenshot_as_base64
# Submit and get cells (same as above)
cells = submit_and_poll(api_key, img_b64, "3x3", keyword)
if not cells:
break
# Click cells
click_cells(driver, cells, "3x3")
# Click verify
driver.find_element(By.ID, "recaptcha-verify-button").click()
driver.switch_to.default_content()
time.sleep(2)
# Check if solved (no more challenge iframe)
try:
driver.switch_to.frame(challenge_iframe)
driver.switch_to.default_content()
except Exception:
return True # Solved
return False
常见错误排查
| 问题 | 常见原因 | 处理方式 |
|---|---|---|
| 返回的单元格编号不对 | grid_size 传错 |
先确认是 3×3 还是 4×4 |
| 点击落在图块外面 | 坐标偏移算错 | 重新读取网格元素的实际宽高 |
ERROR_WRONG_FILE_EXTENSION |
图片格式不对 | 只用 PNG 或 JPEG |
| 点击后又冒出新图块 | 触发了动态网格 | 按上一节重试循环再识别一轮 |
常见问题
CaptchaAI 支持 4×4 网格吗?
支持,提交时把 grid_size 设为 4x4,返回的单元格编号会用 1-16。
单元格编号是从 0 开始还是从 1 开始?
从 1 开始,cell_to_coordinates 里第一步就是减 1 再算行列,自己重写逻辑时漏掉这步最容易导致坐标错位。
国内网络加载不出 reCAPTCHA 网格怎么办?
挑战脚本由 Google 托管,国内网络访问 Google 资源本来就不稳定,这属于网络可达性问题,不是代码或 API 调用写错了,排查时先确认能不能正常打开 reCAPTCHA 官方演示页。
网格图识别一般要等多久?
多数几秒到十几秒出结果,轮询间隔保持 5 秒左右即可,太频繁没有意义。
用 CaptchaAI 跑通网格验证码坐标计算
去 captchaai.com 注册拿到 API Key,直接套用本文的四步流程。