很高兴大家喜欢这个项目!代码已上传至Github仓库:leonof/imgRecJs,后续会持续完善。
目前有多种验证码识别思路,我采用了较为基础的机器学习方法。目标验证码样式相对简单,例如数字类型:

图1:数字验证码示例
或包含字母的组合:

图2:字母数字混合验证码示例
识别速度控制在0.1秒以内,正确率表现非常出色。
在动手编码之前,我们先梳理一下整体实现思路:
- 分析网页DOM结构,载入验证码图片。
- 将图片绘制到Canvas上,获取像素数据。
- 对图片进行二值化、腐蚀膨胀、切割、缩放等预处理。
- 记录处理后的单个字符数据,并人工录入对应真实字符。
- 重复此过程进行训练。
- 识别时,将处理后的图像与库中数据对比,找到最相似的数据,从而得出识别结果。
- (优化)数据量大时,可选取前几个相似数据,按权重选出最可能的字符以提高准确率。
- (优化)当找到相似度足够高的数据时可提前停止搜索,以提升效率。
在正式编码前,我们模拟一个需要输入验证码的简单网页作为识别目标,其界面如下:

图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;
} //尺寸归一化
归一化处理后,单个数字的效果如下:

图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,到基础的图像处理,再到简单的机器学习训练和比对,希望能为你提供一些启发。如果你想了解更多关于图像处理或机器学习的实战技巧,欢迎到云栈社区与其他开发者交流探讨。