java写投票脚本自动化初探
由于大学里面有一些任务需要投票,而又不想劳烦他人去帮我,所以打算自己写一个脚本来实现自动投票的功能。这里记录一下我的整个过程。
技术选型
java8
selenium3 实现chrome自动化的jar包
AUTolt 模拟键盘操作的软件
- 导入selenium3
这里我用的maven导入的,代码如下:
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-server</artifactId>
<version>3.141.59</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.14.0</version>
</dependency>
代码结构
利用代码启动chrome
由于我这里是用的chrome实现的自动化,所以我们先要下chrome的启动器
我们需要下载对应版本的启动器,不然会失效,首先查看自己的chrome版本,然后打开每个版本的文件夹,里面有notes.txt,显示对应的支持版本范围。
下载好之后随便放到一个位置,后面根据路径引入即可,为了方便我将程序放到我的resource目录下了,如上图所示,代码如下:
// 设置webdirver路径 这个BrushTicket是指的这个代码所在的类。
System.setProperty("webdriver.chrome.driver", BrushTicket.class.getClassLoader().getResource("chromedriver.exe").getPath());
// 创建ChromeOptions,options可以设置一些网页请求头啥的
ChromeOptions options = new ChromeOptions();
// 指定本机chrome安装位置
options.setBinary("C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe");
// 创建WebDriver对象
ChromeDriver driver = new ChromeDriver(options);
// 打开那个网站
driver.get("https://www.wjx.cn/m/36384473.aspx");
要记住,这个driver是最核心的类,我们后面根据这个driver在网页上做各种操作
网页自动点击操作
这个做法很简单,基本就是定位需要点击的位置,然后点击。
我截取了一小段代码,基本上所有点击的功能就都会了
driver.findElement(By.xpath("//div[@id='divSubmit']/div[2]")).click();
嗯哼,这里其实就需要注意一下这个xpath的定位问题,//div[@id='xxx'],就是定位这个id为啥的元素,其实也可以用通配符,比如说 //* [@id='xxx']。后面再像洋葱一样,一层层的扒网页就行。
这个findElement的返回值是WebElement,这个元素又可以做各种操作,比如取css的属性,或者自己的属性,也可以点击和输入值(sendKeys)
验证码(未完全解决)
本来以为点完各种按钮,提交之后就完事,结果不!突然出现了验证码。
挣扎了很久,我选择了将验证码下载到本地后,然后解析出验证码这个解决办法。
解决弹窗问题
我们没有管验证码直接提交,会弹出一个警告框,我们先解决弹窗,后来发现问卷星的网页结构是,弹窗由一个div构成的,如果没有弹过的时候是没有这个结构的,第二次弹出之前,会将之前的div由display:none属性改回block属性,这样就简单了。具体代码如下:
/**
* 功能描述: //判断是否由弹出框
* @Param: [driver]
* @Return: boolean
* @Author: WHOAMI
* @Date: 2019/6/8 22:21
*/
private boolean ifAlert(ChromeDriver driver){
try
{
//定位元素,如果没有这个元素直接报错
WebElement webElement = driver.findElement(By.xpath("//div[@id='alert_box']"));
Thread.sleep(300);
if(!webElement.getCssValue("display").equals("none")) {
return true;
}
}
catch (Exception Ex)
{
Ex.printStackTrace();
return false;
}
return false;
}
根据有没有Alert框判断是不是让我们输入验证码。
下载图片
首先我们需要利用Action解决右键另存图片这个操作
具体代码如下:
// 这个driver是ChromeDriver
Actions action = new Actions(driver);
// 定位到验证码图片的位置
WebElement element = driver.findElement(By.xpath("//div[@id='tdCode']/table/tbody/tr/td[3]/img"));
// action移动到指定位置
action.moveToElement(element);
// 休眠等待网页反应
Thread.sleep(200);
// 这里模拟右键 打开菜单
action.contextClick(element).build().perform();
//模拟键盘操作(这里是移动向下方向键) 这个Robot是java.awt包下面的
Robot robot = new Robot();
Thread.sleep(400);
// 模拟键盘下方向键
robot.keyPress(KeyEvent.VK_DOWN);
Thread.sleep(400);
robot.keyPress(KeyEvent.VK_DOWN);
// 模拟回车键
Thread.sleep(400);
robot.keyPress(KeyEvent.VK_ENTER);
后面需要了解一下java.awt这个包
由于后面的选择保存位置然后下载操作是selenium3实现不了的,所以我们选择了AUTolt这个软件,下载网址
,然后安装即可,这里不再赘述。
软件安装好之后,是这个样子的
操作AUTolt定位窗口
先打开游览器准备上面另存为的操作
定位另存为窗口需要这个数据 Class: 比如上面的窗口时#32770,下面写脚本需要用。
定位按钮啊,编辑框啥的需要这个数据 CLassNameNN
其实我这里为了方便保存图片都是一个名字,而且为了方便都没有删除图片,所以这里出现了一个替换的问题,解决办法其实就是在定位一下替换的窗口和确定按钮就行
写脚本
打开写脚本的工具,我的代码是这样的,改改就能用
ControlFocus("另存为", "","Edit1")
;ControlFocus("title","text",controlID) Edit1=Edit instance 1
; Wait 10 seconds for the Upload window to appear
WinWait("[CLASS:#32770]","",10)
; Set input focus to the edit control of Upload window using the handle returned by WinWait
ControlFocus("另存为","","Edit1")
Sleep(1000)
; Set the File name text on the Edit field
ControlSetText("另存为", "", "Edit1", "buff.gif")
Sleep(200)
; Click on the Open button
ControlClick("另存为", "","Button2");
ControlFocus("确认另存为", "","Edit2")
WinWait("[CLASS:#32770]","",10)
sleep(200)
ControlClick("确认", "","Button1");
写完脚本后,保存成au3格式的文件,准备合成exe
合成exe文件
打开合成exe程序,确定脚本和生成exe路径即可。
java引用该文件
//调用你使用Compile Script to.exe生成的可执行exe文件 这个download.exe就是我生成的exe文件
//对Windows窗体进行操作:更换文件名,并保存到指定文件夹
Runtime.getRuntime().exec("D:\\code\\java\\download.exe");
//代码等待程序完成在进行接下来的操作。
Thread.sleep(4000);
验证码去除干扰线
由于我没有学openCV算法,所以我就直接粘代码了。
/**
* 功能描述: //验证码去除干扰线
* @Param: [sfile:图片文件路径, destDir:文件名字]
* @Return: void
* @Author: WHOAMI
* @Date: 2019/6/8 22:11
*/
public static void cleanLinesInImage(File sfile, String destDir) throws IOException {
File destF = new File(destDir);
if (!destF.exists())
{
destF.mkdirs();
}
BufferedImage bufferedImage = ImageIO.read(sfile);
int h = bufferedImage.getHeight();
int w = bufferedImage.getWidth();
// 灰度化
int[][] gray = new int[w][h];
for (int x = 0; x < w; x++)
{
for (int y = 0; y < h; y++)
{
int argb = bufferedImage.getRGB(x, y);
// 图像加亮(调整亮度识别率非常高)
int r = (int) (((argb >> 16) & 0xFF) * 1.1 + 30);
int g = (int) (((argb >> 8) & 0xFF) * 1.1 + 30);
int b = (int) (((argb >> 0) & 0xFF) * 1.1 + 30);
if (r >= 255)
{
r = 255;
}
if (g >= 255)
{
g = 255;
}
if (b >= 255)
{
b = 255;
}
gray[x][y] = (int) Math
.pow((Math.pow(r, 2.2) * 0.2973 + Math.pow(g, 2.2)
* 0.6274 + Math.pow(b, 2.2) * 0.0753), 1 / 2.2);
}
}
// 二值化
int threshold = ostu(gray, w, h);
BufferedImage binaryBufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY);
for (int x = 0; x < w; x++)
{
for (int y = 0; y < h; y++)
{
if (gray[x][y] > threshold)
{
gray[x][y] |= 0x00FFFF;
} else
{
gray[x][y] &= 0xFF0000;
}
binaryBufferedImage.setRGB(x, y, gray[x][y]);
}
}
//去除干扰线条
for(int y = 1; y < h-1; y++){
for(int x = 1; x < w-1; x++){
boolean flag = false ;
if(isBlack(binaryBufferedImage.getRGB(x, y))){
//左右均为空时,去掉此点
if(isWhite(binaryBufferedImage.getRGB(x-1, y)) && isWhite(binaryBufferedImage.getRGB(x+1, y))){
flag = true;
}
//上下均为空时,去掉此点
if(isWhite(binaryBufferedImage.getRGB(x, y+1)) && isWhite(binaryBufferedImage.getRGB(x, y-1))){
flag = true;
}
//斜上下为空时,去掉此点
if(isWhite(binaryBufferedImage.getRGB(x-1, y+1)) && isWhite(binaryBufferedImage.getRGB(x+1, y-1))){
flag = true;
}
if(isWhite(binaryBufferedImage.getRGB(x+1, y+1)) && isWhite(binaryBufferedImage.getRGB(x-1, y-1))){
flag = true;
}
if(flag){
binaryBufferedImage.setRGB(x,y,-1);
}
}
}
}
// 矩阵打印
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
if (isBlack(binaryBufferedImage.getRGB(x, y)))
{
System.out.print("*");
} else
{
System.out.print(" ");
}
}
System.out.println();
}
ImageIO.write(binaryBufferedImage, "jpg", new File(destDir, sfile
.getName()));
}
public static boolean isBlack(int colorInt)
{
Color color = new Color(colorInt);
if (color.getRed() + color.getGreen() + color.getBlue() <= 300)
{
return true;
}
return false;
}
public static boolean isWhite(int colorInt)
{
Color color = new Color(colorInt);
if (color.getRed() + color.getGreen() + color.getBlue() > 300)
{
return true;
}
return false;
}
public static int isBlackOrWhite(int colorInt)
{
if (getColorBright(colorInt) < 30 || getColorBright(colorInt) > 730)
{
return 1;
}
return 0;
}
public static int getColorBright(int colorInt)
{
Color color = new Color(colorInt);
return color.getRed() + color.getGreen() + color.getBlue();
}
public static int ostu(int[][] gray, int w, int h)
{
int[] histData = new int[w * h];
// Calculate histogram
for (int x = 0; x < w; x++)
{
for (int y = 0; y < h; y++)
{
int red = 0xFF & gray[x][y];
histData[red]++;
}
}
// Total number of pixels
int total = w * h;
float sum = 0;
for (int t = 0; t < 256; t++)
sum += t * histData[t];
float sumB = 0;
int wB = 0;
int wF = 0;
float varMax = 0;
int threshold = 0;
for (int t = 0; t < 256; t++)
{
wB += histData[t]; // Weight Background
if (wB == 0)
continue;
wF = total - wB; // Weight Foreground
if (wF == 0)
break;
sumB += (float) (t * histData[t]);
float mB = sumB / wB; // Mean Background
float mF = (sum - sumB) / wF; // Mean Foreground
// Calculate Between Class Variance
float varBetween = (float) wB * (float) wF * (mB - mF) * (mB - mF);
// Check if new maximum found
if (varBetween > varMax)
{
varMax = varBetween;
threshold = t;
}
}
return threshold;
}
粘贴完代码后运行就会在相同的路径下覆盖原来的验证码图片,但是据我测试,这个代码效果不是很理想,所以才是未完成的操作。
图像识别
图片识别我打算利用tess4j。
maven导入项目
<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>4.3.1</version>
</dependency>
导入语言包,这一步操作需要我们下载tess4j的源代码,然后解压之后将tessdata移出来,供下面导入。
接着我们利用tess4j进行图像识别
我发现根据上面图像处理之后,图像放大10倍后tess4j的识别率是最高的,所以有了下面的代码
/**
* 功能描述: //将代码放大10倍
* @Param: [file]
* @Return: java.awt.image.BufferedImage
* @Author: WHOAMI
* @Date: 2019/6/8 22:24
*/
private static BufferedImage change(File file){
// 读取图片字节数组
BufferedImage textImage = null;
try{
InputStream in = new FileInputStream(file);
BufferedImage image = ImageIO.read(in);
in.close();
textImage = ImageHelper.convertImageToGrayscale(ImageHelper.getSubImage(image, 0, 0, image.getWidth(), image.getHeight())); //对图片进行处理
textImage = ImageHelper.getScaledInstance(image, image.getWidth() * 10, image.getHeight() * 10); //将图片扩大5倍
}catch (IOException e) {
e.printStackTrace();
}
return textImage;
}
接着就是我们的核心代码了
public static String deal(String pathname,String fileName){
//打开需要处理的图片
File imageFile = new File(pathname+"\\"+fileName);
try {
//去干扰线
cleanLinesInImage(imageFile,pathname);
} catch (IOException e) {
e.printStackTrace();
}
//开始图像识别
Tesseract instance = new Tesseract();
// 这是我的语言包的路径
instance.setDatapath("D:\\code\\java\\ChromeOperation\\tessdata");
instance.setLanguage("eng");//选择字库文件(只需要文件名,不需要后缀名)
//将验证码图片的内容识别为字符串
try {
// 打开处理后的图像
File image = new File(pathname+"\\"+fileName);
String result = instance.doOCR(change(image));
//利用正则表达式过滤掉非法字符
String correct = result.replaceAll("[^0-9a-zA-Z]J*","");
// 返回正确的字符串
return correct;
} catch (TesseractException e) {
e.printStackTrace();
}
return null;
}
重复验证
由于上面的代码并不理想,所以我打算利用死循环直到他输入正确为止,代码如下:
while(ifAlert(driver)){
driver.findElement(By.xpath("//div[@id='alert_box']/div[2]/div[2]/div[2]")).click();
dealImg(driver);
}