【代码学习】记录一次滑块验证解决思路

注意: 本文最后更新于 110 天前,内容可能已不再准确,请酌情参考。

前言

由于本人住的地方如果欠电费了,缴完电费后要等待一个多小时才会负电。因此突发奇想,写个脚本每天8:30推送电量余额以及昨日用电量。

实现环境


1. Windows10
2. nodejs(16.14.0)

实现过程

寻找数据来源


1.此次数据来源有微信,支付宝,以及官方网站以及APP。考虑到实现成本,选择官网作为我们的数据来源其实是最合适的。

2.选择数据来源后,查询数据肯定是需要登录的,由于网站登录提交的数据以及相应数据都为加密数据,打断点扣算法的效率并不是很高。所以选择使用puppeteer来模拟网站操作,从而获取网站信息。

登录


官方网站登录有账号密码登录,短信登录以及APP扫码登录。其中账号密码登录会触发滑块验证,短信登录以及App扫码登录相对来说比较麻烦。因此优先考虑账号密码登录。
  • 账号密码登录会触发滑块验证。对滑块图进行分析,其实就是一张背景图,一张滑块图。
    file

    1.其实到这里,我们可以用opencv来实现滑块验证了,但是由于我现在使用的是nodejs,也就是说,我需要安装对应的依赖--opencv4nodejs。但是这里有很大的坑,nodejs安装这个依赖几乎很难成功,需要配置特别多的东西。在我奋战6个小时后,配置了一大堆还是以失败告终。但是我又不想转python,因此这个时候就需要对图片进行处理了,然后算出滑块需要移动的距离。2.从这两张图片其实可以看出来,滑块跟背景图上的缺口其实是在一条水平线上的。因此我们只需要算出来滑块最左侧到缺口最左侧的距离即可。如下图

    file


有了思路后,就会有一些问题。比如如何确定缺口最左侧的位置。这个问题要从图片的构成来说起。图片其实就是由一个一个的像素点构成的。可以理解为一个矩阵或者坐标。从图片左上角开始,向右为x轴,向下为y轴。如下图

file

把背景图放到ps里进行放大操作,可以看到如下的像素点。

file

所以其实这个图片就是一个横坐标x为410,纵坐标y为200.也就是这个图片的像素410*200。

明白图片的构成后,我们就要想办法搞清楚怎么样才能知道缺口最左侧的所在的坐标的位置。把这个问题转换一下,就是想办法找到在x相同的情况下,随着y的增加,会连续出现灰黑色的点就说明已经到缺口所在的位置了。

那什么是灰黑色,图片上的一个点其实是用一个数组来表示的,即RGBA[0,0,0,0]。这个数组决定了这个点的颜色。也就是说,这个坐标轴上的一个点是用[0,0,0,0]来表示的。

搞清楚之后,我们只需要拿到图片的坐标信息,然后遍历就可以找到缺口所在的位置了。具体代码如下。

async function calculateSliderDistance(backgroundPath, sliderPath) {
    // 加载背景图和滑块图
    const [bgImage, sliderImage] = await Promise.all([
        loadImage(backgroundPath),
        loadImage(sliderPath)
    ]);

    const width = bgImage.width;
    const height = bgImage.height;

    // 创建 Canvas 对象
    const canvas = createCanvas(width, height);
    const ctx = canvas.getContext('2d');

    ctx.drawImage(sliderImage, 0, 0);
    const sliderData = ctx.getImageData(0, 0, width, height);

    const sliderTwoDimensionalData = convert1DArrayTo2DImage(sliderData.data, sliderData.width, sliderData.height);

    var findFirstOpaqueRow1 = findFirstOpaqueRow(sliderTwoDimensionalData);
    // console.log("滑块上界:", findFirstOpaqueRow1);

    ctx.clearRect(0, 0, width, height);
    ctx.drawImage(bgImage, 0, 0);
    const bgData = ctx.getImageData(0, 0, width, height);
    const bgTwoDimensionalData = convert1DArrayTo2DImage(bgData.data, bgData.width, bgData.height);
    const moveDistance = findColumnDifference(bgTwoDimensionalData, findFirstOpaqueRow1, 60, 10);
    // console.log(moveDistance);

    // 清除之前绘制的内容
    ctx.clearRect(0, 0, width, height);
    ctx.drawImage(bgImage, 0, 0);

    // 移动滑块图像并绘制到 Canvas 上
    if (moveDistance !== -1) {
        const sliderWidth = sliderImage.width;
        const sliderHeight = sliderImage.height;
        ctx.drawImage(sliderImage, moveDistance, 0, sliderWidth, sliderHeight);
    }

    // 保存 Canvas 内容为图像文件
    const buffer = canvas.toBuffer('image/png');
    fs.writeFileSync('result.png', buffer);

    return moveDistance;
}

利用canvas可以直接获取到图像的信息,但是由于获取到的是一个一维数组,并不能表示为矩阵,坐标系这种。所以我加了一个方法convert1DArrayTo2DImage来转换成对应的格式。

获取到对应的坐标信息后,我就可以进行遍历了,但是从头开始遍历,其实很浪费时间。为何不从缺口上界开始呢。那么这个上界又如何确定呢,这个时候就依赖滑块这张图片了。因为滑块始终处于最左侧,所以我们可以对滑块的矩阵进行遍历,找到第一次出现不是透明色的y轴的坐标,然后背景图就可以从此坐标开始,从而分割成从如下图开始遍历。

file


所以到这里,我们确定了缺口最左侧的位置,只需要计算出需要移动的距离即可,也就是x的坐标。具体代码如下。
function findFirstOpaqueRow(image) {
    for (let y = 0; y < image.length; y++) {
        for (let x = 0; x < image[y].length; x++) {
            const pixel = image[y][x];
            // 检查透明度(A)是否大于 0
            if (pixel[3] > 0) {
                return y; // 返回首次出现不透明像素的行数
            }
        }
    }
    return -1; // 如果没有不透明像素,返回 -1
}

function findColumnDifference(image, startRow, maxRGB, minConsecutive) {
    const width = image[0].length;
    const height = image.length;

    // 确保行数上界在有效范围内
    if (startRow >= height || startRow < 0) {
        throw new Error('Invalid start row index');
    }

    // 初始化变量
    let consecutiveCount = 0;
    let startColumn = -1;

    // 遍历每列
    for (let x = 0; x < width; x++) {
        let isValidColumn = true;

        // 检查当前列的每个像素点
        for (let y = startRow; y < height; y++) {
            const [r, g, b] = image[y][x];
            if (r > maxRGB || g > maxRGB || b > maxRGB) {
                isValidColumn = false;
                consecutiveCount = 0;
                break;
            }else{
                // console.log(第${y}行,第${x}列 , image[y][x]); if (consecutiveCount === 0) { startColumn = x; // 记录符合条件的列的起始列数 } consecutiveCount++; if (consecutiveCount >= minConsecutive) { return startColumn; } } } } return -1; // 如果没有找到符合条件的列,返回 -1 }

最后结果

可以看到最后输出的result.png是拼接成功的,需要向右滑动223个像素点

file

本内容需要登录后查看
阅读剩余
THE END