Selenuim 解决最简单滑动验证码 java版
新年快乐!
先记录下写这篇随笔的初衷: 同一个项目组的测试组在推广写自动化用例,组长说用selenium写,底下的测试们只能照做喽。一群测试小伙伴们都没学过java,于是一个玩得来的测试同事经常来找我,问我java语法、selenium定位元素等等怎么写。 我也没学过selenium,不过懂点java语法。 他们碰到一个难题,自动化用例都阻塞在登陆页面了,开发、测试环境页面上有用到极验验证码,还好只是最简单的滑动验证码。这下自动化用例登陆都登不进去,全阻塞在那儿了。 用selenium的java版本写出来了,也算有点收获,毕竟靠自己亲手解决了一个难题。
坎坷经历:最开始看到这样一个滑动验证码,思路是验证会发送ajax请求,请求参数携带了一个加密的字符串w, 一开始还想着从geetest.js文件入手,我没有啥JavaScript专业的学习,研究了一天半天就放弃了。毕竟JS编码加混淆对我来说太难了。 后来换了个思路,把这两张图给他另存为到本地,然后通过比较像素点,找到缺口的横坐标。 想法是好的, 但是验证码用canvas画的,我没有用过canvas,但是看到验证码可以右键另存为到本地,于是我又去搜索selenium如何右键另存为图片(要使用autoit,好像有点困难,于是我放弃了),这下又折腾了一两天。 后来旁边的前端小伙伴告诉我canvas可以转为base64编码,于是我就试了一试,所幸我们的环境上是可行的。 回家后又在本地随意找了个网站试了试,可行的所以记录下来。如果没有硬性要求要用selenium,也没有严格的内网环境,完全可以使用python,github上有解决方案,或者使用别人开发好的接口,好像是收费的。我只是兴趣之余给我的测试小伙伴减少工作难题,毕竟人家老是来找我探讨。
0 演示效果图
效果图大约2M,文件有点大,页面加载可能有点慢。
1 滑块验证码思路
1.1 像素点
将有缺口和没有缺口的验证码图片逐点比对,像素点差60以上,我们就认为这个点是缺块内的;找到第一个缺块点的X坐标,滑块移动到这就个位置了。
1.2 移动轨迹
测试的时候发现滑块对上了,但是提示"怪物吃掉了",看接口响应是 forbidden,应该是运动轨迹被识别为自动的,所以禁止登陆。这个时候你完全可以写个自己的运动轨迹。先加速后匀速,或者匀速过去,超一点再倒回来。 方式可以有很多,实现目的就行。
2 JAVA代码
2.1 java实现代码
测试类代码如下:
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
public class AutoTests {
public static void main(String[] args) throws Exception {
try {
System.setProperty("webdriver.chrome.driver", "D:\\new_soft\\chromedriver.exe");
WebDriver driver = new ChromeDriver();
driver.get("https://www.geetest.com/Register");
driver.manage().window().maximize();
driver.findElement(By.xpath("//input[@placeholder=\"手机号码\"]")).sendKeys("11111111111");
driver.findElement(By.xpath("//div[text()=\"获取验证码\"]")).click();
//处理滑动验证码
SlideVerifyBlock.common(driver);
Thread.sleep(2000);
driver.quit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
滑块验证码代码如下:
滑块验证码算出来的像素点距离减去8,是因为滑块距离图片最左边一般都是紧贴着的,我量了一下大约8,所以才减去这么多。moveWay1
和moveWay2
于2021/2/12测试时候是均可通过的。
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import sun.misc.BASE64Decoder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class SlideVerifyBlock {
// 减少计算50
private static final int OFFSET = 50;
// 滑块和左边轴都有一定的距离, 误差范围内都可以接受
private static final int LEFT_GAP = 8;
//加速度
private static int a_speed = 150;
public static void common(WebDriver driver) throws Exception {
String bgClassName = "geetest_canvas_bg geetest_absolute",
bgFullClassName = "geetest_canvas_fullbg geetest_fade geetest_absolute";
String getImgBase64 = "return document.getElementsByClassName(\'%s\')[0].toDataURL('image/png');";
//等待一下验证码加载出来
Thread.sleep(1000);
String bgImg = ((JavascriptExecutor) driver).executeScript(String.format(getImgBase64, bgClassName)).toString();
bgImg = bgImg.startsWith("data:image/png;base64,") ? bgImg.substring(22) : bgImg;
String bgFullImg = ((JavascriptExecutor) driver).executeScript(String.format(getImgBase64, bgFullClassName)).toString();
bgFullImg = bgFullImg.startsWith("data:image/png;base64,") ? bgFullImg.substring(22) : bgFullImg;
BASE64Decoder decoder = new BASE64Decoder();
byte[] bgBytes = decoder.decodeBuffer(bgImg);
byte[] bgFullBytes = decoder.decodeBuffer(bgFullImg);
generateFile(bgBytes, "C:\\Users\\Administrator\\Desktop\\笔记", "bg.png");
generateFile(bgFullBytes, "C:\\Users\\Administrator\\Desktop\\笔记", "bgfull.png");
InputStream inputStream = new ByteArrayInputStream(bgBytes);
InputStream inputStream2 = new ByteArrayInputStream(bgFullBytes);
BufferedImage bg = ImageIO.read(inputStream), fullBg = ImageIO.read(inputStream2);
;
int width = bg.getWidth(), height = bg.getHeight();
int[] tmp1 = new int[3], tmp2 = new int[3];
int gap = 0;
outer:
for (int i = OFFSET; i < width; i++) {
for (int j = OFFSET; j < height; j++) {
int rgb1 = bg.getRGB(i, j), rgb2 = fullBg.getRGB(i, j);
tmp1[0] = (rgb1 >> 16) & 0xff;
tmp1[1] = (rgb1 >> 8) & 0xff;
tmp1[2] = rgb1 & 0xff;
tmp2[0] = (rgb2 >> 16) & 0xff;
tmp2[1] = (rgb2 >> 8) & 0xff;
tmp2[2] = rgb2 & 0xff;
if (Math.abs(tmp1[0] - tmp2[0]) < 60 && Math.abs(tmp1[1] - tmp2[1]) < 60 && Math.abs(tmp1[2] - tmp2[2]) < 60) {
continue;
} else {
gap = i;
break outer;
}
}
}
inputStream.close();
inputStream2.close();
System.out.println("缺口位置:" + gap);
//得到缺块坐标移动
WebElement slider = driver.findElement(By.className("geetest_slider_button"));
moveWay1(driver, slider, gap);
}
public static void generateFile(byte[] data, String fileDirector, String filename) throws Exception {
FileOutputStream stream = new FileOutputStream(fileDirector + File.separator + filename);
stream.write(data);
stream.flush();
stream.close();
}
public static void moveWay1(WebDriver driver, WebElement slider, int gap) {
Actions actions = new Actions(driver);
actions.clickAndHold(slider);
actions.perform();
List<Double> doubles = moveManualiy(driver, gap - LEFT_GAP);
Double[] array = doubles.toArray(new Double[doubles.size()]);
double res = 0;
for (int i = 0; i < array.length; i++) {
// 由于经常会出现 forbidden
int intValue = new Double(array[i]).intValue();
res += (array[i] - intValue);
actions.moveByOffset(intValue, (i % 2) * 1);
actions.perform();
}
actions.moveByOffset(new Double(res).intValue(), 0);
actions.pause(200 + new Random().nextInt(300)).release(slider);
actions.perform();
}
// 先过去,再倒回来
public static void moveWay2(WebDriver driver, WebElement slider, int gap) {
Actions actions = new Actions(driver);
actions.clickAndHold(slider);
actions.moveByOffset(gap - LEFT_GAP, 0);
actions.perform();
actions.moveByOffset(10, 0);
actions.perform();
actions.moveByOffset(-10, 0);
actions.perform();
actions.pause(200 + new Random().nextInt(300)).release(slider);
actions.perform();
}
public static List<Double> moveManualiy(WebDriver driver, double distance) {
DecimalFormat df = new DecimalFormat("######0.00");
// 先加速移动 然后匀速移动 1/2*a*t0^2 + at0 * t1 = distance
double current = 0;
ArrayList<Double> list = new ArrayList<>();
int a = 0, cnt = 0;
while (current < distance) {
if (cnt < 10) {
a = a_speed;
} else {
a = 0;
}
if (a == a_speed) {
list.add(Double.valueOf(df.format(0.5 * a * Math.pow((cnt + 1) * (0.1), 2) - current)));
current = 0.5 * a * Math.pow((cnt + 1) * (0.1), 2);
} else {
if (distance - current < a_speed * 0.1) {
break;
} else {
current = current + a_speed * 0.1;
list.add(a_speed * 0.1);
}
}
cnt++;
}
list.add(distance - current);
return list;
}
}
3 收获
和测试小伙伴一起学习selenium使用过程中,也学到了一些知识。学习一些东西,有一些收获就要记录下来。虽然以后可能不怎么会用到这门技术。
3.1 xpath
以前我都不知道什么是xpath,我理解的xpath是定位扩展标记语言的方式,比如通过selenium定位元素:F12控制台还能这么用,右键直接可以复制元素的xpath,直接就能用了。
优点:简单快捷
缺点:/html/body/div[3]/div[2]/div[6]/div/div[1]/div[1]/div/a/div[1]/div/canvas[2]
形如这种页面稍微变化下,就不能用了; 不利于阅读。
xpath几种定位方式:
-
//元素名[@属性名="属性值"]
比如//div[@id="udesk_container"]
-
//元素名[text()="文本内容"]
比如//p[text()="武汉极意网络科技有限公司"]
-
//元素名/parent::父元素类型
比如//p[text()='如果需要了解产品信息或售前服务欢迎联系我们']/parent::div
,用来定位到查找元素的父亲元素 -
//元素名/子元素名[下标从1开始]
比如//div[@class="geetest_panel_success geetest_success_animate"]/div[2]
,用来查找指定元素的子元素
以上就是我从xpath中学到的以及在日常中能常使用的。
3.2 base64图片内容转为字节数组
转为字节数组之后就可以生成文件到本地。
BASE64Decoder decoder = new BASE64Decoder();
byte[] bgBytes = decoder.decodeBuffer(bgImg);
FileOutputStream stream = new FileOutputStream(fileDirector + File.separator + filename);
stream.write(data);
stream.flush();
stream.close();