Jmeter+tess4j识别图形验证码

发布时间:2026/10/31 10:59:29
Jmeter+tess4j识别图形验证码

Jmeter+tess4j识别图形验证码

因工作远程Windows机器的运行内存限制,OCRserver存在无法启动的情况,因此寻找其他方式替代。找到了tess4j及其相关依赖可以实现。

初始代码由豆包完成,后续使用deepseek、千问、元宝优化,最终完成。

一、使用工具

  • JMeter:压测平台,通过 JSR223 Sampler 执行脚本
  • Groovy:JMeter 内嵌脚本语言,负责图像预处理逻辑
  • Tess4J:Tesseract 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/目录启动(默认启动方式),否则需按实际启动位置调整路径。

四、运行方式

  1. JMeter 中新建http request用来获取图形验证码,再添加后置处理器JSR223 后置处理器
  2. 语言选择Groovy
  3. 将脚本粘贴进去
importjava.io.FileOutputStreamimportjava.text.SimpleDateFormatimportnet.sourceforge.tess4j.Tesseractimportjava.io.Fileimportjavax.imageio.ImageIOimportjava.awt.image.BufferedImageimportjava.awt.Colorimportjava.awt.Graphics2Dimportjava.awt.RenderingHintsbyte[]imageData=prev.getResponseData()def timestamp=newSimpleDateFormat("yyyyMMddHHmmssSSS").format(newDate())def random=(int)(Math.random()*1000)def savePath="./img/"newFile(savePath).mkdirs()def originalFile=newFile(savePath+"captcha_orig_${timestamp}_${random}.png")def processedFile=newFile(savePath+"captcha_proc_${timestamp}_${random}.png")def debugBinFile=newFile(savePath+"debug_bin_${timestamp}_${random}.png")try{FileOutputStreamfos=newFileOutputStream(originalFile)fos.write(imageData)fos.close()BufferedImageimg=ImageIO.read(originalFile)if(img==null){vars.put("vCode","");return}intwidth=img.getWidth()intheight=img.getHeight()// ========== 1. 动态阈值二值化 ==========longsumBrightness=0for(inty=0;y<height;y++){for(intx=0;x<width;x++){intrgb=img.getRGB(x,y)intr=(rgb>>16)&0xFFintg=(rgb>>8)&0xFFintb=rgb&0xFFsumBrightness+=(r+g+b)/3}}intavgBright=(int)(sumBrightness/(width*height))intdynamicThreshold=Math.max(180,avgBright-20)log.info("☀️ 动态阈值: ${dynamicThreshold} (平均亮度: ${avgBright})")for(inty=0;y<height;y++){for(intx=0;x<width;x++){intrgb=img.getRGB(x,y)intr=(rgb>>16)&0xFFintg=(rgb>>8)&0xFFintb=rgb&0xFFintmax_val=Math.max(r,Math.max(g,b))intmin_val=Math.min(r,Math.min(g,b))if(max_val<dynamicThreshold||(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 开运算 ==========BufferedImageeroded=newBufferedImage(width,height,BufferedImage.TYPE_INT_RGB)for(inty=0;y<height;y++){for(intx=0;x<width;x++){if(x==0||y==0||x==width-1||y==height-1){eroded.setRGB(x,y,img.getRGB(x,y));continue}if(img.getRGB(x,y)==Color.BLACK.getRGB()){booleanhasWhite=falsefor(intdy=-1;dy<=1;dy++){for(intdx=-1;dx<=1;dx++){if(img.getRGB(x+dx,y+dy)==Color.WHITE.getRGB()){hasWhite=true;break}}if(hasWhite)break}eroded.setRGB(x,y,hasWhite?Color.WHITE.getRGB():Color.BLACK.getRGB())}else{eroded.setRGB(x,y,Color.WHITE.getRGB())}}}// 膨胀恢复BufferedImagedilated=newBufferedImage(width,height,BufferedImage.TYPE_INT_RGB)for(inty=0;y<height;y++){for(intx=0;x<width;x++){if(x==0||y==0||x==width-1||y==height-1){dilated.setRGB(x,y,eroded.getRGB(x,y));continue}booleanhasBlack=falsefor(intdy=-1;dy<=1;dy++){for(intdx=-1;dx<=1;dx++){if(eroded.getRGB(x+dx,y+dy)==Color.BLACK.getRGB()){hasBlack=true;break}}if(hasBlack)break}dilated.setRGB(x,y,hasBlack?Color.BLACK.getRGB():Color.WHITE.getRGB())}}img=dilated// ========== 3. 放大 4 倍 ==========intscale=4intnewW=width*scaleintnewH=height*scaleBufferedImagescaledImg=newBufferedImage(newW,newH,BufferedImage.TYPE_INT_RGB)Graphics2Dg2d=scaledImg.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()img=scaledImg width=newW height=newH// ========== 4. 连通域过滤 ==========boolean[][]visited=newboolean[width][height]intminValidX=width,maxValidX=0,minValidY=height,maxValidY=0for(inty=0;y<height;y++){for(intx=0;x<width;x++){if(img.getRGB(x,y)==Color.BLACK.getRGB()&&!visited[x][y]){def queue=[[x,y]]def comp=[]visited[x][y]=trueintcMinX=x,cMaxX=x,cMinY=y,cMaxY=y,count=0while(queue){def(cx,cy)=queue.remove(0)comp.add([cx,cy])count++if(cx<cMinX)cMinX=cxif(cx>cMaxX)cMaxX=cxif(cy<cMinY)cMinY=cyif(cy>cMaxY)cMaxY=cyfor(int[]d:[[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[-1,1],[1,-1],[1,1]]){intnx=cx+d[0],ny=cy+d[1]if(nx>=0&&nx<width&&ny>=0&&ny<height&&!visited[nx][ny]&&img.getRGB(nx,ny)==Color.BLACK.getRGB()){visited[nx][ny]=truequeue.add([nx,ny])}}}intcompW=cMaxX-cMinX+1intcompH=cMaxY-cMinY+1doubledensity=(double)count/(compW*compH)booleanisNoise=(count<100)||(compH<30)||(density<0.15&&compW>50)if(isNoise){for(def p in comp)img.setRGB(p[0],p[1],Color.WHITE.getRGB())}else{if(cMinX<minValidX)minValidX=cMinXif(cMaxX>maxValidX)maxValidX=cMaxXif(cMinY<minValidY)minValidY=cMinYif(cMaxY>maxValidY)maxValidY=cMaxY}}}}// ========== 5. 裁剪与补白 ==========if(maxValidX>minValidX&&maxValidY>minValidY){intcropX=Math.max(0,minValidX-10)intcropY=Math.max(0,minValidY-10)intcropW=Math.min(width-cropX,(maxValidX-minValidX)+20)intcropH=Math.min(height-cropY,(maxValidY-minValidY)+20)BufferedImagecropped=img.getSubimage(cropX,cropY,cropW,cropH)intpadding=40BufferedImagepaddedImg=newBufferedImage(cropW+padding*2,cropH+padding*2,BufferedImage.TYPE_INT_RGB)Graphics2DgPad=paddedImg.createGraphics()gPad.setColor(Color.WHITE)gPad.fillRect(0,0,paddedImg.getWidth(),paddedImg.getHeight())gPad.drawImage(cropped,padding,padding,null)gPad.dispose()img=paddedImg}ImageIO.write(img,"png",processedFile)// ========== 6. Tesseract 识别 ==========Tesseracttesseract=newTesseract()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")StringresultRaw=tesseract.doOCR(processedFile)def result=resultRaw.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")resultRaw=tesseract.doOCR(processedFile)def resultRetry=resultRaw.replaceAll("[^0-9A-Za-z]","")if(resultRetry.length()>result.length())result=resultRetry}// ========== 7. 结果校验与兜底提取 ==========if(result.length()!=4){log.warn("⚠️ 识别结果非4位: '${result}' (原始输出: '${resultRaw.replaceAll("\\s+", "")}'),尝试从原始输出重新提取...")// Tesseract 有时会在中间插入空格或换行,重新从原始输出提取def cleaned=resultRaw.replaceAll("[^0-9A-Za-z]","")if(cleaned.length()>=4){result=cleaned.substring(0,4)log.info("🔧 重新提取结果: ${result}")}}if(result.length()>4)result=result.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","")}
  1. 识别结果自动写入变量vCode,后续请求通过${vCode}引用

五、调试输出

脚本会在 JMeter 启动目录下自动创建img/文件夹,保存以下文件供对比排查:

文件说明
captcha_orig_*原始验证码图片
debug_bin_*二值化后的调试图
captcha_proc_*最终送入 OCR 的处理图