背景
最近由于需要爬取一些数据,但是这个数据必须在登陆状态下才能得到,调研了很多爬虫的反爬技术的攻防,发现采用一些比较底层的爬虫框架虽然速度更快扩展性更好,但是成本比较高,目标网站任何改动都可以让整个爬虫崩溃,因此需要花大力气去维护,但是我的需求并不是大量数据,而是 “爬下来”,所以最后选择了selenium去进行爬取,理论上所见即所得,即所爬。
关于验证码
在登陆的时候,发现需要进行一个验证码的验证,验证码本质就是识别机器和人,这本身就是一个攻防战,理论上如果足够像人就够了。那剩下的就是爬虫能够如何让其更加的像人,这个话题就比较玄学了,所以验证码设计与爬虫这个猫鼠游戏其实挺有意思的。
现在常见的验证码,主要分为,图片字幕,鼠标轨迹捕捉,文字组合,以及12306这种反人类的知识认知解答等等,以至于连真人都很难识别,由于12306的垄断性质所以其可以做的很极端,但是即使是这样,也催生了一个特殊的职业,人工打码,雇佣真人来进行识别,据我所知网上所谓的一些在家点点鼠标就能赚钱的工作一部分就是这个事情。
大部分互联网公司更倾向于在用户体验和验证码识别机器与人类之间做平衡,毕竟互联网公平竞争的时代,用户体验就是王道。这次需要破解就是滑动验证码。
就是这样一个验证码。
思路整理
这个验证码它是如何进行识别的呢,就是按住下面的滑条,移动图片中的滑块与背景图中的凹陷进行匹配,匹配成完整的图片,不仅如此,整个滑动的操作本身也必须是符合正常人的一个操作,例如滑动不应该是瞬间匹配,而是先快到缺块附近,再慢慢进行精准匹配,这才是一个正常人类。
技术分析
-
竟然要模拟滑动,首先selenium本身是支持按住鼠标滑动并松开鼠标的操作的,这个可以查阅selenium文档即可。
-
拉动滑条的长度,需要分析整个图片缺陷的位置,并且这个像素的长度不简单就是滑条的长度,而是一个比例缩放的过程,因此第一步应该是计算出凹陷占整个图片的比例,然后再根据滑条的像素长度 乘以 比例得到最终滑动。公式:滑动比例 * 滑条长度 = 滑动像素长度。
-
如何分析图片凹陷位置呢?竟然要分析图片,肯定先获取到图片,selenium path从页面抓取验证码图片,因为图片本身是由像素点构成,所以打算从分析每个像素点的特征着手,在我看来图片就是个二维数组,每个点就是一个RGB值,应该有某种方式去解析图片成这个RGB值的二维数组。于是我去开源库寻找,后来发现JDK自带就可以解析图片。
-
解析好了图片成二维数组,再去分析图片特征,发现只要分析这两个边的位置,其实就可以求平均,找到凹陷的正中点了,这两条竖直的白线有什么特点呢,提到“白”,肯定RGB有一定的特征。根据这个特征识别出来这些点,并通过计算得到这两个竖线的横坐标。
-
然后计算出位置在整个图片长度的比例,再通过selenium拿到滑动条的长度,即可计算出滑动的长度。
-
模拟滑动,这个过程模拟人类滑动,不能简单的匀速,而是要随机数散列,动态剩余长度随机数滑动,剩余的长度越小,随机数的范围也就越小,因此达到一种速度放慢的效果,欺骗检测。
代码实战
public boolean skipRobotTest() {
try {
// 下载图片
ImageDownload imageDownload = new ImageDownload();
WebElement img = getRobotTestImage();
String imgUrl = img.getAttribute("src");
imageDownload.dowloadImage(imgUrl, CURRENT_ROBOT_TEST_IMAGE);
// 获取比例
double slideRate = getSlideRate(CURRENT_ROBOT_TEST_IMAGE);
// 滑动滑条
moveButton(slideRate);
} catch (Throwable e) {
return true;
}
return false;
}
为了保证破解验证码的稳定性,避免一次破解程序崩溃的尴尬,我们可以简单在调用外层写一个重试的逻辑:
while (!skipRobotTest()) {
LogUtils.print("skip robot will retry");
continue;
}
LogUtils.print("login success");
接下来是精彩环节:
每个像素点RGB值其实有4个字节组成的混合值,因此对应分别获取三个点值需要进行位运算。
private double getSlideRate(String imageName) {
try {
BufferedImage image = ImageIO.read(new File(ChromeSupport.INS_PATH + imageName + ".jpeg"));
int width = image.getWidth();
int height = image.getHeight();
// 标记较白的点为凹陷轮廓边缘点
List<Integer> widthEdgeList = Lists.newArrayList();
for (int i = 0;i < width;i ++) {
for (int j = 0; j < height; j++) {
// 这里分别获取RGB值的,红,绿,蓝 的值
int rgb = image.getRGB(i, j);
int redV = (rgb & 0xff0000) >> 16;
int greenV = (rgb & 0xff00) >> 8;
int blueV = (rgb & 0xff);
// 分析发现,数值越大,越接近白色,因此这里分别判断三个值,达到230即可标记为一个较白的点
if (redV > 230 && greenV > 230 && blueV > 230) {
widthEdgeList.add(i);
}
}
}
Map<Integer, List<Integer>> map = widthEdgeList.stream().collect(Collectors.groupingBy(Integer::intValue));
ArrayList<List<Integer>> lists = Lists.newArrayList(map.values());
Collections.sort(lists, (o1, o2) -> {
if (o1.size() > o2.size()) {
return -1;
} else if (o1.size() < o2.size()) {
return 1;
}
return 0;
});
// 左竖线横坐标
int leftEdge = lists.get(0).get(0);
// 右竖线横坐标
int rightEdge = lists.get(1).get(0);
// 得到凹陷正中央
int slidePixel = (rightEdge + leftEdge) / 2;
// 滑动比例
double rate = Double.valueOf(slidePixel) / Double.valueOf(width);
// 柔性调整
return zoomRate(rate);
} catch (IOException e) {
LogUtils.errorPrint(e, "get kill robot rate error");
}
return 0;
}
关于柔性调整,最终实验发现光滑动条*比例还不够,验证码会在越偏离整个背景图片的正中央位置进行放大比例,这个柔性比例就是在比例在某个区间的时候去进行同步放大一定的比例,去抵消被放大的比例。这个方法很简单粗暴,就是if else去调整,根据识别的成功率逐渐完善:
private double zoomRate(double rate) {
double originRate = rate;
if (rate < 0.45) {
rate -= 0.02;
}
if (rate >= 0.45 && rate < 0.6) {
return rate;
} else if (rate >= 0.6 && rate < 0.67) {
rate += 0.02;
} else if (rate >= 0.67 && rate < 0.75){
rate += 0.02;
} else if (rate >= 0.75 && rate < 0.8){
rate += 0.05;
} else {
rate += 0.07;
}
LogUtils.print("kill robot slide rate %s, zoom rate %s", originRate, rate);
return rate;
}
准备好了所有数据,然后开始滑动操作:
private void moveButton(double slideRate) {
// 获取滑动条点击样式元素
WebElement moveButtonEl = webDriver.findElement(By.xpath("xxxxxxx"));
Actions moveAction = new Actions(webDriver);
moveAction.clickAndHold(moveButtonEl);
// 540为滑动条的全部长度,随机滑动步数
int targetMoveCount = (int) (540 * slideRate);
// getRandomStep获得随机长度移动数组
for (Integer count : getRandomStep(targetMoveCount)) {
moveAction.moveByOffset(count, 0);
moveAction.perform();
}
moveAction.release(moveButtonEl).perform();
}
随机长度生成规则:
public List<Integer> getRandomStep(int targetMoveCount) {
List<Integer> list = Lists.newArrayList();
while (targetMoveCount > 0) {
// 剩余长度生成随机数
int count = RandomUtils.nextInt(0, targetMoveCount + 1);
list.add(count);
// 得到剩余长度
targetMoveCount -= count;
}
return list;
}
完成
总结
由于涉及目标网站,所以这里就不展示效果,得到这样的优化之后,成功率基本达到100%,整个过程学习到不少图片识别OCR相关的知识,也熟悉了爬虫框架,甚至了解了人体行为学的东西,还是挺好玩的。