背景
最近由于需要爬取一些資料,但是這個資料必須在登陸狀态下才能得到,調研了很多爬蟲的反爬技術的攻防,發現采用一些比較底層的爬蟲架構雖然速度更快擴充性更好,但是成本比較高,目标網站任何改動都可以讓整個爬蟲崩潰,是以需要花大力氣去維護,但是我的需求并不是大量資料,而是 “爬下來”,是以最後選擇了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去調整,根據識别的成功率逐漸完善:
/**
* 發現會根據50%的偏移量需要增量
* @param rate
* @return
*/
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相關的知識,也熟悉了爬蟲架構,甚至了解了人體行為學的東西,還是挺好玩的。