历史技术记录。 本文以 2022 年页面结构为背景,讲解离线图片匹配、坐标换算和轨迹生成。图像匹配结果不等于服务端验证通过;示例不保证适用于当前验证码。
一、ddddocr 在这里负责什么
这次实验使用 ddddocr 的 slide_match,给它滑块图片和背景图片,获得候选矩形坐标。图像匹配、文本 OCR、目标检测与按指定顺序点图标是不同任务,不能由一个接口的结果推出全部验证码都能完成。
2022 年测试页面的图片原始尺寸是 300×150,页面显示为 260×130。这个比例只属于当时页面;现在应读取实际图片宽度与渲染宽度,还要考虑裁剪、初始位置和 CSS 变换。

二、先在本地图片上确认匹配结果
安装环境后,准备自己的 target.png 和 background.png:
python -m pip install ddddocr
from pathlib import Path
import ddddocr
def match_local(target_path, background_path, simple_target=False):
matcher = ddddocr.DdddOcr(det=False, ocr=False)
result = matcher.slide_match(
Path(target_path).read_bytes(),
Path(background_path).read_bytes(),
simple_target=simple_target)
box = result.get('target')
if not isinstance(box, (list, tuple)) or len(box) != 4:
raise ValueError(f'未得到预期的匹配矩形:{result}')
x1, y1, x2, y2 = box
if x2 <= x1 or y2 <= y1:
raise ValueError(f'匹配矩形无效:{box}')
return result
if __name__ == '__main__':
print(match_local('target.png', 'background.png'))
当前上游示例将 target 解读为 [x1, y1, x2, y2]。透明滑块和不带透明背景的目标可能要使用不同设置;simple_target=True 不是所有图片的默认答案。官方滑块匹配用法
如果原图来自 data URI,应先分离元信息、验证 Base64,再解码,不用一个贪婪正则假定所有内容都一定是图片:
import base64
def decode_data_image(value):
header, separator, payload = value.partition(',')
if not separator or not header.startswith('data:image/') or ';base64' not in header:
raise ValueError('预期 Base64 图片 data URI')
return base64.b64decode(payload, validate=True)
三、从图片坐标到显示坐标
无裁剪、无额外变换且匹配坐标确实以完整背景左上角为原点时,可以按宽度缩放:
def image_x_to_css(x_image, image_width, rendered_width):
if image_width <= 0 or rendered_width <= 0:
raise ValueError('图片宽度必须为正')
return x_image * rendered_width / image_width
这个函数只返回背景里的显示位置。真正的拖动位移还要减去滑块当前显示位置,并考虑匹配器是否裁去了滑块透明边缘;缺口左边缘、拼图边缘与按钮中心可能不是同一坐标。缩放不均匀、页面滚动或设备像素比变化也要按实际渲染关系检查,不能始终写 target[0] * 260 / 300。
四、原轨迹和浏览器片段为何不能沿用
旧 get_track() 用固定初速、加速和减速积分,没有步数上限;长距离情况下速度可能减成负值,累计位移倒退,循环无法结束。逐步 round() 也使整数位移和原浮点累计量不同,所有动作相加不保证等于目标距离。
在自有测试页面或动画中,需要生成位移序列时,可以用有上限的插值并检查总和;它只是数学轨迹,不代表任何验证码服务认可的用户行为:
def integer_steps(distance, count=20):
if distance < 0 or count <= 0:
raise ValueError('distance 必须非负,count 必须为正')
target = round(distance)
points = [round(target * i / count) for i in range(count + 1)]
steps = [right - left for left, right in zip(points, points[1:])]
assert sum(steps) == target
return steps
原 move_target(drive) 中同时使用 drive、未定义的 driver 和 self.driver,会产生 NameError;这不是识别率问题。程序还应区分元素不存在、图片解析失败、匹配失败和服务端拒绝,不通过继续尝试掩盖错误。

滑动后是否出现图标验证取决于服务端流程,不能把某次没有出现额外验证的现象当作固定规则;进一步验证出现时应按页面交互完成,不能由 ddddocr 这个库的名字推定它会识别所要求的图标顺序。离线图像匹配示例不验证当前携程页面的完整交互流程。
评论区