找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2344

积分

0

好友

342

主题
发表于 2025-12-30 03:20:18 | 查看: 22| 回复: 0

很高兴大家喜欢这个项目!代码已上传至Github仓库:leonof/imgRecJs,后续会持续完善。

目前有多种验证码识别思路,我采用了较为基础的机器学习方法。目标验证码样式相对简单,例如数字类型:

数字验证码示例:5594
图1:数字验证码示例

或包含字母的组合:

字母数字混合验证码示例:6ho4
图2:字母数字混合验证码示例

识别速度控制在0.1秒以内,正确率表现非常出色。

在动手编码之前,我们先梳理一下整体实现思路:

  1. 分析网页DOM结构,载入验证码图片。
  2. 将图片绘制到Canvas上,获取像素数据。
  3. 对图片进行二值化、腐蚀膨胀、切割、缩放等预处理。
  4. 记录处理后的单个字符数据,并人工录入对应真实字符。
  5. 重复此过程进行训练。
  6. 识别时,将处理后的图像与库中数据对比,找到最相似的数据,从而得出识别结果。
  7. (优化)数据量大时,可选取前几个相似数据,按权重选出最可能的字符以提高准确率。
  8. (优化)当找到相似度足够高的数据时可提前停止搜索,以提升效率。

在正式编码前,我们模拟一个需要输入验证码的简单网页作为识别目标,其界面如下:

验证码识别模拟网页界面
图3:待识别的模拟网页界面

点击图片可更换验证码,输入框用于输入,按钮模拟提交。其对应的HTML结构如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>验证码</title>
</head>
<body>
    <img onclick="changPic()" id="img" src="img0.jpg">
    <input type="text" id="input">
    <button onclick="sub()">确定</button>
    <script>
        var now = 0;
        function changPic() {
            now++;
            document.getElementById('img').src = 'img' + (now % 5) + '.jpg';
        }
        function sub() {
            alert(document.getElementById('input').value);
        }
    </script>
</body>
</html>

图4:模拟网页的HTML代码结构


一、分析DOM并载入图片

可以看到,验证码图片的URL是 img/0.jpg。我们可以很容易地获取到图片元素或数据:

var img = document.getElementById("img");

二、利用Canvas获取像素数据

首先需要创建Canvas并初始化:

var canvas1 = document.createElement("canvas");
document.getElementsByTagName("body")[0].appendChild(canvas1);
canvas1.style.backgroundColor = "cornsilk";
var ctx1 = canvas1.getContext("2d");

随后,将图片绘制到Canvas上:

ctx1.drawImage(img, 0, 0, img.width, img.height);

接着,就可以获取图片的像素数据:

var imgData = ctx1.getImageData(0, 0, WIDTH, HEIGHT);

三、图像预处理

这部分是识别的核心,直接影响准确率和速度。针对简单的验证码(主要干扰为旋转和缩放),我们直接进行二值化处理。

1. 二值化
思路是计算图片的平均灰度作为阈值,高于阈值的置为纯白(255),低于的置为纯黑(0)。

function toHex(fromImgData) { //二值化图像
    var fromPixelData = fromImgData.data;
    var greyAve = 0;
    for (var j = 0; j < WIDTH * HEIGHT; j++) {
        var r = fromPixelData[4 * j];
        var g = fromPixelData[4 * j + 1];
        var b = fromPixelData[4 * j + 2];
        greyAve += r * 0.3 + g * 0.59 + b * 0.11;
    }
    greyAve /= WIDTH * HEIGHT; //计算平均灰度值。
    for (j = 0; j < WIDTH * HEIGHT; j++) {
        r = fromPixelData[4 * j];
        g = fromPixelData[4 * j + 1];
        b = fromPixelData[4 * j + 2];
        var grey = r * 0.333 + g * 0.333 + b * 0.333; //取平均值。
        grey = grey > greyAve ? 255 : 0;
        fromPixelData[4 * j] = grey;
        fromPixelData[4 * j + 1] = grey;
        fromPixelData[4 * j + 2] = grey;
    }
    return fromImgData;
} //二值化图像

二值化后效果对比如下(左为原图,右为处理后):

图像二值化处理前后对比
图5:图像二值化处理效果对比

处理之后,将图片转换为0/1数组保存:

function toXY(fromImgData) {
    var result = new Array(HEIGHT);
    var fromPixelData = fromImgData.data;
    for (var j = 0; j < HEIGHT; j++) {
        result[j] = new Array(WIDTH);
        for (var k = 0; k < WIDTH; k++) {
            var r = fromPixelData[4 * (j * WIDTH + k)];
            var g = fromPixelData[4 * (j * WIDTH + k) + 1];
            var b = fromPixelData[4 * (j * WIDTH + k) + 2];
            result[j][k] = (r + g + b) > 500 ? 0 : 1; //赋值0、1给内部数组
        }
    }
    return result;
} //图像转数组

2. 腐蚀与膨胀
腐蚀旨在消除游离的黑色噪点(将白色周围的像素变白),膨胀则消除数字内部的白色噪点(将黑色周围的像素变黑),同时使图像更平滑。

function corrode(fromArray) { //腐蚀(简单)
    for (var j = 1; j < fromArray.length - 1; j++) {
        for (var k = 1; k < fromArray[j].length - 1; k++) {
            if (fromArray[j][k] == 1 && fromArray[j - 1][k] + fromArray[j + 1][k] + fromArray[j][k - 1] + fromArray[j][k + 1] == 0) {
                fromArray[j][k] = 0;
            }
        }
    }
    return fromArray;
}

function expand(fromArray) { //膨胀(简单)
    for (var j = 1; j < fromArray.length - 1; j++) {
        for (var k = 1; k < fromArray[j].length - 1; k++) {
            if (fromArray[j][k] == 0 && fromArray[j - 1][k] + fromArray[j + 1][k] + fromArray[j][k - 1] + fromArray[j][k + 1] == 4) {
                fromArray[j][k] = 1;
            }
        }
    }
    return fromArray;
}

3. 字符切割
由于目标验证码字符无粘连,切割相对简单:从上到下、从左到右扫描图片,当发现某一竖列全部为白色时,即执行切割。粘连字符的处理更为复杂,此处不展开。

function split(fromArray, count) {
    var numNow = 0;
    var status = false;
    var w = fromArray[0].length;
    for (var k = 0; k < w; k++) { //遍历图像
        var sumUp = 0;
        for (var j = 0; j < fromArray.length; j++) //检测整列是否有图像
            sumUp += fromArray[j][k];
        if (sumUp == 0) { //切割
            for (j = 0; j < fromArray.length - 1; j++)
                fromArray[j].remove(k);
            w--;
            k--;
            status = false;
            continue;
        } else { //切换状态
            if (!status)
                numNow++;
            status = true;
        }
        if (numNow != count) { //不是想要的数字
            for (j = 0; j < fromArray.length - 1; j++)
                fromArray[j].remove(k);
            w--;
            k--;
        }
    }
    return fromArray;
} //切割,获取特定数字

切割后,左右空白被移除,但上下可能仍有空白,可用类似思路处理。

4. 尺寸归一化(缩放)
旋转并非必要步骤,可通过增加训练数据量来弥补。这里演示缩放操作,利用Canvas将切割后的字符图像缩放到统一尺寸。

function zoomToFit(fromArray) {
    var imgD = fromXY(fromArray);
    var w = lastWidth;
    var h = lastHeight;
    var tempc1 = document.createElement("canvas");
    var tempc2 = document.createElement("canvas");
    tempc1.width = fromArray[0].length;
    tempc1.height = fromArray.length;
    tempc2.width = w;
    tempc2.height = h;
    var tempt1 = tempc1.getContext("2d");
    var tempt2 = tempc2.getContext("2d");
    tempt1.putImageData(imgD, 0, 0, 0, 0, tempc1.width, tempc1.height);
    tempt2.drawImage(tempc1, 0, 0, w, h);
    var returnImageD = tempt2.getImageData(0, 0, WIDTH, HEIGHT);
    fromArray = toXY(returnImageD);
    fromArray.length = h;
    for (var i = 0; i < h; i++)
        fromArray[i].length = w;
    return fromArray;
} //尺寸归一化

归一化处理后,单个数字的效果如下:

归一化处理后的单个数字‘4’
图6:归一化处理后的单个数字‘4’

四、数据记录与训练

图像预处理完成后,我们将得到的数组与对应的真实字符一起保存。可以采用多种方式存储,例如搭建服务器使用数据库。

五、重复训练

在页面中增加手动输入区域,提交一个验证码后刷新继续。大约提交20个验证码(80个字符)后,系统便能时常正确识别4位验证码。当单个字符的训练数据量达到300条左右时(约75个验证码),识别正确率可超过95%;达到500条时,已基本不会出错,此时可实现自我训练。识别一次耗时约0.06秒。

六、识别比对

训练完成后,将数据导出为一个大数组供JavaScript读取。识别时遍历所有数据,逐像素比对。由于尺寸已归一化,直接计算匹配的像素数量即可,匹配数最多的即为识别结果。以下是原始的PHP比对逻辑供参考:

function check($str) {
    $str = str_split($str, 1);
    $length = count($str);
    $tempNum = 0;
    $tempSimmiar = 0;
    $query = "SELECT * FROM numkeys";
    $sth = execSql($query);
    while ($RES = $sth->fetch()) {
        $thisSimmiar = 0;
        $thisFeature = str_split($RES["feature"], 1);
        $thisNum = $RES["resultnum"];
        for ($i = 0; $i < $length; $i++) {
            if ($thisFeature[$i] == $str[$i]) {
                $thisSimmiar++;
            }
        }
        if ($thisSimmiar > $tempSimmiar) {
            $tempSimmiar = $thisSimmiar;
            $tempNum = $thisNum;
        }
    }
    return $tempNum;
}

七、优化方向

主要优化潜力仍在图像预处理阶段,通过减少干扰来提升对复杂验证码的识别能力和效率。对于简单的验证码和千条级别的数据量,每次识别耗时约0.1秒,已能满足基本需求。

附:训练与识别接口

为方便大家验证流程,提供了以下接口:

1. 训练
POST发送 username(用户名)、password(密码)、n1, n2, n3, n4(四个字符的数组)、num(真实四位字符)至训练接口。

function sendData() {
    var str = prompt("请输入验证码:", "");
    if (!str) return false;
    postData = { //整合数据包
        username: 'pdgzfx',
        password: 'pdgzfx',
        nums: str,
        n1: numsArray[0],
        n2: numsArray[1],
        n3: numsArray[2],
        n4: numsArray[3]
    };
    $.ajax({
        url: 'http://www.leonszone.cn/test/yanzhengma/train.php',
        type: 'POST',
        data: postData,
        success: function(data) {
            console.log(data);
            setTimeout(function() {
                location.reload();
            }, 1000);
        }
    });
}

2. 识别
POST发送 username、password、n1, n2, n3, n4 至识别接口。

function getData() {
    postData = { //整合数据包
        username: 'pdgzfx',
        password: 'pdgzfx',
        nums: 'help!!!',
        n1: numsArray[0],
        n2: numsArray[1],
        n3: numsArray[2],
        n4: numsArray[3]
    };
    $.ajax({
        url: 'http://www.leonszone.cn/test/yanzhengma/check.php',
        type: 'POST',
        data: postData,
        success: function(data) {
            $("#Vercode").val(data);
            console.log(data);
        }
    });
}

3. 注册
为防止数据混淆,需要先注册用户名密码。可通过POST或GET请求发送至注册接口。

function registData() {
    var postData = {
        username: 'yourUsername',
        password: 'yourPassword',
    };
    $.ajax({
        url: 'http://www.leonszone.cn/test/yanzhengma/regist.php',
        type: 'POST',
        data: postData,
        success: function(data) {
            console.log(data);
        }
    });
}

或直接浏览器访问:http://www.leonszone.cn/test/yanzhengma/regist.php?username=你的用户名&password=你的密码


以上就是使用纯JavaScript实现简单网页验证码识别的完整思路与关键代码。这个过程涵盖了从前端技术操作DOM和Canvas,到基础的图像处理,再到简单的机器学习训练和比对,希望能为你提供一些启发。如果你想了解更多关于图像处理或机器学习的实战技巧,欢迎到云栈社区与其他开发者交流探讨。




上一篇:实战演示:如何利用特殊编码绕过AWS WAF实现XSS攻击
下一篇:Java接口设计:参数使用Map真的能提升开发效率吗?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 08:35 , Processed in 0.385531 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表