关于滑动验证码的解决方法,网上已经有很多详细的描述了,但是绝大多数都是用的python,这边就扩充一下java,但是验证准确率不是100%,只是强调一下在解决中可能会出现的问题。
注意点
元素截取
- 元素截图时,尽可能不要通过截取整个屏幕,然后截图子图的方式来截取验证码,这样会因为显示分辨率导致位置偏差存在,无法获取验证码的准确位置,可参考 https://blog.youkuaiyun.com/weixin_44578172/article/details/111387534
- java中直接调用元素的
getScreenshotAs
即可,但需要选择合理的标签,否侧在执行js代码后会出现图片位移少量的问题。我这边就是选择的最外侧的div标签来进行保存的,而且选择的是先保存无缺口的验证码,然后通过js代码恢复后,再保存具有缺口的验证码
缺口判断
- 扫描两张图片的起始位置会对缺口的判断有影响,可通过坐标尺或微信截图的方式,确定出方块的初始位置在哪里,即下面代码中
getGap
方法中的pos
变量 - 验证码缺口判断时,需注意RGB判断阈值,有些验证码中会有些虚假的阴影,并不是我们所要确定的验证码位置,因此,对于阈值的选择也十分重要,这会提高验证精度,即下面代码中
equalPixel
方法中的threshold
滑块移动
- 滑块在移动的过程中不要一直加速,因为实际人在操作中,最后一点会稍微减点速的,在计算滑块运动轨迹的时候,最后一个位移距离很有可能会造成滑块超出缺口边界,因此,需要强制对最后一个位移距离进行修改,避免该情况发生,详情见
trace
方法,滑块的移动尽可能真实的模仿人为操作,来提高准确率 - 滑块在移动时会有卡顿感,这是因为selenium中默认中的moveByOffset是有200ms的等待时间的,因此,我们可以根据源码重写该方法,使其等待时间为0ms,见
moveWithouWait
方法 - 滑块的边界与验证码的边界具有一定的距离,需要将这个距离给考虑进来,可根据一定的测试或测量来选择合适的值,即
main
方法中的left-=7
- 移动滑块的时候,一定要注意最后给释放掉
下面代码在实际中会出现验证码回拉的情况,但是这也是提高准确率的一种方式,但是目前来看,这种方法在理论上会出现无解的情况,即一直反复的拉来拉去。但在测试时,还并未出现该情况,但后续需要考虑优化。
/**
* Description: 实现滑动验证码的验证
* @date:2021/03/30 09:23
* @author: lyf
*/
public class SildeCode {
private WebDriver driver;
private Actions actions;
private WebElement element;
private JavascriptExecutor js;
private BufferedImage imgBefore; // 带有缺口的验证码
private BufferedImage imgAfter; // 不带有缺口的验证码
/**
* 初始化操作
*/
public void init() {
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.get("http://www.geetest.com/Register");
js = (JavascriptExecutor) driver;
js.executeScript("window.scrollTo(1,100)");
actions = new Actions(driver);
}
/**
* Selenium方法等待元素出现
* @param driver 驱动
* @param by 元素定位方式
* @return 元素控件
*/
public static WebElement WaitMostSeconds(WebDriver driver, By by) {
try {
WebDriverWait AppiumDriverWait = new WebDriverWait(driver, 5);
return (WebElement) AppiumDriverWait.until(ExpectedConditions
.presenceOfElementLocated(by));
} catch (Exception e) {
e.printStackTrace();
}
throw new NoSuchElementException("元素控件未出现");
}
/**
* 保存截图的方法
* @param screen 元素截图
* @param name 截图保存名字
*/
public void savePng(File screen, String name) {
String screenShortName = name + ".png";
try {
System.out.println("save screenshot");
FileUtils.copyFile(screen, new File(screenShortName));
} catch (IOException e) {
System.out.println("save screenshot fail");
e.printStackTrace();
} finally {
System.out.println("save screenshot finish");
}
}
/**
* 获取无缺口的验证码和带有缺口的验证码
*/
public void saveCode() {
// 获取无缺口的截图
js.executeScript("document.querySelectorAll('canvas')[2].style=''");
element = WaitMostSeconds(driver, By.cssSelector("div.geetest_window"));
File screen = element.getScreenshotAs(OutputType.FILE); //执行屏幕截取
savePng(screen, "无缺口");
// 获取有缺口的截图
js.executeScript("document.querySelectorAll('canvas')[2].classList=[]");
element = WaitMostSeconds(driver, By.cssSelector("div.geetest_window"));
screen = element.getScreenshotAs(OutputType.FILE); //执行屏幕截取
savePng(screen, "有缺口");
}
/**
* 比较两张截图上的当前像素点的RGB值是否相同
* 只要满足一定误差阈值,便可认为这两个像素点是相同的
*
* @param x 像素点的x坐标
* @param y 像素点的y坐标
* @return true/false
*/
public boolean equalPixel(int x, int y) {
int rgbaBefore = imgBefore.getRGB(x, y);
int rgbaAfter = imgAfter.getRGB(x, y);
// 转化成RGB集合
Color colBefore = new Color(rgbaBefore, true);
Color colAfter = new Color(rgbaAfter, true);
int threshold = 80; // RGB差值阈值
if (Math.abs(colBefore.getRed() - colAfter.getRed()) < threshold &&
Math.abs(colBefore.getGreen() - colAfter.getGreen()) < threshold &&
Math.abs(colBefore.getBlue() - colAfter.getBlue()) < threshold) {
return true;
}
return false;
}
/**
* 比较两张截图,找出有缺口的验证码截图中缺口所在位置
* 由于滑块是x轴方向位移,因此只需要x轴的坐标即可
*
* @return 缺口起始点x坐标
* @throws Exception
*/
public int getGap() throws Exception {
imgBefore = ImageIO.read(new File("有缺口.png"));
imgAfter = ImageIO.read(new File("无缺口.png"));
int width = imgBefore.getWidth();
int height = imgBefore.getHeight();
int pos = 60; // 小方块的固定起始位置
// 横向扫描
for (int i = pos; i < width; i++) {
for (int j = 0; j < height; j++) {
if (!equalPixel(i, j)) {
pos = i;
return pos;
}
}
}
throw new Exception("未找到滑块缺口");
}
/**
* 计算滑块到达目标点的运行轨迹
* 先加速,后减速
* @param distance 目标距离
* @return 运动轨迹
*/
public List<Integer> trace(int distance) {
java.util.List<Integer> moveTrace = new ArrayList<>();
int current = 0; // 当前位移
int threshold = distance * 3 / 5; // 减速阈值
double t = 0.2; // 计算间隔
double v = 0.0; // 初速度
double a; // 加速度
while (current < distance) {
if (current < threshold) {
a = 2;
} else {
a = -4;
}
// 位移计算公式
double tmp = v;
// 移动速度,会出现负值的情况,然后往反方向拉取
v = tmp + a * t;
int move = (int) (tmp * t + 0.5 * a * t * t);
current += move;
moveTrace.add(move);
}
// 考虑到最后一次会超出移动距离,将其强制修改回来,不允许超出
int length = moveTrace.size();
moveTrace.set(length - 1, moveTrace.get(length - 1) + (current > distance ? -(current - distance) : 0));
return moveTrace;
}
/**
* 消除selenium中移动操作的卡顿感
* 这种卡顿感是因为selenium中自带的moveByOffset是默认有200ms的延时的
* 可参考:https://blog.youkuaiyun.com/fx9590/article/details/113096513
*
* @param x x轴方向位移距离
* @param y y轴方向位移距离
*/
public void moveWithoutWait(int x, int y) {
PointerInput defaultMouse = new PointerInput(MOUSE, "default mouse");
actions.tick(defaultMouse.createPointerMove(Duration.ofMillis(0), PointerInput.Origin.pointer(), x, y)).perform();
}
/**
* 移动滑块,实现验证
* @param moveTrace 滑块的运动轨迹
* @throws Exception
*/
public void move(List<Integer> moveTrace) throws Exception {
// 获取滑块对象
element = WaitMostSeconds(driver, By.cssSelector("div.geetest_slider_button"));
// 按下滑块
actions.clickAndHold(element).perform();
Iterator it = moveTrace.iterator();
while (it.hasNext()) {
// 位移一次
int dis = (int) it.next();
moveWithoutWait(dis, 0);
}
// 模拟人的操作,超过区域
moveWithoutWait(5, 0);
moveWithoutWait(-3, 0);
moveWithoutWait(-2, 0);
// 释放滑块
actions.release().perform();
Thread.sleep(500);
}
/**
* 调出验证码时的一些准备工作
* @throws Exception
*/
public void prepare() throws Exception {
// 调出验证码
element = WaitMostSeconds(driver, By.cssSelector("div.phone > input"));
element.clear();
element.sendKeys("12345678910");
element = WaitMostSeconds(driver, By.cssSelector("div.sendCode"));
element.click();
// 等待验证码出现
element = WaitMostSeconds(driver, By.cssSelector("a.geetest_close"));
Thread.sleep(500);
// 保存验证码
saveCode();
}
public static void main(String[] args) throws Exception {
SildeCode sc = new SildeCode();
sc.init();
sc.prepare();
int left = sc.getGap();
// 验证码的边界差值
left -= 7;
List<Integer> moveTrace = sc.trace(left);
sc.move(moveTrace);
}
}
好好学习,天天向上