验证码是爬虫程序绕不过的坎,有各种各样的验证码挡在前进的道路上,比如本文将要重点介绍的旋转验证码,网上能找到不少关于这种验证码的逆向方法,整体思路都是一样的,首先需要通过深度学习模型识别出图片的旋转角度,接下来逆向分析加密旋转角度的一系列过程的js代码,根据逆向出的过程构造出对应的参数,模拟验证码验证接口调用实现旋转验证码的识别。
不得不说,独自搞定这一切相当耗时、耗力,而且还可能无功而返,怪不得逆向会经常伴随着掉头发的调侃,即使参照网上大佬们的技术文章,真正实践起来才发现每走一步都可能有坑,有可能是平台反爬策略更新导致和原文已不一致,也可能因为内容比较敏感导致关键细节被故意隐藏,还可能是技术实力欠缺等等。总之,这种逆向的方法让很多人倍感无力,也许硬着头皮研究下去最终可以搞定,但肯定需要花费不少的时间,而时间往往是不够用的。
这里提出另外一种经过验证可行的解决方案,那就是直接模拟用户鼠标操作去拖动滑块实现旋转验证码识别(其他验证码也可以使用这个思路)。简单分析下下面的旋转码操作就可以发现滑块滑到头,图片就旋转了一圈360°,它们之间是线性关系,可以计算出这个比例关系,一旦确定了图片的旋转角度就可以根据这个比例关系换算出滑块滑动的距离,然后模拟拖动滑块就完成了旋转验证码的验证。
这种处理验证码的方式完全不需要去逆向分析前端代码,完全是站在用户的视角在操作,当然它显得不够极客,可能性能也稍差点,但是真的不会掉太多头发,关键是它可以解决问题,不管黑猫白猫,能抓老鼠就是好猫。按照这个思路的话,只需要重点解决下面几个问题:
截取图片数据
这一步的目的是提取旋转验证码图片数据,用于接下来使用深度学习模型识别旋转角度。可以通过JavaScript代码创建canvas画布并绘制验证码图像的方式来获取图片数据,如果你使用selenium、playwright这样的自动化框架的话,它们都支持直接执行js代码并获取返回结果。这里需要注意的地方,一是图像元素需要设置允许跨域,否则无法获取到图片数据,而且设置跨域属性会触发重新请求图片,如果设置跨域后立刻绘制图像是获取不到数据的,需要等待响应图片渲染完成。另外一个就是canvas.toDataURL需要指定图片格式为png,因为jpeg是不支持透明背景的,使用jpeg格式会导致旋转角度识别库识别旋转角度完全错误。
// img_selector表示验证码图片选择器
var imgelm=document.querySelector(img_selector);
// 设置允许跨域,否则无法获取图片数据
// TIP:设置跨域后会触发重新请求图片,
// 立刻获取图片尺寸结果为全0,需要等待一段时间重新获取
imgelm.setAttribute("crossOrigin","anonymous");
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width=imgelm.naturalWidth;
canvas.height=imgelm.naturalHeight;
ctx.drawImage(imgelm, 0, 0);
// 注意:jpeg格式不支持透明背景,这里使用png格式
var dataURL = canvas.toDataURL('image/png');
return dataURL;
识别旋转角度
关于旋转角度识别,可以使用https://github.com/Starry-OvO/rotate-captcha-crack这个模型,这里通过运行server.py开启一个web服务,通过调用接口,传入图片数据会返回识别出的旋转角度。
(使用指导参考:https://cloud.tencent.com/developer/article/2418786)
不过,由于百度旋转验证码图片已经升级,图片全部使用AI生成,更不易识别,当前的模型识别成功率有点低,不过可以通过增加重试来规避这个问题,如果你对深度学习比较熟悉的话也可以使用它的模型重新训练最新的验证码图片。
另外,百度除了会出现旋转验证码之外,有时候也会出现其他类型验证,比如滑块验证码,这种情况就需要同时处理滑块验证码和旋转验证码,思路也是一样的,至于滑块验证码的识别可以使用ddddocr这个库(https://gitee.com/fkgeek/ddddocr),官网文档给出了详细的使用方法。
模拟拖动滑块
不管是滑块验证码还是旋转验证码,识别出了图片滑块的距离、旋转角度之后都可以简单地通过线性关系得出拖动滑块的距离,接下来就是模拟滑块拖动指定距离就可以了,下面的js代码实现了模拟拖动滑块的功能。代码中滑动距离仅给出示例,实际运行时请改为换算后的动态值。
() => {
// selector:滑块选择器 offsetX: 滑动距离
function slide(selector, offsetX) {
// 滑块定位
let slider = document.querySelector(selector);
// 计算滑块矩形区域对角坐标
let rect = slider.getBoundingClientRect(),
x0 = rect.x || rect.left,
y0 = rect.y || rect.top,
x1 = x0 + offsetX,
y1 = y0;
// 模拟鼠标按下动作
let mousedown = document.createEvent('MouseEvents');
mousedown.initMouseEvent('mousedown', true, true, window, 0, x0, y0, x0, y0, false, false, false, false, 0, null);
slider.dispatchEvent(mousedown);
//模拟鼠标拖动动作,将滑块移动距离简单拆分为4个相同距离的动作
let mousemove = document.createEvent('MouseEvents');
mousemove.initMouseEvent('mousemove', true, true, window, 0, x0+offsetX/4, y1, x0+offsetX/4, y1, false, false, false, false, 0, null);
slider.dispatchEvent(mousemove);
mousemove.initMouseEvent('mousemove', true, true, window, 0, x0+offsetX/2, y1, x0+offsetX/2, y1, false, false, false, false, 0, null);
slider.dispatchEvent(mousemove);
mousemove.initMouseEvent('mousemove', true, true, window, 0, x0+3*offsetX/4, y1, x0+3*offsetX/4, y1, false, false, false, false, 0, null);
slider.dispatchEvent(mousemove);
mousemove.initMouseEvent('mousemove', true, true, window, 0, x0+offsetX, y1, x0+offsetX, y1, false, false, false, false, 0, null);
slider.dispatchEvent(mousemove);
// 模拟鼠标释放动作
let mouseout = document.createEvent('MouseEvents');
mouseout.initMouseEvent('mouseup', true, true, window, 0, x1, y1, x1, y1, false, false, false, false, 0, null);
slider.dispatchEvent(mouseout);
}
// 执行滑块拖动,以下distance和滑块选择器只是示例,根据实际情况进行调整
var distance = 200;
slide('div.passMod_slide-btn', distance);
}
最后,展示一下通过上面的思路使用playwright实现的旋转验证码的识别过程,本次出现了两次验证码,第一次是滑块验证码,通过后又出现了旋转验证码,可以发现旋转验证码识别了3次才成功,就是因为模型对于最新的百度旋转验证码图片角度识别率变差导致的,实现上通过增加重试次数进行规避。经测试,无界面模式也可以正常运行。
参考文献:
[1]https://cloud.tencent.com/developer/article/2418786
[2]https://github.com/Starry-OvO/rotate-captcha-crack
[3]https://gitee.com/fkgeek/ddddocr
[4]https://blog.csdn.net/m0_58618019/article/details/132918404