之前有寫過關於puppeteer的相關文章
前一段時間,LZ又接到一個需求,要爬取某快遞公司網站的訂單資料,起初覺得不就是爬一下資料嘛,雖然nodejs玩的不是特別溜,但爬一些簡單資料還是難不倒我這種戰五渣的。
當我開啟網站,輸入資料,準備來一波頁面結構分析的時候,突然間跳出來一個滑塊驗證碼。臥槽......
WTF,你讓我爬個鳥啊.....
盯著滑塊驗證碼瞅了兩天,終於我得出一個結論 滑塊驗證碼阻止了人類文明的進步
!
每天早上產品色笑眯眯來問我進度的時候,我的內心都是崩潰的
難受歸難受,但業務還是要做的。最後,我想到了之前用puppeteer開發的模擬cas(單點登入)來解決我司某些應用在開發、測試環境自動登入的功能。現在我就以一種情況為例,來看下怎麼用node+puppeteer高效的破解滑塊驗證碼。
之前有一兄弟在掘金上寫過用puppeteer破解滑塊驗證碼, 接下來我們就用一些另外的思路去破解
這裡我們也以前端網為例:
const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const pixels = require("image-pixels");
const resemble = require("resemblejs");
let page = null;
const bgImg = path.resolve(__dirname, "bg.png");
const fullbgImg = path.resolve(__dirname, "fullbg.png");
async function run() {
const browser = await puppeteer.launch({
headless: false
});
page = await browser.newPage();
// 開啟前端網
await page.goto("https://www.qdfuns.com/");
await page.waitForSelector(".hand");
await page.click("a[data-type=login]");
const geetest_btn = ".geetest_btn";
await page.waitForSelector(geetest_btn);
await page.click(geetest_btn);
await page.waitFor(1000);
// 獲取滑動距離
async function getDistance() {
// 獲取canvas
let { bg, fullbg } = await page.evaluate(() => {
const fullbg = document.querySelector(".geetest_canvas_fullbg");
const bg = document.querySelector(".geetest_canvas_bg");
return {
bg: bg.toDataURL(),
fullbg: fullbg.toDataURL()
};
});
bg = bg.replace(/^data:image\/\w+;base64,/, "");
fullbg = fullbg.replace(/^data:image\/\w+;base64,/, "");
var bgDataBuffer = new Buffer(bg, "base64");
var fullbgDataBuffer = new Buffer(fullbg, "base64");
fs.writeFileSync(bgImg, bgDataBuffer);
fs.writeFileSync(fullbgImg, fullbgDataBuffer);
// 通過resemble比較背景圖和缺口圖的不同
resemble(bgImg)
.compareTo(fullbgImg)
.ignoreColors()
.onComplete(async function(data) {
fs.writeFileSync(path.resolve(__dirname, `diff.png`), data.getBuffer());
});
var { data } = await pixels(path.resolve(__dirname, `diff.png`), {
cache: false
});
// 獲取缺口距離左邊的做小位置,即計為需要滑動的距離
let arr = [];
for (let i = 10; i < 150; i++) {
for (let j = 80; j < 220; j++) {
var p = 260 * i + j;
p = p << 2;
if (data[p] === 255 && data[p + 1] === 0 && data[p + 2] === 255) {
arr.push(j);
break;
}
}
}
return Math.min(...arr);
}
const distance = await getDistance();
const button = await page.$(".geetest_slider_button");
const box = await button.boundingBox();
const axleX = Math.floor(box.x + box.width / 2);
const axleY = Math.floor(box.y + box.height / 2);
await btnSlider(distance);
// 滑動滑塊
async function btnSlider(distance) {
await page.mouse.move(axleX, axleY);
await page.mouse.down();
await page.waitFor(200);
await page.mouse.move(box.x + distance / 4, axleY, { steps: 20 });
await page.waitFor(200);
await page.mouse.move(box.x + distance / 3, axleY, { steps: 18 });
await page.waitFor(350);
await page.mouse.move(box.x + distance / 2, axleY, { steps: 15 });
await page.waitFor(400);
await page.mouse.move(box.x + (distance / 3) * 2, axleY, { steps: 15 });
await page.waitFor(350);
await page.mouse.move(box.x + (distance / 4) * 3, axleY, { steps: 10 });
await page.waitFor(350);
await page.mouse.move(box.x + distance + 30, axleY, { steps: 10 });
await page.waitFor(300);
await page.mouse.up();
await page.waitFor(1000);
const text = await page.evaluate(() => {
return document.querySelector(".geetest_result_box").innerText;
});
console.log(text);
let step = 0;
if (text) {
// 如果失敗重新獲取滑塊
if (
text.includes("怪物吃了拼圖") ||
text.includes("拖動滑塊將懸浮影像正確拼合")
) {
await page.waitFor(2000);
await page.click(".geetest_refresh_1");
await page.waitFor(1000);
step = await getDistance();
await btnSlider(step);
} else if (text.includes("速度超過")) {
console.log("success");
}
}
}
}
run();
複製程式碼
執行該程式,控制檯輸出如下(運氣好的話,可能一次就過了,具體要看中間的處理過程怎麼優化求解)
這裡面,需要注意以下幾點
缺口圖存在干擾缺口圖,resemble在比對的時候需要會得到兩個缺口,這裡目前沒有一個很好的辦法來確定到底哪個缺口是我們所需要的(下面我們會提到一個針對該問題的方法來避免該干擾項)
滑動的時候需要控制下滑動速度,具體怎麼個滑動法,那就仁者見仁智者見智了
你以為這樣就結束了
很多情況下滑塊驗證碼並不會給我們完整的背景圖,這時候我們該怎麼有效的去定位缺口呢,在這裡我們可以使用gm 把我們的背景圖片模糊以下,然後在用resemblejs
去比對下兩個圖片,但是此時圖片會有很多地方比對出不同,此時我們可以獲取到小滑塊圖片距離父輩元素的位置,藉此來減少畫素比對範圍(這可以有效解決我們上面所提到的避免干擾項問題)
const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const pixels = require("image-pixels");
const resemble = require("resemblejs");
const gm = require("gm");
let page = null;
const bgImg = path.resolve(__dirname, "bg.png");
const bgBlurImg = path.resolve(__dirname, "bgBlur.png");
const bgDiffImg = path.resolve(__dirname, "bgDiff.png");
async function run() {
const browser = await puppeteer.launch({
headless: false
});
page = await browser.newPage();
await page.goto(
"https://x.tongdun.cn/onlineExperience/slidingPuzzle?source=baidu&plan=%E5%8F%8D%E6%AC%BA%E8%AF%88&unit=%E6%99%BA%E8%83%BD%E9%AA%8C%E8%AF%81&keyword=%E6%99%BA%E8%83%BD%E9%AA%8C%E8%AF%81%E7%A0%81&e_creative=24659987438&e_adposition=cl1&e_keywordid=101045415224&e_keywordid2=101045415224&audience=236369"
);
await page.waitForSelector("#loginBtn");
await page.click("#loginBtn");
const slidetrigger = ".td-pop-slidetrigger";
await page.waitForSelector(slidetrigger);
await page.click(slidetrigger);
await page.waitFor(1000);
const slideIdentity = ".td-pop-slide-identity";
await page.waitFor(slideIdentity);
// 獲取小滑塊的top值,來減少比對範圍
const top = await page.evaluate(() => {
const identity = document.querySelector(".td-pop-slide-identity");
return identity.offsetTop;
});
async function getDistance() {
// 獲取缺口圖片
let { bg } = await page.evaluate(() => {
const bg = document.querySelector(".td-bg-img");
return {
bg: bg.toDataURL()
};
});
bg = bg.replace(/^data:image\/\w+;base64,/, "");
var bgDataBuffer = new Buffer(bg, "base64");
fs.writeFileSync(bgImg, bgDataBuffer);
// 圖片模糊
gm(bgImg)
.blur(1)
.write(bgBlurImg, function(err) {
if (!err) console.log("done");
});
// 圖片對比
resemble(bgImg)
.compareTo(bgBlurImg)
.ignoreColors()
.onComplete(async function(data) {
fs.writeFileSync(bgDiffImg, data.getBuffer());
});
var { data } = await pixels(bgDiffImg, {
cache: false
});
let arr = [];
// 比對範圍內的畫素點
for (let i = top; i < top + 44; i++) {
for (let j = 60; j < 320; j++) {
var p = 320 * i + j;
p = p << 2;
if (data[p] === 255 && data[p + 1] === 0 && data[p + 2] === 255) {
arr.push(j);
break;
}
}
}
const { maxStr } = getMoreNum(arr);
return Number(maxStr);
}
const distance = await getDistance();
const button = await page.$(slidetrigger);
const box = await button.boundingBox();
const axleX = Math.floor(box.x + box.width / 2);
const axleY = Math.floor(box.y + box.height / 2);
console.log(distance, "distance");
console.log(box.x + distance);
await btnSlider(distance);
async function btnSlider(distance) {
await page.mouse.move(axleX, axleY);
await page.mouse.down();
await page.waitFor(200);
await page.mouse.move(box.x + distance / 4, axleY, { steps: 20 });
await page.waitFor(200);
await page.mouse.move(box.x + distance / 3, axleY, { steps: 18 });
await page.waitFor(350);
await page.mouse.move(box.x + distance / 2, axleY, { steps: 15 });
await page.waitFor(400);
await page.mouse.move(box.x + (distance / 3) * 2, axleY, { steps: 15 });
await page.waitFor(350);
await page.mouse.move(box.x + (distance / 4) * 3, axleY, { steps: 10 });
await page.waitFor(350);
await page.mouse.move(box.x + distance + 20, axleY, { steps: 10 });
await page.waitFor(300);
await page.mouse.up();
await page.waitFor(1000);
}
}
run();
function getMoreNum(arr) {
var obj = {};
var arr1 = [];
for (var i = 0; i < arr.length; i++) {
if (arr1.indexOf(arr[i]) == -1) {
obj[arr[i]] = 1;
arr1.push(arr[i]);
} else {
obj[arr[i]]++;
}
}
var max = 0;
var maxStr;
for (var i in obj) {
if (max < obj[i]) {
max = obj[i];
maxStr = i;
}
}
return { max, maxStr };
}
複製程式碼
該示例沒新增錯誤之後重滑邏輯
此種方法存在的問題
自己模糊化背景圖片就行畫素比較,成功率較低,需優化(亦可以通過比對的圖片通過其灰度值來鎖定區域)
以上,我們介紹了兩種方法來破解解決滑塊驗證碼。此外,LZ還嘗試了使用圖片二值化方法來進行圖片缺口的定位,該方法的成功率遠高於第二種方法,具體實現方法就不寫了,讀者可以自行探索哈。
示例程式碼均可在 github檢視