JMeter開發插件——圖片驗證碼識別

咱們在性能測試中總會時不時地遭遇到來自於應用系統的各類阻礙,圖片驗證碼就是一類最多見的束縛,登陸或交易時須要按照圖片中的內容輸入正確的驗證信息後,數據才能夠提交成功,這使得許多性能測試工具只能望而卻步。網上也出現了一些LoadRunner的解決方案,但結合LoadRunner對於C腳本內存控制和識別成功率低下等諸多問題,這些方案沒有什麼實際用途。然而,爲JMeter開發插件卻給咱們提供了一條可行的道路來衝破圖片驗證碼的束縛!java

選擇一個理想的第三方圖形圖像識別工具
在此咱們首先須要一個比較理想的圖形圖像識別工具來完成將驗證碼中的圖形圖像文字識別轉換爲文本文字主體識別工做,在此咱們選擇Tesseract, Tesseract是一個開源的OCR(Optical Character Recognition,光學字符識別)引擎,能夠識別多種格式的圖像文件並將其轉換成文本,發佈在Googel Project上,地址爲http://code.google.com/p/tesseract-ocr/(但Googel Project中止維護後不知道如今在哪裏維護)。ios

一組用於驗證碼識別的JMeter插件
咱們常見的驗證碼圖片樣本以下:
正則表達式

1. 降噪

當你遇到這樣的驗證碼時,首先你要作的就是降噪,將背景的一些干擾咱們識別文本內容的線條過濾掉,人眼須要降噪,識別軟件在進行識別前也須要幫助其進行降噪來加大識別成功率,一般降噪的方案是對圖片像素點進行逐個掃描,經過建立降噪規則對背景噪音進行過濾,如上面的樣本,咱們能夠創建以下降噪規則和方法:apache

public static int isFilter(int colorInt) { Color color = new Color(colorInt); if ((color.getRed() > 85 && color.getRed() < 255) && (color.getGreen() > 85 && color.getGreen() < 255) && (color.getBlue() > 85 && color.getBlue() < 255)) { return 1; } return 0; } public static BufferedImage removeBackgroud(BufferedImage img) throws Exception { int width = img.getWidth(); int height = img.getHeight(); for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y) { if (isFilter(img.getRGB(x, y)) == 1) { img.setRGB(x, y, Color.WHITE.getRGB()); } } } return img; } 

能夠看到效果很是明顯,但降噪也有它的侷限性,好比會把一些須要正常顯示的圖形文字過濾掉一部分,諸如此類問題咱們會在後面的介紹中經過其餘方式解決,但對於圖形圖像識別軟件的輸入來講,必須對其加以降噪才能保證讀取正確率。app

2. 識別插件(第一個Extractor插件)

咱們在最初的章節介紹了Extractor的基本實現方法,在此咱們仍是簡單回顧一下後置處理器的一些功能,下圖顯示了JMeter爲咱們默認提供的後置處理器:框架

 

所謂後置處理器是相對Sampler的後置,主要用於處理Sampler所抽樣獲得的SamplerResult對象,對SamplerResult作修飾或經過SamplerResult抽取信息,最常使用的是「正則表達式提取器」、「CSS/JQuery Extractor」、「XPath Extractor」,使用它們能夠實現性能測試腳本中最重要的「關聯」操做。ide

好了,咱們的需求是對驗證碼進行讀取,即經過驗證碼URL獲取到圖片資源(這部分由「HTTP請求Sampler」完成),而後提取資源中的圖形圖像信息做爲Tesseract的輸入,最後在將Tesseract的輸出做爲一個JMeter參數數據進行保存。慣例使用分離法,分爲邏輯控制部分VcodeExtractor和GUI部分VcodeExtractorGUI,另外,還包括對圖片進行處理的ImageIOHelper類以及實現調用Tesseract對驗證碼信息識別並讀取的OCR類函數

ImageIOHelper主要包含兩大部分,一部分就是前面所介紹的降噪邏輯,另外一部分是將圖片格式轉換爲tiff格式以更好地進行識別,這部分的代碼參考以下:工具

public static File createImage(File imageFile, String imageFormat) { File tempFile = null; ImageInputStream iis = null; ImageOutputStream ios = null; ImageReader reader = null; ImageWriter writer = null; try { Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName(imageFormat); reader = readers.next(); iis = ImageIO.createImageInputStream(imageFile); reader.setInput(iis); IIOMetadata streamMetadata = reader.getStreamMetadata(); TIFFImageWriteParam tiffWriteParam = new TIFFImageWriteParam(Locale.CHINESE); tiffWriteParam.setCompressionMode(ImageWriteParam.MODE_DISABLED); Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("tiff"); writer = writers.next(); BufferedImage bi = removeBackgroud(reader.read(0)); IIOImage image = new IIOImage(bi,null,reader.getImageMetadata(0)); tempFile = tempImageFile(imageFile); ios = ImageIO.createImageOutputStream(tempFile); writer.setOutput(ios); writer.write(streamMetadata, image, tiffWriteParam); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { // TODO Auto-generated catch block
 e.printStackTrace(); } finally { if(iis != null){ try { iis.close(); } catch (IOException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } } if(ios != null){ try { ios.close(); } catch (IOException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } } if(writer != null){ writer.dispose(); } if(reader != null){ reader.dispose(); } } return tempFile; } private static File tempImageFile(File imageFile) { String path = imageFile.getPath(); StringBuffer strB = new StringBuffer(path); return new File(strB.toString().replaceFirst("jpg", "tif")); }

OCR類主要是經過Process調用已經安裝的Tesseract程序,調用命令基本形式爲 tesseract xxx.tif 1 -l eng,參考以下代碼:oop

import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; public class OCR { private final String LANG_OPTION = "-l"; private final String EOL = System.getProperty("line.separator"); private String tessPath = "D://Program Files (x86)//Tesseract-OCR"; public String recognizeText(File imageFile,String imageFormat) { File tempImage = ImageIOHelper.createImage(imageFile,imageFormat); File outputFile = new File(imageFile.getParentFile(),"output" + imageFile.getName()); StringBuffer sb = new StringBuffer(); List<String> cmd = new ArrayList<String>(); cmd.add(tessPath+"//tesseract"); cmd.add(""); cmd.add(outputFile.getName()); cmd.add(LANG_OPTION); cmd.add("eng"); ProcessBuilder pb = new ProcessBuilder(); pb.directory(imageFile.getParentFile()); cmd.set(1, tempImage.getName()); pb.command(cmd); pb.redirectErrorStream(true); Process process = null; BufferedReader in = null; int wait; try { process = pb.start(); //tesseract.exe xxx.tif 1 -l eng
            wait = process.waitFor(); if(wait == 0){ in = new BufferedReader(new InputStreamReader(new FileInputStream(outputFile.getAbsolutePath()+".txt"),"UTF-8")); String str; while((str = in.readLine())!=null){ sb.append(str).append(EOL); } in.close(); }else{ tempImage.delete(); } new File(outputFile.getAbsolutePath()+".txt").delete(); } catch (IOException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } catch (InterruptedException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } finally { if(in != null){ try { in.close(); } catch (IOException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } } } tempImage.delete(); return sb.toString(); } }

VcodeExtractor類繼承AbstractScopedTestElement抽象類,實現PostProcessor接口的process方法,來處理利用OCR讀取驗證碼信息的邏輯控制,參考代碼以下:

import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.Serializable; import org.apache.jmeter.processor.PostProcessor; import org.apache.jmeter.samplers.SampleResult; import org.apache.jmeter.testelement.AbstractScopedTestElement; import org.apache.jmeter.threads.JMeterContext; import org.apache.jmeter.threads.JMeterVariables; import org.apache.jorphan.logging.LoggingManager; import org.apache.log.Logger; public class VcodeExtractor extends AbstractScopedTestElement implements PostProcessor, Serializable{ private static final Logger log = LoggingManager.getLoggerForClass(); @Override public void process() { // TODO Auto-generated method stub
        JMeterContext context = getThreadContext(); SampleResult previousResult = context.getPreviousResult(); if (previousResult == null) { return; } log.debug("VcodeExtractor processing result"); String status = previousResult.getResponseCode(); int id = context.getThreadNum(); String imageName = id + ".jpg"; if(status.equals("200")){ byte[] buffer = previousResult.getResponseData(); FileOutputStream out = null; File file = null; try { file = new File(imageName); out = new FileOutputStream(file); out.write(buffer); out.flush(); } catch (IOException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } finally { if(out != null){ try { out.close(); } catch (IOException e) { // TODO Auto-generated catch block
 e.printStackTrace(); } } } try { String vcode = new OCR().recognizeText(file, "jpg"); vcode = vcode.replace(" ", "").trim(); JMeterVariables var = context.getVariables(); var.put("vcode", vcode); var.put("vuser", String.valueOf(id)); } catch (Exception e) { e.printStackTrace(); } } } }

代碼邏輯很是簡潔,即經過getThreadContext()方法獲取當前線程(vuser)的上下文,從而從上下文中獲取到前一個Sampler所抽樣的結果,爲保證結果不爲空咱們作了一個簡單的處理,也能夠添加一些更爲精細的控制,以下代碼:

if(context.getPreviousSampler() instanceof HTTPSampler){ return; }

判斷前一個Sampler是否爲HTTPSampler,以限定有效使用範圍。
將previousResult.getResponseData()保存爲文件後,經過前面咱們建立的OCR完成識別任務後,將識別結果經過JMeterVariables對象保存下來,在此咱們分別創建了兩個參數」vcode」和」vuser」,後面咱們能夠用它們進行測試。

該版本的VcodeExtractorGUI類只是單純實現一個可視化的界面用於在測試計劃Tree中進行操做:

import org.apache.jmeter.processor.gui.AbstractPostProcessorGui; import org.apache.jmeter.testelement.TestElement; public class VcodeExtractorGUI extends AbstractPostProcessorGui{ @Override public TestElement createTestElement() { // TODO Auto-generated method stub
        VcodeExtractor extractor = new VcodeExtractor(); modifyTestElement(extractor); return extractor; } @Override public String getLabelResource() { // TODO Auto-generated method stub
        return this.getClass().getName(); } @Override public String getStaticLabel() {//設置顯示名稱 // TODO Auto-generated method stub
        return "VcodeExtractor"; } @Override public void modifyTestElement(TestElement extractor) { // TODO Auto-generated method stub
        super.configureTestElement(extractor); } }

這意味着識別存在錯誤!

3. 提升識別成功率

識別成功率是成敗的關鍵,提高成功率能夠採起如下方案:

訓練Tesseract

提供大量樣原本訓練Tesseract對特定圖形的識別成功率。

修正錯誤的識別結果

有些識別錯誤是這樣的,如:
將J識別爲[,將M識別爲|\/|,將N識別爲||,這種識別錯誤是機器識別離散一些的像素點產生的,人眼是能夠修正的,所以,咱們能夠創建映射表方式將錯誤字符進行修正。

避免混淆形狀接近的圖形字符

有些識別錯誤是這樣的,如:
將5識別爲S,將1識別爲I,將0識別爲O,這種識別錯誤是純的圖形混淆產生的,人眼也可能犯此類錯誤,咱們管它叫「看不清」。

4. 看不清,換一張(第一個Controller插件)

「看不清,換一張」不管對人眼或機器識別都是一種彌補方案,咱們對於「看不清」的字符須要模擬換一張從新識別的操做,這裏咱們引入一個新的插件Controller(邏輯控制器),照例咱們先來回顧一下該插件的一些功能,下圖顯示了JMeter爲咱們默認提供的邏輯控制器:

前面的章節曾經介紹過所謂邏輯控制器主要就是用來控制線程行爲的,固然也包括一些用於劃分Sampler或功能邊界的控制器如事務控制器和錄製控制器,主要是依靠一些限定的條件或閾值的判斷,按想要的方式控制整體線程或單獨線程行爲。

好了,咱們的需求很明確「看不清,換一張」,在此能夠徹底照搬循環控制器的源代碼,參考LoopController類和LoopControlPanel類,只須要對LoopController在每次循環結束後判斷是否退出的函數中增長咱們對於圖片是否看清的邏輯,代碼以下:

private final static String PATTERN = "34789ABCEFHKLPRTUVWXY"
    private boolean isVerify(String vcode){ int length = vcode.length(); //對長度進行判斷
        if(length != 4){ return false; } //對內容進行判斷
        for(int i = 0; i < length; i++){ if(PATTERN.indexOf(vcode.toCharArray()[i]) < 0){ return false; } } return true; } @Override public Sampler next() { JMeterContext context = getThreadContext(); JMeterVariables var = context.getVariables(); String vcode = var.get("vcode"); if(vcode != null){ if(isVerify(vcode)){ setDone(true); return null; } } if(endOfLoop()) { if (!getContinueForever()) { setDone(true); } return null; } return super.next(); }

若是經過isVerify函數校驗(看得清楚)就直接退出循環,不然(看不清楚)就接着從新請求圖片驗證碼進行校驗(換一張),建立此邏輯控制器VcodeVerifyController。

將插件打包插入JMeter框架,能夠在邏輯控制器列表中查看到VcodeVerifyController組件:

 

 

所有經過了登陸驗證,但根據測試發現識別率成功基本在75%左右,所以,還須要進一步完善,第一是經過改進識別邏輯,第二是增長一個驗證碼若是識別錯誤從新進行識別提交登陸事務的過程控制。

 

 原文地址https://blog.csdn.net/xreztento/article/details/48682923

大量jmeter二次開發文章地址https://blog.csdn.net/xreztento/article/category/2551407

相關文章
相關標籤/搜索