项目中有个需求希望能获取公司的工商注册信息,刚开始是想与第三方数据公司合作,因种 种原因合作没有达成。于是想做个爬虫直接从工商局的《企业信用信息公示系统》中获取。 要想从《企业信用信息公示系统》中爬取数据,首先必须解决掉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