购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.4 CAPTCHA验证码识别

在了解了各种类型验证码的生成流程后,我们开始探讨如何对验证码进行识别操作。本节将分别介绍文本验证码、滑块验证码和点选验证码的识别方法。

3.4.1 文本验证码识别方案1

文本验证码有着“悠久”的使用历史,针对这种类型验证码的研究也相当丰富。虽然不同研究者或机构在识别模型的实现方法不尽相同,但总体上可分为两大类。其中一种方法包括预处理、文本字符切割、单字符识别等步骤来处理,它的基本处理步骤如图3-10所示。

图3-10 文本验证码识别步骤

接下来,我们将逐一介绍各个处理步骤中的主要技术。首先,我们来看图像预处理技术。图像预处理技术有很多种,下面简单列举一些常用的技术和相关示例代码。在这些示例代码中,我们使用图像处理库OpenCV进行处理。

1.灰度化处理

灰度化处理是将彩色图像转换为灰度图像的过程,通常是图片预处理的第一步。在灰度图像中,每个像素仅表示亮度信息,而不包含颜色信息。彩色图像通常由红色(R)、绿色(G)、蓝色(B)三个颜色通道组成。灰度化处理就是将这三个通道的信息综合,转换成单一的灰度值。常用的灰度化方法说明如下:

(1)加权平均法(Luminosity Method):这是最常用的一种灰度化方法,根据人眼对不同颜色的敏感度给予不同的权重,公式为:Gray = 0.299*R + 0.587*G + 0.114*B。

(2)平均法(Average Method):简单地取三个颜色通道的平均值作为灰度值,公式为:Gray= (R + G + B) / 3。

(3)最大值法(Max Method):取三个颜色通道中的最大值作为灰度值,公式为:Gray = max(R,G, B)。

借助OpenCV库,我们可以轻松实现验证码图片的灰度化处理,相关示例代码如下:

     Mat image = Imgcodecs.imread("文本验证码.jpeg");
     Mat grayImage = new Mat();
     Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);
     Imgcodecs.imwrite("gray.jpeg", grayImage);
2.二值化处理

图像二值化处理是图像预处理中的一种基本技术,它将图像中的像素值转换为0或255,从而将彩色图像转换为仅包含黑白两种颜色的图像。这种处理方法适用于去除图像的背景噪声、分离前景和背景,以及在图像分析(如文字识别、边缘检测)前进行的预处理步骤。

二值化处理的基本原理是选取一个阈值(Threshold),然后将图像中的每个像素与这个阈值进行比较。如果像素值大于阈值,该像素点就被设置为一个值(通常是255,表示白色);如果像素值小于或等于阈值,则被设置为另一个值(通常是0,表示黑色)。通过这种方式,原始图像被转换成一个二值图像。

在二值化处理过程中,选择合适的阈值至关重要。下面介绍一些常用的阈值选择方法和技巧:

(1)全局阈值法:选择一个全局阈值对整个图像进行二值化。该全局阈值可以通过反复试验获得,也可以通过目标像素与背景像素的比较差异来获得。在比较像素差异时可以考虑使用大律法。

(2)自适应阈值法:对于每个像素,阈值是根据像素周围小区域的值动态计算的。

以下是使用OpenCV库实现图像灰度化和二值化处理的示例代码:

     Mat image = Imgcodecs.imread("文本验证码.jpeg");
     // 灰度化处理
     Mat grayImage = new Mat();
     Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);
     // 二值化处理
     Mat binary = new Mat();
     Imgproc.threshold(grayImage, binary, 0, 255, Imgproc.THRESH_BINARY_INV +
Imgproc.THRESH_OTSU);
     Imgcodecs.imwrite("binary.jpeg", binary);

针对图片的预处理技术还有很多,例如使用高斯模糊图像处理技术可以使图像变得更加平滑,从而减少图像中的噪声。感兴趣的读者可以自行深入调研,这里不再一一列举。

3.字符切割算法

介绍完图像预处理技术后,接下来介绍如何对文本验证码中的字符进行切割。因为现有的图像识别技术在识别单个字符时具有较高的准确率,所以很多解决方案倾向于将整体文本切割成单个字符后再进行识别。基于这一点,字符切割算法的准确性变得尤为重要。

一种常见的切割算法是垂直投影技术,其主要思想是对二值化后的图像在垂直方向上进行投影,统计每一列的像素值之和。字符区域的列像素值之和通常比非字符区域(即背景)要高。通过分析垂直投影的结果,可以找到字符之间的间隙,这些间隙通常表现为连续的列,其像素值之和接近0。这些低值区域的边界即为字符的切割点。

另一种常见的切割算法是连通域分析法。连通域分析是一种基于图像区域的方法,它可以识别图像中相互连接的像素块。在二值化处理后的图像中,可以通过寻找连通的前景像素来识别单个字符。

以下是使用OpenCV库基于垂直投影算法对文本验证码字符进行切割的示例代码:

     Mat image = Imgcodecs.imread("文本验证码.jpeg");
     // 灰度化处理
     Mat grayImage = new Mat();
     Imgproc.cvtColor(image, grayImage, Imgproc.COLOR_BGR2GRAY);
     // 二值化处理
     Mat binary = new Mat();
     Imgproc.threshold(grayImage, binary, 0, 255, Imgproc.THRESH_BINARY_INV +
Imgproc.THRESH_OTSU);
     Imgcodecs.imwrite("binary.jpeg", binary);
     // 垂直投影
     Mat verticalProjection = new Mat();
     // Core.reduce方法用于在矩阵的某个维度上进行降维操作
     Core.reduce(binary, verticalProjection, 0, Core.REDUCE_SUM, CvType.CV_32S);
     // 找到字符分割线
     List<Integer> cutPoints = new ArrayList<>();
     boolean inCharacter = false;
     for (int i = 0; i < verticalProjection.cols(); i++) {
         double[] val = verticalProjection.get(0, i);
         if (val[0] > 0) {
             if (!inCharacter) {
                 cutPoints.add(i);
                 inCharacter = true;
             }
         } else {
             if (inCharacter) {
                 cutPoints.add(i);
                 inCharacter = false;
             }
         }
     }
     // 根据分割线切割字符
     for (int i = 0; i < cutPoints.size() - 1; i = i + 2) {
         int start = cutPoints.get(i);
         int end = cutPoints.get(i + 1);
         Mat character = new Mat(binary, new Range(0, binary.rows()), new Range(start, end));
         Imgcodecs.imwrite("char_" + i + ".jpg", character);
     }

成功执行上面的代码之后,可以得到切割后的单个字符文件列表。

4.字符识别

现在,我们进入文本验证码识别的最后一个步骤——单体字符的识别。要对前面切割出来的单个字符进行识别,通常需要使用机器学习模型。一般的处理过程如下:

(1)准备训练数据:收集或生成大量的字符图像,并为每个图像标注正确的字符。这些图像应覆盖你想要识别的所有字符。

(2)训练模型:使用或创建一个适合的机器学习模型来识别图像中的字符。

(3)应用模型进行字符识别:将切割出的字符图像输入训练好的模型中,模型将输出每个图像最可能对应的字符。

现在,我们将简单演示如何训练一个字符识别模型,并最终将它应用到我们的Java爬虫程序中。

首先,需要准备训练数据。真实场景中的相关训练数据可以通过多次访问目标网站的验证码生成的API获得。将准备好的训练数据放置到标准的目录结构下,相关目录结构如下:

     - training_data/
         - class1/
             - image1.jpg
             - image2.jpg
             - ...
         - class2/
             - image1.jpg
             - image2.jpg
             - ...
         - class3/
             - image1.jpg
             - image2.jpg
             - ...
         - ...

接下来,搭建模型训练环境。因为大部分模型训练框架都是基于Python语言环境的,所以这里也将使用基于Python环境的TensorFlow框架来训练模型。

安装和配置Python语言开发环境的方式有多种。笔者推荐使用Anaconda工具来配置和管理Python语言开发环境。Anaconda是一个用于数据科学、机器学习和科学计算的开源包管理工具。Anaconda提供了强大的包管理系统,包含超过1500个常用的数据科学和机器学习包,用户可以轻松地安装、更新和管理这些包。笔者认为,Anaconda工具最吸引人的功能是允许用户创建多个独立的Python环境,每个环境可以拥有不同的Python版本和安装包,从而在不同项目中使用不同的环境,避免包冲突问题。

因为本次实验的主要目的是演示字符识别模型的训练过程,所以选择了MobileNet这个轻量级的卷积神经网络模型。MobileNet专为移动和边缘设备上的应用而设计,以实现高效的图像分类和相关视觉任务。MobileNet的核心是使用深度可分离卷积(Depthwise Separable Convolution),这种方法可以显著减少模型的大小和计算复杂度,同时保持较高的性能。

相关的模型训练示例代码如下:

     from tensorflow.keras.applications import MobileNet
     from tensorflow.keras.models import Model
     from tensorflow.keras.layers import Dense, GlobalAveragePooling2D
     from tensorflow.keras.optimizers import Adam
     from tensorflow.keras.preprocessing.image import ImageDataGenerator
     # 加载在 ImageNet 数据集上预训练的 MobileNet 模型,设置 include_top=False 表示不包含顶部的全连
接层,因为我们将要添加自定义输出层
     base_model = MobileNet(weights='imagenet', include_top=False)
     # 添加自定义输出层,包括全局平均池化层、全连接层和 softmax 层
     x = base_model.output
     x = GlobalAveragePooling2D()(x)
     x = Dense(1024, activation='relu')(x)
     predictions = Dense(10, activation='softmax')(x)
     # 构建最终模型
     model = Model(inputs=base_model.input, outputs=predictions)
     # 冻结预训练模型的所有层,只训练自定义的输出层,保持预训练模型的参数不变和模型的泛化能力
     for layer in base_model.layers:
         layer.trainable = False
     # 编译模型,配置模型训练过程中使用的优化器和参数
     model.compile(optimizer=Adam(), loss='categorical_crossentropy',
metrics=['accuracy'])
     # 使用ImageDataGenerator创建图像数据生成器,该生成器会对图像数据进行增强处理和归一化处理等
     train_datagen = ImageDataGenerator(rescale=1./255, rotation_range=20,
width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2,
horizontal_flip=True)
     # 使用新创建的图像数据生成器加载训练数据集
     train_generator = train_datagen.flow_from_directory('$traindata_path/training_data',
target_size=(224, 224), batch_size=32, class_mode='categorical')
     # 对模型进行训练,迭代训练20轮
     model.fit(train_generator, epochs=20)
     # 保存训练好的模型
     model.save('single_char_model.h5')

成功执行上述代码后,将得到一个训练好的模型。随机输入一幅切割好的单字符图片,该模型可以给出相应的识别结果。利用该模型进行预测的示例代码如下:

     import tensorflow as tf
     from PIL import Image
     import numpy as np
     model = tf.keras.models.load_model('single_char_model.h5')
     def load_and_prepare_image(image_path):
         img = Image.open(image_path)
         img = img.resize((224, 224))
         # 确保图像为三通道,如果原图是灰度图,将它转换为RGB图
         if img.mode != 'RGB':
             img = img.convert('RGB')
         img = np.array(img) / 255.0  # 归一化
         img = np.expand_dims(img, axis=0)  # 添加批次维度
         return img
     # 对图像进行分类预测
     def predict_image_class(image_path, model):
         image = load_and_prepare_image(image_path)
         prediction = model.predict(image)
         predicted_class_index = np.argmax(prediction)
         return predicted_class_index
     predicted_class_index = predict_image_class('single_char.jpg', model)
     print(predicted_class_index)

如果我们希望在Java爬虫程序中使用训练好的神经网络模型,可以借助一些Java支持的机器学习算法库来实现,例如Deeplearning4j。Deeplearning4j是一个用于在Java开发环境下构建、训练和部署深度学习模型的开源库。通过Deeplearning4j中的KerasModelImport模块,我们可以将在Keras中训练或保存的模型加载到Deeplearning4j中,从而实现在Deeplearning4j环境中部署和使用已经训练好的Keras模型。以下是使用Keras训练模型识别单字符图片内容的Java示例代码:

     // 加载Keras模型
     String modelPath = "single_char_model.h5";
     ComputationGraph model = KerasModelImport.importKerasModelAndWeights(modelPath);
     System.out.println(model.summary());
     // 读取和预处理图像数据
     NativeImageLoader loader = new NativeImageLoader(224, 224, 3);
     String imageFilePath = "single_char.jpg";
     INDArray imageArray = loader.asMatrix(new File(imageFilePath));
     imageArray.divi(255.0);
     //NOTE: 可能需要对数据格式进行转换
     imageArray = imageArray.reshape(1, 224, 224, 3);
     // 对图像进行分类预测
     INDArray output = model.outputSingle(imageArray);
     int predictedClassIndex = output.argMax(1).getInt(0);
     System.out.println("Predicted class index: " + predictedClassIndex);

3.4.2 文本验证码识别方案2

随着验证码技术的不断发展,字符扭曲和粘连使得字符之间的距离变为零甚至是负数。这种字符样式的变化使得字符的切割操作变得越来越困难和烦琐。与此同时,复杂的干扰线也变得越来越难以去除。在这种情况下,研究人员越来越倾向于借助强大的机器学习模型进行端到端训练,即直接进行从原始图像到最终字符序列的训练过程,避免了烦琐的预处理和字符分割步骤。

接下来演示如何训练AOCR(Attention based OCR,基于注意力机制的光学字符识别)模型来识别文本验证码。该模型结合了卷积神经网络(Convolutional Neural Network, CNN)和循环神经网络(Recurrent Neural Network, RNN)以及注意力机制(Attention Mechanism),以处理图像中的文本识别任务。AOCR模型的基本处理架构如图3-11所示。

图3-11 AOCR模型的处理架构

图3-11所示的AOCR模型处理架构中包含一些机器学习领域常用的术语,下面对这些术语进行简要解释。

卷积计算是机器学习领域常用的一种数学运算,通常用于从图像数据中提取特征。如果将一幅图像中的数据转换成一维向量输入到神经网络模型中,那么图像数据中的空间信息将会丢失。通过卷积计算提取图像中的特征则可以避免这个问题。

卷积计算过程主要涉及三个核心概念。

● 卷积核:一个小型矩阵,用于从输入数据中提取特征。卷积核的大小、形状和数值决定了它可以捕捉的特征类型。

● 滑动步长:卷积核在输入数据上滑动,每次移动一定的步长,每次滑动之后,卷积核与其覆盖的输入数据部分对应元素相乘,并将结果相加,得到卷积的输出值。

● 特征图:卷积计算之后输出的矩阵。将卷积核与输入数据的每个覆盖区域进行元素乘积后,再求和,得到的单个数值构成了输出的特征图中的一个元素。

卷积计算的基本过程如图3-12所示。

图3-12 卷积计算过程示意图

激活函数在神经网络模型中发挥着重要作用。激活函数可以对输入信息进行非线性变换,并将非线性变换后的信息传递给神经网络模型的下一层。如果不使用激活函数,神经网络模型中的每一层输出都是上一层输入的线性函数,无论神经网络有多少层,最终的输出都是输入的线性组合。线性函数在很多情况下都无法真实、准确地反映出现实世界中数据的变化,因为现实世界中的数据变化通常是非线性的。因此,在神经网络模型中引入激活函数可以显著提升模型对数据的学习和理解能力。

池化操作是卷积神经网络模型中常用的一种降维技术。池化操作可以对卷积计算后生成的特征图进行下采样(即降低数据维度),同时保留数据的显著特征。合理使用池化操作不仅可以有效提取图像的显著特征,还可以显著降低计算资源的消耗。

注意力机制(Attention Mechanism)在深度学习领域是一种重要技术,它模仿了人类的注意力原理,允许模型在处理信息时聚焦于最重要的部分。这种机制在自然语言处理(Natural Language Processing, NLP)、图像识别、语音识别等多个领域都有广泛的应用。

感兴趣的读者可以通过以下GitHub仓库链接阅读该模型的具体实现细节:https://github.com/emedvedev/attention-ocr。

现在,我们来看AOCR模型的具体训练步骤。

步骤01 安装AOCR模块。执行以下命令:

     pip install aocr

步骤02 准备验证码数据集。

利用3.2.1节的验证码生成代码,我们可以生成验证码图片数据集合,并将它整理成AOCR模型训练所需的数据格式。

示例代码如下:

     DefaultKaptcha kaptcha = new DefaultKaptcha();
     Properties properties = new Properties();
     properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
     Config config = new Config(properties);
     kaptcha.setConfig(config);
     // 生成并存储验证码训练集图片和标签
     File trainLabels = new File("trainLabels.txt");
     FileWriter trainLabelsWriter = new FileWriter(trainLabels);
     for(int i = 0; i < 1000; i++) {
         String text = kaptcha.createText();
         BufferedImage image = kaptcha.createImage(text);
         String fileName = "trainData/" + i + ".png";
         ImageIO.write(image, "png", new File(fileName));
         trainLabelsWriter.write(fileName + " " + text + "\n");
     }
     trainLabelsWriter.close();
     // 生成并存储验证码测试集图片和标签
     File testLabels = new File("testLabels.txt");
     FileWriter testLabelsWriter = new FileWriter(testLabels);
     for(int i = 0; i < 200; i++) {
         String text = kaptcha.createText();
         BufferedImage image = kaptcha.createImage(text);
         String fileName = "testData/" + i + ".png";
         ImageIO.write(image, "png", new File(fileName));
         testLabelsWriter.write(fileName + " " + text + "\n");
     }
     testLabelsWriter.close();

成功执行上述代码后,我们将得到包含1000幅验证码图片的训练集(trainData目录)和包含200幅验证码图片的测试集(testData目录),以及AOCR模型训练所需的标签注解文件trainLabels.txt和testLabels.txt。

接下来,我们需要将标签注解文件转换成TFRecords格式的文件,执行命令如下:

     aocr dataset trainLabels.txt trainLabel.tfrecords
     aocr dataset testLabels.txt testLabel.tfrecords

步骤03 模型训练。执行以下命令:

     aocr train trainLabel.tfrecords --max-width=220 --max-height=70

上述命令执行完毕后,训练好的模型数据将保存到checkpoints目录下。

接下来,执行命令 aocr test testLabel.tfrecords --max-width=220--max-height=70 来测试模型对验证码的识别效果。实际测试结果通常会非常好,具体结果如图3-13所示。

图3-13 AOCR模型测试结果

步骤04 模型导出与部署。

模型训练成功后,最终需要将模型部署到线上以提供服务。在将模型部署到线上之前,需要将Checkpoint格式的模型转换成SavedModel格式的模型。执行命令aocr export aocr_model之后,将生成一个aocr_model目录,该目录中包含saved_model.pb文件和variables文件夹。aocor_model文件夹就是以SavedModel格式保存的模型。

接下来,我们尝试将该模型部署到Java爬虫程序中(当然,也可以使用TensorFlow Serving等专用的机器学习模型服务系统,爬虫程序可以通过请求TensorFlow Serving提供的接口服务来使用模型)。

如果我们希望将之前训练好的模型部署到爬虫程序中,需要先在Java爬虫程序工程中添加Java TensorFlow API的依赖包。

     <dependency>
         <groupId>org.tensorflow</groupId>
         <artifactId>tensorflow</artifactId>
         <version>${tensorflow.version}</version>
     </dependency>

接下来,我们需要查看相关模型的签名信息以及输入输出信息等。执行saved_model_cli show命令后,可以看到模型的具体信息,如图3-14所示。

图3-14 AOCR模型的签名信息

根据上面的模型信息,我们可以将模型部署到Java爬虫程序中。相关示例代码如下:

     String imagePath = "testData/0.png";
     byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
     SavedModelBundle tensorflowModelBundle = SavedModelBundle.load("aocr_model", "serve");
     Session session = tensorflowModelBundle.session();
     Tensor inputTensor = Tensor.create(imageBytes);
     List<Tensor<?>> output = session.runner().feed("input_image_as_bytes:0", inputTensor)
             .fetch("prediction:0")
             .fetch("probability:0")
             .run();
     System.out.println("Output prediction: " + new String(output.get(0).bytesValue()));
     System.out.println("Output probability: " + output.get(1).doubleValue());

3.4.3 滑块验证码的识别

滑块验证码是近些年来新出现的一种验证码形式。相较于传统的文本验证码,滑块验证码提供了更好的用户体验和安全性。然而,随着机器学习算法和计算机视觉技术的发展,滑块验证码的自动化识别技术也越来越完善。滑块验证码的识别过程主要解决了两个关键问题:

(1)定位滑块验证码中背景图片上的缺口位置。

(2)模拟滑动交互操作,将滑块移动到背景图的缺口位置。

首先,我们来解决第一个问题。通过使用计算机视觉技术中的图像边缘检测技术和模板匹配技术,可以有效地定位滑块验证码背景图片上的滑块缺口位置。

1.图像边缘检测技术

图像边缘检测技术的目的是识别图像中亮度变化显著的点。这些点通常对应物体的边界、轮廓或其他重要的图像特征。图像边缘检测技术能够突出图像中的结构信息,即物体的轮廓或边界。这些轮廓和边界是物体识别和分类中的重要特征。通过仅关注边缘,可以简化图像内容,使后续的图像处理更加集中于图像的关键结构。此外,经过边缘检测处理后的图像通常只包含边缘信息,大量非边缘区域被简化为背景。这种简化操作减少了图像的数据量,从而降低了后续处理的计算复杂度。

对图片进行边缘检测的处理效果如图3-15所示。

图3-15 图片边缘检测技术应用效果

2.模板匹配技术

图像模板匹配技术是一种在计算机视觉领域常用的图像处理技术。它的基本思路是在较大图像中寻找与给定模板图像最匹配的部分。具体实现时,模板图像在背景图像(或称为源图像)上滑动,通过计算模板图像与它覆盖区域的相似度来识别特定的图像区域。相似度通过使用相关性或差异度来衡量。

在理解了相关技术实现的原理后,我们来看如何具体实现识别滑块缺口在背景图片上位置的代码。

     String bgImagePath = "background.png";
     String sliderImagePath = "slider.png";
     // 图片灰度化处理
     Mat bgImg = Imgcodecs.imread(bgImagePath, Imgcodecs.IMREAD_GRAYSCALE);
     Mat sliderImg = Imgcodecs.imread(sliderImagePath, Imgcodecs.IMREAD_GRAYSCALE);
     Mat bgImgEdge = new Mat();
     Mat sliderImgEdge = new Mat();
     // 使用边缘检测增强图像特征
     Imgproc.Canny(bgImg, bgImgEdge, 50, 150);
     Imgproc.Canny(sliderImg, sliderImgEdge, 50, 150);
     // 使用模板匹配寻找最佳匹配位置
     Mat result = new Mat();
     Imgproc.matchTemplate(bgImgEdge, sliderImgEdge, result, Imgproc.TM_CCOEFF_NORMED);
     Core.MinMaxLocResult mmr = Core.minMaxLoc(result);
     // 计算滑块的顶点位置
     Point topLeft = mmr.maxLoc;
     Point bottomRight = new Point(topLeft.x + sliderImg.cols(), topLeft.y +
sliderImg.rows());
     // 返回滑块缺口的大致位置
     System.out.println("缺口位置大致为:" + topLeft + "到" + bottomRight);
     Imgproc.rectangle(bgImg, topLeft, bottomRight, new Scalar(255, 0, 0), 2);
     HighGui.imshow("result", bgImg);
     HighGui.waitKey();

成功执行上述代码后,我们将得到滑块缺口在背景图片上的大致位置。具体效果如图3-16所示。

图3-16 滑块缺口定位效果图

解决滑块缺口定位问题后,下一步是确定如何将滑块移动到缺口的正确位置。相对于文本验证码,滑块验证码的验证逻辑不仅依赖于滑块缺口在背景图片中的坐标位置,更关注验证过程中的数据,也就是滑动轨迹。那么,我们如何向验证码服务器端提交正确的请求信息呢?通常有三种解决思路。

第一种思路是阅读网站的JavaScript文件源代码。通过分析JavaScript文件源代码,我们可以了解网站收集了哪些数据并提交给服务器端进行验证。但是,很多网站会对自己的JavaScript文件进行混淆加密处理,因此我们可能需要对JavaScript文件进行反混淆操作。这种解决方案的成本相对较高,周期也会更长。

第二种思路是捕获网站JavaScript文件发送给网站服务器端的验证请求,从而获取哪些数据被提交到服务器端进行验证操作。然而,如果网站使用HTTPS协议加密请求数据,我们仍然需要对JavaScript文件进行逆向分析,以获取请求数据的明文信息。

第三种思路是UI自动化。使用Selenium等自动化框架来模拟滑块的滑动操作,将滑块移动到合适的位置。这种方式相对于逆向处理JavaScript文件的成本要低很多。

接下来,我们将展示如何通过Selenium框架实现对滑块验证码的自动化处理操作。完整的代码如下:

     String url = "https://www.somesite.com/login.jsp";
     System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY,
"$chromedriver_path/chromedriver");
     ChromeOptions chromeOptions = new ChromeOptions();
     ChromeDriver chromeDriver = new ChromeDriver(chromeOptions);
     chromeDriver.get(url);
     /** 获取滑块验证码的背景图片和滑块图片 **/
     String base64BgImg =
"iVBORw0KGgoAAAANSUhEUgAAAWgA…1dN4Eq+BG3QF/wqoLpv8DW9Y4Aks33kAAAAAASUVORK5CYII=";
     String base64SliderImg =
"iVBORw0KGgoAAAANSUhEUgAAADw...AAAA8CAYAAAA6/NlyAAAG+0lEQVR42u2aS2xUVRjHWbAw";
     Mat bgMat = base64ToMat(base64BgImg);
     Mat sliderMat = base64ToMat(base64SliderImg);
     double distance = getDistance(bgMat, sliderMat);
     simulateSlide(chromeDriver, (int)distance);
     chromeDriver.quit();
     // 模拟滑动滑块,注意:直接进行滑动操作,没有模拟人类行为
     public static void simulateSlide(ChromeDriver chromeDriver, int distance) {
         WebElement sliderButton = chromeDriver.findElement(By.xpath(SLIDER_BUTTON_XPATH));
         Actions actions = new Actions(chromeDriver);
         actions.clickAndHold(sliderButton);
         actions.moveByOffset((int)distance, 0);
         actions.release();
         actions.perform();
     }
     // 获取滑块图片在背景图片上的缺口位置
     public static double getDistance(Mat bgMat, Mat sliderMat) {
         Mat bgGrayMat = new Mat();
         Mat sliderGrayMat = new Mat();
         Imgproc.cvtColor(bgMat, bgGrayMat, Imgproc.COLOR_BGR2GRAY);
         Imgproc.cvtColor(sliderMat, sliderGrayMat, Imgproc.COLOR_BGR2GRAY);
         // 使用边缘检测增强图像特征
         Mat bgImgEdge = new Mat();
         Mat sliderImgEdge = new Mat();
         Imgproc.Canny(bgGrayMat, bgImgEdge, 50, 150);
         Imgproc.Canny(sliderGrayMat, sliderImgEdge, 50, 150);
         // 使用模板匹配寻找最佳匹配位置
         Mat result = new Mat();
         Imgproc.matchTemplate(bgImgEdge, sliderImgEdge, result, Imgproc.TM_CCOEFF_NORMED);
         Core.MinMaxLocResult mmr = Core.minMaxLoc(result);
         // 计算滑块的顶点位置
         Point topLeft = mmr.maxLoc;
         return topLeft.x;
     }
     
     
     public static Mat base64ToMat(String base64Image) {
         byte[] decodedBytes = Base64.getDecoder().decode(base64Image);
         MatOfByte mob = new MatOfByte(decodedBytes);
         return Imgcodecs.imdecode(mob, Imgcodecs.IMREAD_COLOR);
     }

需要注意的是,上面的示例代码假设滑块图片默认出现在背景图片的最左侧位置。如果滑块图片的初始位置是随机的,还需要获取滑块图片的实际初始位置。

3.4.4 点选验证码的识别

本小节主要探讨如何自动化识别点选验证码。点选验证码要求用户根据提示在复杂背景图片上点击特定符号,它的主要形式如图3-17所示。

图3-17 文字点选验证码示例

自动化识别点选验证码的过程主要解决两个关键问题:

(1)定位背景图像中所有可能的文字区域。

(2)识别文字区域中的文本信息。

在着手解决这两个关键问题之前,我们先做一个简短的实验说明。本小节的实验将使用自制的文字点选验证码,以便有效控制样本集的数量,并节省数据标注成本和模型训练成本。实验中的点选验证码只展示“网络爬虫”这四个汉字,但汉字的位置和颜色是随机的。虽然样本空间被简化和缩小了,但整体流程在真实场景中依然有效。

1.数据标注

首先,我们来看如何解决第一个关键问题。要准确定位背景图像中的文字区域,我们必须借助自然场景下的目标检测算法。目前常用的目标检测算法包括YOLO算法和EAST算法等。无论使用哪种目标检测算法,首先都需要对算法模型进行训练,而训练算法模型的主要前置条件是标注数据。

目前比较流行的开源图像标注工具是LabelImg。由于系统环境不同,LabelImg的具体安装方式也会有所不同。读者可以参考官方文档:https://github.com/HumanSignal/labelImg。

现在,我们已经使用前面章节的点选验证码生成示例代码创建了34个验证码作为训练和验证样本,并且创建了2个验证码作为测试样本。图片创建好之后,可以使用LabelImg工具对图像上的文字区域进行标注。LabelImg工具的操作界面如图3-18所示。

图3-18 LabelImg工具的操作界面

数据标注完成后,我们会在标注数据的存储目录中看到与被标注图像同名的TXT文件,其中包含标注的矩形框类别和坐标等信息。因为我们选择的是YOLO模型格式,所以被标注图像的TXT文件的内容格式为:<class_id><center_x> <center_y> <width> <height>,其中<class_id>是目标类别的索引,<center_x>和<center_y>是目标中心点在图像中的相对坐标,<width>和<height>是目标边界框的宽度和高度。这些坐标和尺寸都被归一化到[0,1]范围内。具体示例如图3-19所示。

图3-19 标注数据存储格式

同时,我们还会看到一个classes.txt文件,该文件中的每行是一个标注类别。

2.模型训练

数据标注完毕后,首先尝试训练YOLO算法模型来完成目标检测定位的任务。YOLO(You Only Look Once)是目前非常流行的一种目标检测算法。自2015年首次被提出后,该算法一直在不断改进和完善。接下来,我们将训练YOLOv5算法模型,看看其在图像目标检测任务中的效果。

首先,我们需要配置YOLOv5模型训练所需的环境。安装YOLOv5并执行如下命令:

     git clone https://github.com/ultralytics/yolov5.git
     cd yolov5
     pip install -r requirements.txt

配置好YOLOv5模型训练所需的环境之后,我们需要将之前标注的数据按照要求放置到相应的目录结构下,基本的目录结构如下:

     /parent_folder
         /dataset
             /images
                 /train
                 /val
             /labels
                 /train
                 /val

images目录下放置的是图片列表,labels目录下放置的是标注文件列表。在我们的实验中,共选择了34幅图片,其中30幅放置到images/train/目录下,对应的标注信息放置到labels/train/目录下。剩下的4幅图片作为验证集数据,放置到相应的目录下。

然后,通过YOLOv5项目的release页面(https://github.com/ultralytics/yolov5/releases)下载预训练模型。在我们的实验中,选择了小型预训练模型yolov5s.pt。

预训练模型是在大规模数据集上事先训练好的神经网络模型。这些模型通常通过使用大量的标记数据(例如图像、文本或音频数据)进行训练,以学习如何识别特定的模式、特征或关系。预训练模型能够捕捉通用的数据特征,并且具有在其他任务上进行微调的能力。

具体来说,预训练模型的训练通常分为以下两个阶段。

(1)预训练阶段:在大规模数据集上进行训练,例如在包含数百万幅图片的ImageNet数据集上进行训练,以识别图像中的物体。

(2)微调阶段:将预训练模型应用于特定的任务,并在相对较小的数据集上进行微调,以适应特定的数据和问题。

预训练模型的优势在于它们能够学习到通用的特征表示,因此在许多不同的任务上都表现良好。通过在特定任务上微调这些模型,可以加快模型收敛的速度,并且通常能够取得比从零开始训练模型更好的效果。

接下来,准备两个YAML文件。

第一个YAML文件指定了训练数据目录和验证数据目录的位置,以及模型预测类别的数量和名称。在我们的实验中,其内容如下:

     train: yolov5/dataset/images/train             # 训练数据图片路径
     val: yolov5/dataset/images/val                 # 验证数据图片路径
     nc: 4                                          # 类别数
     names: ['pa', 'chong', 'wang', 'luo' ]         # 类别名称

第二个YAML文件是有关模型架构配置的YAML文件。在yolov5/models/目录下已经放置了一些默认的模型架构配置文件。在我们的实验中,使用yolov5s.yaml配置文件。将文件的一行修改成nc: 4。

模型训练环境配置完毕后,就可以执行训练脚本train.py对模型进行训练了。相关命令如下:

     python train.py --img 320 --batch 8 --epochs 200 --data data.yaml --cfg
custom_yolov5s.yaml
     --weights yolov5s.pt --name yolov5s_results

上面的命令指定了很多参数,逐一介绍如下:

● --img 320:指定输入图像的大小。

● --batch 8:设置每个批次处理的图像数量。

● --epochs 200:指定训练的迭代次数。每次迭代都会遍历一次全部数据。

● --data data.yaml:指定train.py脚本读取数据配置信息的位置和具体信息。

● --cfg yolov5s.yaml:指向一个包含模型架构配置的YAML文件。

● --weights yolov5s.pt:指定预训练模型。

● --name yolov5s_results:设置保存训练结果的目录名称。

训练完毕之后,我们可以在/yolov5/runs/train/yolov5s_results目录下查看相关的训练结果,如图3-20所示。

打开val_batch0_labels.jpg,我们会看到YOLOv5模型对验证数据集中的4幅图片的文字识别结果,如图3-21所示。

图3-20 YOLOv5训练结果示例

图3-21 YOLOv5模型验证数据集文字识别结果

模型训练完毕后,我们再生成两幅图片来测试一下模型的检测效果。具体命令如下:

     python detect.py --weights runs/train/exp3/weights/best.pt --img 320
     --conf 0.3 --source dataset2/images/test

上述命令的三个主要参数的含义如下。

● --weights:模型路径。

● --conf:设置接受的置信度范围。

● --source:指定输入的测试图片路径。

在本实验中,测试结果如图3-22所示。

图3-22 YOLOv5模型测试结果

3.模型部署

训练完模型之后,我们需要将训练好的模型部署到Java爬虫程序中。在之前的章节中,我们已经介绍了两个Java开发环境下使用的机器学习库Deeplearing4j和Java TensorFlow API。接下来,我们将介绍一个新兴的开源Java深度学习库DJL(Deep Java Library)。该开源库由亚马逊主导开发,旨在帮助Java开发者快速将优秀的AI能力集成到自己的Java应用中。

首先,需要将训练好的模型从PyTorch格式导出为TorchScript格式。YOLOv5项目工程提供了export.py脚本来完成模型格式的转换。与train.py类似,export.py有很多命令参数。具体的参数名称和作用,可以在export.py文件中查看到。

     def parse_opt(known=False):
         """Parses command-line arguments for YOLOv5 model export configurations, returning
the parsed options."""
         parser = argparse.ArgumentParser()
         parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml",
help="dataset.yaml path")
         parser.add_argument("--weights", nargs="+", type=str, default=ROOT / "yolov5s.pt",
help="model.pt path(s)")
         parser.add_argument("--imgsz", "--img", "--img-size", nargs="+", type=int,
default=[640, 640], help="image (h, w)")
         parser.add_argument("--batch-size", type=int, default=1, help="batch size")
         parser.add_argument("--device", default="cpu", help="cuda device, i.e. 0 or 0,1,2,3
or cpu")
         parser.add_argument("--half", action="store_true", help="FP16 half-precision
export")
         parser.add_argument("--inplace", action="store_true", help="set YOLOv5 Detect()
inplace=True")
         parser.add_argument("--keras", action="store_true", help="TF: use Keras")
         parser.add_argument("--optimize", action="store_true", help="TorchScript: optimize
for mobile")
         parser.add_argument("--int8", action="store_true", help="CoreML/TF/OpenVINO INT8
quantization")
         parser.add_argument("--per-tensor", action="store_true", help="TF per-tensor
quantization")
         parser.add_argument("--dynamic", action="store_true", help="ONNX/TF/TensorRT:
dynamic axes")
         parser.add_argument("--simplify", action="store_true", help="ONNX: simplify
model")
         parser.add_argument("--opset", type=int, default=17, help="ONNX: opset version")
         parser.add_argument("--verbose", action="store_true", help="TensorRT: verbose
log")
         parser.add_argument("--workspace", type=int, default=4, help="TensorRT: workspace
size (GB)")
         parser.add_argument("--nms", action="store_true", help="TF: add NMS to model")
         parser.add_argument("--agnostic-nms", action="store_true", help="TF: add agnostic
NMS to model")
         parser.add_argument("--topk-per-class", type=int, default=100, help="TF.js NMS:
topk per class to keep")
         parser.add_argument("--topk-all", type=int, default=100, help="TF.js NMS: topk for
all classes to keep")
         parser.add_argument("--iou-thres", type=float, default=0.45, help="TF.js NMS: IoU
threshold")
         parser.add_argument("--conf-thres", type=float, default=0.25, help="TF.js NMS:
confidence threshold")
         parser.add_argument(
             "--include",
             nargs="+",
             default=["torchscript"],
             help="torchscript, onnx, openvino, engine, coreml, saved_model, pb, tflite,
edgetpu, tfjs, paddle",
         )
         opt = parser.parse_known_args()[0] if known else parser.parse_args()
         print_args(vars(opt))
         return opt

导出的具体命令如下:

     python export.py --weights best.pt --img 320

模型成功导出后,我们就可以使用DJL深度学习库来加载和使用该模型了。

DJL深度学习库支持多种深度学习框架,包括TensorFlow、PyTorch和MXNet等,同时还提供了自己的深度学习引擎,使用户能够在不同的框架之间无缝切换。该库还提供了一系列预训练模型和工具,帮助用户快速构建和部署深度学习模型。我们具体使用的DJL依赖库如下:

     <dependency>
         <groupId>ai.djl.pytorch</groupId>
         <artifactId>pytorch-engine</artifactId>
     </dependency>
     <dependency>
         <groupId>ai.djl.pytorch</groupId>
         <artifactId>pytorch-model-zoo</artifactId>
     </dependency>
     <dependency>
         <groupId>ai.djl.pytorch</groupId>
         <artifactId>pytorch-native-auto</artifactId>
     </dependency>

示例代码如下:

     // 创建管道,对图像进行预处理
     Pipeline pipeline = new Pipeline();
     pipeline.add(new Resize(320, 320));
     pipeline.add(new ToTensor());
     // 创建转换器,定义模型的输入输出处理方式
     Translator<Image, DetectedObjects> translator =  YoloV5Translator
             .builder()
             .setPipeline(pipeline)
             .optSynset(Arrays.asList("pa", "chong", "wang", "luo"))
             .build();
     // 配置并加载模型
     Criteria<Image, DetectedObjects> criteria = Criteria.builder()
             .setTypes(Image.class, DetectedObjects.class)
             .optModelUrls(YOLOV5Demo.class.getResource("/$model_path").getPath())
             .optModelName("best.torchscript")
             .optTranslator(translator)
             .optProgress(new ProgressBar())
             .optDevice(Device.cpu())
             .build();
     ZooModel<Image,DetectedObjects> model = ModelZoo.loadModel(criteria);
     // 加载图像,并进行预测
     Image img = ImageFactory.getInstance().fromFile(Paths.get("$img_path"));
     Predictor<Image, DetectedObjects> predictor = model.newPredictor();
     DetectedObjects results = predictor.predict(img);
     System.out.println(results);

注意 模型训练过程中使用的PyTorch版本与模型部署使用的PyTorch版本需要保持一致,否则可能导致识别错误。另外,不同的算法模型库在实现细节上的差异也会影响识别结果。

在获取到文字区域的坐标之后,我们可以通过OCR模型(例如前面章节中提到的AOCR模型)将文字区域的图像转换成文本。具体的训练过程和实现细节,读者可参考前面章节的内容。 VTLW+WF1l1UoVeaHJFsfrdmg4gj+vp1HBnTp89+YIPTfQkpBtgx2cacjy0ko3pbN

点击中间区域
呼出菜单
上一章
目录
下一章
×