新年快樂!
先記錄下寫這篇隨筆的初衷: 同一個項目組的測試組在推廣寫自動化用例,組長說用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();