之前看過一篇腦洞大開的文章,介紹了各個大廠的前端反爬蟲技巧,但也正如此文所說,沒有100%的反爬蟲方法,本文介紹一種簡單的方法,來繞過所有這些前端反爬蟲手段。
下面的程式碼以百度指數為例,程式碼已經封裝成一個百度指數爬蟲node庫: https://github.com/Coffcer/baidu-index-spider
note: 請勿濫用爬蟲給他人添麻煩
百度指數的反爬蟲策略
觀察百度指數的介面,指數資料是一個趨勢圖,當滑鼠懸浮在某一天的時候,會觸發兩個請求,將結果顯示在懸浮框裡面:
按照常規思路,我們先看下這個請求的內容:
請求 1:
請求 2:
可以發現,百度指數實際上在前端做了一定的反爬蟲策略。當滑鼠移動到圖表上時,會觸發兩個請求,一個請求返回一段html,一個請求返回一張生成的圖片。html中並不包含實際數值,而是通過設定width和margin-left,來顯示圖片上的對應字元。並且請求引數上帶有res、res1這種我們不知如何模擬的引數,所以用常規的模擬請求或者html爬取的方式,都很難爬到百度指數的資料。
爬蟲思路
怎麼突破百度這種反爬蟲方法呢,其實也很簡單,就是完全不去管他是如何反爬蟲的。我們只需模擬使用者操作,將需要的數值截圖下來,做影象識別就行。步驟大概是:
- 模擬登入
- 開啟指數頁面
- 滑鼠移動到指定日期
- 等待請求結束,擷取數值部分的圖片
- 影象識別得到值
- 迴圈第3~5步,就得到每一個日期對應的值
這種方法理論上能爬任何網站的內容,接下來我們來一步步實現爬蟲,下面會用到的庫:
- puppeteer 模擬瀏覽器操作
- node-tesseract tesseract的封裝,用來做影象識別
- jimp 圖片裁剪
安裝 Puppeteer, 模擬使用者操作
Puppeteer是Google Chrome團隊出品的Chrome自動化工具,用來控制Chrome執行命令。可以模擬使用者操作,做自動化測試、爬蟲等。用法非常簡單,網上有不少入門教程,順著本文看完也大概可以知道如何使用。
API文件: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md
安裝:
npm install --save puppeteer
複製程式碼
Puppeteer在安裝時會自動下載Chromium,以確保可以正常執行。但是國內網路不一定能成功下載Chromium,如果下載失敗,可以使用cnpm來安裝,或者將下載地址改成淘寶的映象,然後再安裝:
npm config set PUPPETEER_DOWNLOAD_HOST=https://npm.taobao.org/mirrors
npm install --save puppeteer
複製程式碼
你也可以在安裝時跳過Chromium下載,通過程式碼指定本機Chrome路徑來執行:
// npm
npm install --save puppeteer --ignore-scripts
// node
puppeteer.launch({ executablePath: '/path/to/Chrome' });
複製程式碼
實現
為版面整潔,下面只列出了主要部分,程式碼涉及到selector的部分都用了...代替,完整程式碼參看文章頂部的github倉庫。
開啟百度指數頁面,模擬登入
這裡做的就是模擬使用者操作,一步步點選和輸入。沒有處理登入驗證碼的情況,處理驗證碼又是另一個話題了,如果你在本機登入過百度,一般不需要驗證碼。
// 啟動瀏覽器,
// headless引數如果設定為true,Puppeteer將在後臺操作你Chromium,換言之你將看不到瀏覽器的操作過程
// 設為false則相反,會在你電腦上開啟瀏覽器,顯示瀏覽器每一操作。
const browser = await puppeteer.launch({headless:false});
const page = await browser.newPage();
// 開啟百度指數
await page.goto(BAIDU_INDEX_URL);
// 模擬登陸
await page.click('...');
await page.waitForSelecto('...');
// 輸入百度賬號密碼然後登入
await page.type('...','username');
await page.type('...','password');
await page.click('...');
await page.waitForNavigation();
console.log('✅ 登入成功');
複製程式碼
模擬移動滑鼠,獲取需要的資料
需要將頁面滾動到趨勢圖的區域,然後移動滑鼠到某個日期上,等待請求結束,tooltip顯示數值,再截圖儲存圖片。
// 獲取chart第一天的座標
const position = await page.evaluate(() => {
const $image = document.querySelector('...');
const $area = document.querySelector('...');
const areaRect = $area.getBoundingClientRect();
const imageRect = $image.getBoundingClientRect();
// 滾動到圖表視覺化區域
window.scrollBy(0, areaRect.top);
return { x: imageRect.x, y: 200 };
});
// 移動滑鼠,觸發tooltip
await page.mouse.move(position.x, position.y);
await page.waitForSelector('...');
// 獲取tooltip資訊
const tooltipInfo = await page.evaluate(() => {
const $tooltip = document.querySelector('...');
const $title = $tooltip.querySelector('...');
const $value = $tooltip.querySelector('...');
const valueRect = $value.getBoundingClientRect();
const padding = 5;
return {
title: $title.textContent.split(' ')[0],
x: valueRect.x - padding,
y: valueRect.y,
width: valueRect.width + padding * 2,
height: valueRect.height
}
});
複製程式碼
截圖
計算數值的座標,截圖並用jimp對裁剪圖片。
await page.screenshot({ path: imgPath });
// 對圖片進行裁剪,只保留數字部分
const img = await jimp.read(imgPath);
await img.crop(tooltipInfo.x, tooltipInfo.y, tooltipInfo.width, tooltipInfo.height);
// 將圖片放大一些,識別準確率會有提升
await img.scale(5);
await img.write(imgPath);
複製程式碼
影象識別
這裡我們用Tesseract來做影象識別,Tesseracts是Google開源的一款OCR工具,用來識別圖片中的文字,並且可以通過訓練提高準確率。github上已經有一個簡單的node封裝: node-tesseract,需要你先安裝Tesseract並設定到環境變數。
Tesseract.process(imgPath, (err, val) => {
if (err || val == null) {
console.error('❌ 識別失敗:' + imgPath);
return;
}
console.log(val);
複製程式碼
實際上未經訓練的Tesseracts識別起來會有少數幾個錯誤,比如把9開頭的數字識別成`3,這裡需要通過訓練去提升Tesseracts的準確率,如果識別過程出現的問題都是一樣的,也可以簡單通過正則去修復這些問題。
封裝
實現了以上幾點後,只需組合起來就可以封裝成一個百度指數爬蟲node庫。當然還有許多優化的方法,比如批量爬取,指定天數爬取等,只要在這個基礎上實現都不難了。
const recognition = require('./src/recognition');
const Spider = require('./src/spider');
module.exports = {
async run (word, options, puppeteerOptions = { headless: true }) {
const spider = new Spider({
imgDir,
...options
}, puppeteerOptions);
// 抓取資料
await spider.run(word);
// 讀取抓取到的截圖,做影象識別
const wordDir = path.resolve(imgDir, word);
const imgNames = fs.readdirSync(wordDir);
const result = [];
imgNames = imgNames.filter(item => path.extname(item) === '.png');
for (let i = 0; i < imgNames.length; i++) {
const imgPath = path.resolve(wordDir, imgNames[i]);
const val = await recognition.run(imgPath);
result.push(val);
}
return result;
}
}
複製程式碼
反爬蟲
最後,如何抵擋這種爬蟲呢,個人認為通過判斷滑鼠移動軌跡可能是一種方法。當然前端沒有100%的反爬蟲手段,我們能做的只是給爬蟲增加一點難度。