
Jmetertess4j识别图形验证码因工作远程Windows机器的运行内存限制OCRserver存在无法启动的情况因此寻找其他方式替代。找到了tess4j及其相关依赖可以实现。初始代码由豆包完成后续使用deepseek、千问、元宝优化最终完成。一、使用工具JMeter压测平台通过 JSR223 Sampler 执行脚本GroovyJMeter 内嵌脚本语言负责图像预处理逻辑Tess4JTesseract OCR 的 Java 封装基于 JNA供 Groovy 直接调用二、JAR 包放置位置将 Tess4J 及其所有依赖 JAR 放入 JMeter 的lib/目录JMETER_HOME/└── lib/├── tess4j-5.9.0.jar├── lept4j-1.18.0.jar├── jna-platform-5.14.0.jar├── jna-5.14.0.jar└── jai-imageio-core-1.4.0.jar三、语言包放置位置将 Tesseract 英文语言数据文件放入lib/tessdata/目录JMETER_HOME/└── lib/└── tessdata/└── eng.traineddata脚本中setDatapath(../lib/tessdata)使用相对路径需确保从 JMeter 的bin/目录启动默认启动方式否则需按实际启动位置调整路径。四、运行方式JMeter 中新建http request用来获取图形验证码再添加后置处理器JSR223 后置处理器语言选择Groovy将脚本粘贴进去importjava.io.FileOutputStreamimportjava.text.SimpleDateFormatimportnet.sourceforge.tess4j.Tesseractimportjava.io.Fileimportjavax.imageio.ImageIOimportjava.awt.image.BufferedImageimportjava.awt.Colorimportjava.awt.Graphics2Dimportjava.awt.RenderingHintsbyte[]imageDataprev.getResponseData()def timestampnewSimpleDateFormat(yyyyMMddHHmmssSSS).format(newDate())def random(int)(Math.random()*1000)def savePath./img/newFile(savePath).mkdirs()def originalFilenewFile(savePathcaptcha_orig_${timestamp}_${random}.png)def processedFilenewFile(savePathcaptcha_proc_${timestamp}_${random}.png)def debugBinFilenewFile(savePathdebug_bin_${timestamp}_${random}.png)try{FileOutputStreamfosnewFileOutputStream(originalFile)fos.write(imageData)fos.close()BufferedImageimgImageIO.read(originalFile)if(imgnull){vars.put(vCode,);return}intwidthimg.getWidth()intheightimg.getHeight()// 1. 动态阈值二值化 longsumBrightness0for(inty0;yheight;y){for(intx0;xwidth;x){intrgbimg.getRGB(x,y)intr(rgb16)0xFFintg(rgb8)0xFFintbrgb0xFFsumBrightness(rgb)/3}}intavgBright(int)(sumBrightness/(width*height))intdynamicThresholdMath.max(180,avgBright-20)log.info(☀️ 动态阈值: ${dynamicThreshold} (平均亮度: ${avgBright}))for(inty0;yheight;y){for(intx0;xwidth;x){intrgbimg.getRGB(x,y)intr(rgb16)0xFFintg(rgb8)0xFFintbrgb0xFFintmax_valMath.max(r,Math.max(g,b))intmin_valMath.min(r,Math.min(g,b))if(max_valdynamicThreshold||(max_val-min_val)30){img.setRGB(x,y,Color.BLACK.getRGB())}else{img.setRGB(x,y,Color.WHITE.getRGB())}}}ImageIO.write(img,png,debugBinFile)// 2. 原图 3x3 开运算 BufferedImageerodednewBufferedImage(width,height,BufferedImage.TYPE_INT_RGB)for(inty0;yheight;y){for(intx0;xwidth;x){if(x0||y0||xwidth-1||yheight-1){eroded.setRGB(x,y,img.getRGB(x,y));continue}if(img.getRGB(x,y)Color.BLACK.getRGB()){booleanhasWhitefalsefor(intdy-1;dy1;dy){for(intdx-1;dx1;dx){if(img.getRGB(xdx,ydy)Color.WHITE.getRGB()){hasWhitetrue;break}}if(hasWhite)break}eroded.setRGB(x,y,hasWhite?Color.WHITE.getRGB():Color.BLACK.getRGB())}else{eroded.setRGB(x,y,Color.WHITE.getRGB())}}}// 膨胀恢复BufferedImagedilatednewBufferedImage(width,height,BufferedImage.TYPE_INT_RGB)for(inty0;yheight;y){for(intx0;xwidth;x){if(x0||y0||xwidth-1||yheight-1){dilated.setRGB(x,y,eroded.getRGB(x,y));continue}booleanhasBlackfalsefor(intdy-1;dy1;dy){for(intdx-1;dx1;dx){if(eroded.getRGB(xdx,ydy)Color.BLACK.getRGB()){hasBlacktrue;break}}if(hasBlack)break}dilated.setRGB(x,y,hasBlack?Color.BLACK.getRGB():Color.WHITE.getRGB())}}imgdilated// 3. 放大 4 倍 intscale4intnewWwidth*scaleintnewHheight*scaleBufferedImagescaledImgnewBufferedImage(newW,newH,BufferedImage.TYPE_INT_RGB)Graphics2Dg2dscaledImg.createGraphics()g2d.setColor(Color.WHITE)g2d.fillRect(0,0,newW,newH)g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR)g2d.drawImage(img,0,0,newW,newH,null)g2d.dispose()imgscaledImg widthnewW heightnewH// 4. 连通域过滤 boolean[][]visitednewboolean[width][height]intminValidXwidth,maxValidX0,minValidYheight,maxValidY0for(inty0;yheight;y){for(intx0;xwidth;x){if(img.getRGB(x,y)Color.BLACK.getRGB()!visited[x][y]){def queue[[x,y]]def comp[]visited[x][y]trueintcMinXx,cMaxXx,cMinYy,cMaxYy,count0while(queue){def(cx,cy)queue.remove(0)comp.add([cx,cy])countif(cxcMinX)cMinXcxif(cxcMaxX)cMaxXcxif(cycMinY)cMinYcyif(cycMaxY)cMaxYcyfor(int[]d:[[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[-1,1],[1,-1],[1,1]]){intnxcxd[0],nycyd[1]if(nx0nxwidthny0nyheight!visited[nx][ny]img.getRGB(nx,ny)Color.BLACK.getRGB()){visited[nx][ny]truequeue.add([nx,ny])}}}intcompWcMaxX-cMinX1intcompHcMaxY-cMinY1doubledensity(double)count/(compW*compH)booleanisNoise(count100)||(compH30)||(density0.15compW50)if(isNoise){for(def p in comp)img.setRGB(p[0],p[1],Color.WHITE.getRGB())}else{if(cMinXminValidX)minValidXcMinXif(cMaxXmaxValidX)maxValidXcMaxXif(cMinYminValidY)minValidYcMinYif(cMaxYmaxValidY)maxValidYcMaxY}}}}// 5. 裁剪与补白 if(maxValidXminValidXmaxValidYminValidY){intcropXMath.max(0,minValidX-10)intcropYMath.max(0,minValidY-10)intcropWMath.min(width-cropX,(maxValidX-minValidX)20)intcropHMath.min(height-cropY,(maxValidY-minValidY)20)BufferedImagecroppedimg.getSubimage(cropX,cropY,cropW,cropH)intpadding40BufferedImagepaddedImgnewBufferedImage(cropWpadding*2,cropHpadding*2,BufferedImage.TYPE_INT_RGB)Graphics2DgPadpaddedImg.createGraphics()gPad.setColor(Color.WHITE)gPad.fillRect(0,0,paddedImg.getWidth(),paddedImg.getHeight())gPad.drawImage(cropped,padding,padding,null)gPad.dispose()imgpaddedImg}ImageIO.write(img,png,processedFile)// 6. Tesseract 识别 TesseracttesseractnewTesseract()tesseract.setDatapath(../lib/tessdata)tesseract.setLanguage(eng)tesseract.setOcrEngineMode(1)tesseract.setTessVariable(tessedit_char_whitelist,0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ)tesseract.setTessVariable(user_defined_dpi,300)tesseract.setTessVariable(preserve_interword_spaces,0)tesseract.setTessVariable(load_system_dawg,0)tesseract.setTessVariable(load_freq_dawg,0)tesseract.setTessVariable(tessedit_enable_doc_dict,0)// 第一道防线PSM 7 (单行)tesseract.setTessVariable(tessedit_pageseg_mode,7)StringresultRawtesseract.doOCR(processedFile)def resultresultRaw.replaceAll([^0-9A-Za-z],)// 第二道防线如果 PSM 7 没凑够 4 个降级为 PSM 8 (单词模式)if(result.length()4){log.info(⚠️ PSM 7 识别残缺(${result})触发 PSM 8 重试...)tesseract.setTessVariable(tessedit_pageseg_mode,8)resultRawtesseract.doOCR(processedFile)def resultRetryresultRaw.replaceAll([^0-9A-Za-z],)if(resultRetry.length()result.length())resultresultRetry}// 7. 结果校验与兜底提取 if(result.length()!4){log.warn(⚠️ 识别结果非4位: ${result} (原始输出: ${resultRaw.replaceAll(\\s, )})尝试从原始输出重新提取...)// Tesseract 有时会在中间插入空格或换行重新从原始输出提取def cleanedresultRaw.replaceAll([^0-9A-Za-z],)if(cleaned.length()4){resultcleaned.substring(0,4)log.info( 重新提取结果: ${result})}}if(result.length()4)resultresult.substring(0,4)// 8. 可选特定字符后处理 // 如果跑批中发现固定混淆对取消下面注释并按需修改// 例验证码不含数字0把O映射为0// result result.replace(O, 0)// 例验证码不含数字1把I/l映射为1// result result.replace(I, 1).replace(l, 1)vars.put(vCode,result)log.info(✅最终验证码 vCode${result})}catch(Exceptione){log.error(❌ 处理异常${e.getMessage()},e)vars.put(vCode,)}识别结果自动写入变量vCode后续请求通过${vCode}引用五、调试输出脚本会在 JMeter 启动目录下自动创建img/文件夹保存以下文件供对比排查文件说明captcha_orig_*原始验证码图片debug_bin_*二值化后的调试图captcha_proc_*最终送入 OCR 的处理图