Geetest拖拽验证码破解思路(java)

项目中有个需求希望能获取公司的工商注册信息,刚开始是想与第三方数据公司合作,因种 种原因合作没有达成。于是想做个爬虫直接从工商局的《企业信用信息公示系统》中获取。 要想从《企业信用信息公示系统》中爬取数据,首先必须解决掉Geetest验证码的问题。 经过一系列的摸索,发现要想破解geetest的验证码,主要需要解决如下几个问题:

  1. 背景图的还原
  2. 找到背景图中缺口的位置
  3. 将滑块拖拽到缺口

背景图的还原

Geetest背景图分为两张,一张是完整背景图,一张是带缺口的背景图。每张图片被分成 52 份, 上下两部分各 26 份,然后乱序排列。在网页上显示时,是通过css来将乱序的 碎片重新组列成完整的图片。

乱序图片:origin_img

正常图片:show_img

因此,我们必须将原始的背景乱序图片切根据页面上的css样式将图片切割成 52 份,并 按照先后顺序拼接还原成正确的图片。需要使用到java.awt.image.BufferedImage类:

/**
  * 将Geetest打乱的图片还原,Geetest的原始背景图是分成52份碎片乱序组合的。目前是上下各26份,每份碎片图片宽10px, 高58px。
  *
  * @param image     原始背景图
  * @param locations 展示位置列表,数据结构:[{x=-25, y=-58}, ...]
  * @return 顺序排列好的图片
  */
public static BufferedImage recover(BufferedImage image, List<Map<String, Integer>> locations) throws IOException {
    long begin = System.currentTimeMillis();

    int per_image_with = 10;  // 每张碎片图片的宽度
    int per_image_height = 58; // 每张碎片图片的高度


    List<BufferedImage> upperList = new ArrayList<>();
    List<BufferedImage> downList = new ArrayList<>();

    // 将原始图片裁剪成碎片
    for (Map<String, Integer> location : locations) {
        int x = location.get("x");
        int y = location.get("y");
        if (y == -58) {
            upperList.add(image.getSubimage(abs(x), 58, per_image_with, per_image_height));
        } else if (y == 0) {
            downList.add(image.getSubimage(abs(x), 0, per_image_with, per_image_height));
        }
    }

    BufferedImage newImage = new BufferedImage(upperList.size() * per_image_with, image.getHeight(), image.getType());

    // 重绘图片的上半部分
    int x_offset = 0;
    for (BufferedImage bufferedImage : upperList) {
        Graphics graphics = newImage.getGraphics();
        graphics.drawImage(bufferedImage, x_offset, 0, null);
        x_offset += bufferedImage.getWidth();
    }

    // 重绘图片的下半部分
    x_offset = 0;
    for (BufferedImage bufferedImage : downList) {
        Graphics graphics = newImage.getGraphics();
        graphics.drawImage(bufferedImage, x_offset, 58, null);
        x_offset += bufferedImage.getWidth();
    }

    log.debug("还原图片耗时:{}ms", System.currentTimeMillis() - begin);
    return newImage;
}

找到背景图中缺口的位置

正常图片: 正常图片

缺口图片:  缺口图片

Geetest验证时,只需要将拼图块水平移动到正确的位置即可。因此通过上面两张图片比 对,发现我们只要找到缺口的x坐标即可。

这里我借用的参考内容的方法:两张原始图的大小都是相同的 260*116,那就通过两个 for 循环依次对比每个像素点的 RGB 值,如果相差超过 50 则就认为找到了缺口的位置。

/**
  * 计算验证图的缺口位置(x轴) 两张原始图的大小都是相同的260*116,那就通过两个for循环依次对比每个像素点的RGB值, 如果RGB三元素中有一个相差超过50则就认为找到了缺口的位置
  *
  * @param image1 图像1
  * @param image2 图像2
  * @return 缺口的x坐标
  */
public static int getDiffX(BufferedImage image1, BufferedImage image2) {
    long begin = System.currentTimeMillis();

    for (int x = 0; x < image1.getWidth(); x++) {
        for (int y = 0; y < image1.getHeight(); y++) {
            if (!isSimilar(image1, image2, x, y)) {
                return x;
            }
        }
    }

    log.debug("图片对比耗时:{}ms", System.currentTimeMillis() - begin);
    return 0;
}

/**
 * 判断image1, image2的[x, y]这一像素是否相似,如果该像素的RGB值相差都在50以内,则认为相似。
 *
 * @param image1   图像1
 * @param image2   图像2
 * @param x_offset x坐标
 * @param y_offset y坐标
 * @return 是否相似
 */
public static boolean isSimilar(BufferedImage image1, BufferedImage image2, int x_offset, int y_offset) {
    Color pixel1 = new Color(image1.getRGB(x_offset, y_offset));
    Color pixel2 = new Color(image2.getRGB(x_offset, y_offset));

    return abs(pixel1.getBlue() - pixel2.getBlue()) < 50 && abs(pixel1.getGreen() - pixel2.getGreen()) < 50 && abs(pixel1.getRed() - pixel2.getRed()) < 50;
}

将滑块拖拽到缺口

Geetest在拖拽拼图块时,对拼图块的移动轨迹做了一些 “ 人工智能 ” 的算法识别,使 用程序直接将拼图块拖入缺口时,发现有很大的概率会被识别为机器操作,页面显示怪物 吃掉了饼图。因此我们需要采用一些算法来模拟人拖拽的行为。

/**
  * 根据缺口位置x_offset,仿照手动拖动滑块时的移动轨迹。
  * 手动拖动滑块有几个特点:
  * 开始时拖动速度快,最后接近目标时会慢下来;
  * 总时间大概1~3秒;
  * 有可能会拖超过后再拖回头;
  *
  * @return 返回一个轨迹数组,数组中的每个轨迹都是[x,y,z]三元素:x代表横向位移,y代表竖向位移,z代表时间间隔,单位毫秒
  */
private static List<Map<String, Integer>> getTrack(int x_offset) {
    List<Map<String, Integer>> tracks;
    long begin = System.currentTimeMillis();

    // 实际上滑块的起始位置并不是在图像的最左边,而是大概有6个像素的距离,所以滑动距离要减掉这个长度
    x_offset = x_offset - 6;

    if (getRandom(0, 10) % 2 == 0) {
        tracks = strategics_1(x_offset);
    } else {
        tracks = strategics_2(x_offset);
    }

    log.debug("生成轨迹耗时: {}ms", System.currentTimeMillis() - begin);
    log.debug("计算出移动轨迹: {}", tracks);
    return tracks;
}

/**
  * 轨迹策略1
  */
private static List<Map<String, Integer>> strategics_1(int x_offset) {
    List<Map<String, Integer>> tracks = new ArrayList<>();
    float totalTime = 0;

    int x = getRandom(1, 3);

    // 随机按1~3的步长生成各个点
    while (x_offset - x >= 5) {
        Map<String, Integer> point = new HashMap<>(3);
        point.put("x", x);
        point.put("y", 0);
        point.put("z", 0);
        tracks.add(point);

        x_offset = x_offset - x;
        x = getRandom(1, 5);
        totalTime += point.get("z").floatValue();
    }

    // 后面几个点放慢时间
    for (int i = 0; i < x_offset; i++) {
        Map<String, Integer> point = new HashMap<>(3);
        point.put("x", 1);
        point.put("y", 0);
        point.put("z", getRandom(10, 200));

        tracks.add(point);
        totalTime += point.get("z").floatValue();
    }

    log.debug("预计拖拽耗时: {}ms", totalTime);
    return tracks;
}

/**
  * 轨迹策略2
  */
private static List<Map<String, Integer>> strategics_2(int x_offset) {
    List<Map<String, Integer>> tracks = new ArrayList<>();
    float totalTime = 0;

    int dragX = 0; // 已拖拽的横向偏移量
    int nearRange = getRandom(5, 10); // 靠近缺口的范围
    while (dragX < x_offset - nearRange) { // 生成快速拖拽点,拖拽距离非常靠近切口
        int stepLength = getRandom(1, 5); // 随机按1~5的步长生成各个点
        Map<String, Integer> point = new HashMap<>(3);
        point.put("x", stepLength);
        point.put("y", 0);
        point.put("z", getRandom(0, 2));
        tracks.add(point);

        totalTime += point.get("z").floatValue();
        dragX += stepLength;
    }

    // 随机一定的比例将滑块拖拽过头
    if (getRandom(0, 99) % 2 == 0) {
        int stepLength = getRandom(10, 15); // 随机按1~5的步长生成各个点
        Map<String, Integer> attachPoint = new HashMap<>(3);
        attachPoint.put("x", stepLength);
        attachPoint.put("y", 0);
        attachPoint.put("z", getRandom(0, 2));
        tracks.add(attachPoint);

        dragX += stepLength;
        totalTime += attachPoint.get("z").floatValue();
    }

    // 精确点
    for (int i = 0; i < Math.abs(dragX - x_offset); i++) {
        if (dragX > x_offset) {
            Map<String, Integer> point = new HashMap<>(3);
            point.put("x", -1);
            point.put("y", 0);
            point.put("z", getRandom(10, 100));
            tracks.add(point);

            totalTime += point.get("z").floatValue();
        } else {
            Map<String, Integer> point = new HashMap<>(3);
            point.put("x", 1);
            point.put("y", 0);
            point.put("z", getRandom(10, 100));
            tracks.add(point);

            totalTime += point.get("z").floatValue();
        }
    }

    log.debug("预计拖拽耗时: {}ms", totalTime);
    return tracks;
}

/**
  * 根据移动轨迹,模拟拖动极验的验证滑块
  */
private static boolean simulateDrag(WebDriver webDriver, Site site, List<Map<String, Integer>> tracks) throws InterruptedException {
    log.debug("开始模拟拖动滑块");

    WebElement slider = webDriver.findElement(By.cssSelector(site.getGeetest().getSliderKnob()));
    log.debug("滑块初始位置: {}", slider.getLocation());

    Actions actions = new Actions(webDriver);
    actions.clickAndHold(slider).perform();

    for (Map<String, Integer> point : tracks) {
        int x = point.get("x") + 22;
        int y = point.get("y") + 22;

        actions.moveToElement(slider, x, y).perform();

        int z = point.get("z");
        TimeUnit.MILLISECONDS.sleep(z);
    }

    TimeUnit.MILLISECONDS.sleep(getRandom(100, 200)); // 随机停顿100~200毫秒
    actions.release(slider).perform();

    TimeUnit.MILLISECONDS.sleep(100); // 等待0.1秒后检查结果

    try {
        // 在5秒之内检查弹出框是否消失,如果消失则说明校验通过;如果没有消失说明校验失败。
        new WebDriverWait(webDriver, 5).until((ExpectedCondition<Boolean>) driver -> {
            try {
                WebElement popupElement = driver.findElement(By.cssSelector(site.getGeetest().getPopupWrap()));
                return !popupElement.isDisplayed();
            } catch (NoSuchElementException e) {
                return true; // 元素不存在也返回true
            }
        });

        return true;
    } catch (Exception e) {
        return false;
    }
}

总结

解决好上面的三点之后,Geetest验证基本就破解掉了,但是要爬取企业信用信息公示系统中的数据还有一些事情要做。目前工商局有多个地区系统,每个系统的页面解析处理也不一样。某些地区系统甚至还针对访问频次做了控制,你可以通过 IP 代理来解决。

完整的代码示例请参考: https://github.com/aqlu/geetest-crack

运行效果:效果图

本文的参考资料:http://blog.csdn.net/paololiu/article/details/52514504


   转载规则


《Geetest拖拽验证码破解思路(java)》 Angus_Lu 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
MacOS下基于Python2.7安装OpenCV3 MacOS下基于Python2.7安装OpenCV3
步骤概览安装Xcode以及Apple Command Line Tools安装Homebrew创建Python虚拟环境安装NumPy安装OpenCV安装Xcode以及Apple Command Line Tools从App Store下载并
2017-12-13 09:28:37
下一篇 
MacOS科学上网 MacOS科学上网
本文将介绍如何在MacOS上采用VMess协议与墙外VPS通信,其中主要用到了v2ray这款开源工具。服务端安装(Centos)因为我选择的VPS的Centos的操作系统,所以此章节主要基于Centos操作系统来描述。安装包下载:wget
2017-11-21 15:07:27
  目录