Node: Puppeteer + 影象識別 實現百度指數爬蟲

島書Z發表於2018-02-13

之前看過一篇腦洞大開的文章,介紹了各個大廠的前端反爬蟲技巧,但也正如此文所說,沒有100%的反爬蟲方法,本文介紹一種簡單的方法,來繞過所有這些前端反爬蟲手段。

下面的程式碼以百度指數為例,程式碼已經封裝成一個百度指數爬蟲node庫: https://github.com/Coffcer/baidu-index-spider

note: 請勿濫用爬蟲給他人添麻煩

百度指數的反爬蟲策略

觀察百度指數的介面,指數資料是一個趨勢圖,當滑鼠懸浮在某一天的時候,會觸發兩個請求,將結果顯示在懸浮框裡面:

Node: Puppeteer + 影象識別 實現百度指數爬蟲

按照常規思路,我們先看下這個請求的內容:

請求 1:

Node: Puppeteer + 影象識別 實現百度指數爬蟲
Node: Puppeteer + 影象識別 實現百度指數爬蟲

請求 2:

Node: Puppeteer + 影象識別 實現百度指數爬蟲

可以發現,百度指數實際上在前端做了一定的反爬蟲策略。當滑鼠移動到圖表上時,會觸發兩個請求,一個請求返回一段html,一個請求返回一張生成的圖片。html中並不包含實際數值,而是通過設定width和margin-left,來顯示圖片上的對應字元。並且請求引數上帶有res、res1這種我們不知如何模擬的引數,所以用常規的模擬請求或者html爬取的方式,都很難爬到百度指數的資料。

爬蟲思路

怎麼突破百度這種反爬蟲方法呢,其實也很簡單,就是完全不去管他是如何反爬蟲的。我們只需模擬使用者操作,將需要的數值截圖下來,做影象識別就行。步驟大概是:

  1. 模擬登入
  2. 開啟指數頁面
  3. 滑鼠移動到指定日期
  4. 等待請求結束,擷取數值部分的圖片
  5. 影象識別得到值
  6. 迴圈第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%的反爬蟲手段,我們能做的只是給爬蟲增加一點難度。

相關文章