项目中有个需求希望能获取公司的工商注册信息,刚开始是想与第三方数据公司合作,因种 种原因合作没有达成。于是想做个爬虫直接从工商局的《企业信用信息公示系统》中获取。 要想从《企业信用信息公示系统》中爬取数据,首先必须解决掉Geetest验证码的问题。 经过一系列的摸索,发现要想破解geetest的验证码,主要需要解决如下几个问题:
- 背景图的还原
- 找到背景图中缺口的位置
- 将滑块拖拽到缺口
背景图的还原
Geetest背景图分为两张,一张是完整背景图,一张是带缺口的背景图。每张图片被分成 52 份, 上下两部分各 26 份,然后乱序排列。在网页上显示时,是通过css来将乱序的 碎片重新组列成完整的图片。
乱序图片:
正常图片:
因此,我们必须将原始的背景乱序图片切根据页面上的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



 
 