Puppeteer爬蟲實戰(三)

戴箍的三佬發表於2020-07-21

本篇文章針對大家熟知的技術站點作為目標進行技術實踐。

確定需求

  訪問目標網站並按照篩選條件(關鍵詞、日期、作者)進行檢索並獲取返回資料中的目標資料。進行技術拆分如下:

  1. 開啟目標網站
  2. 找到輸入框元素輸入關鍵詞,找到日期元素設定日期,找到搜尋按鈕觸發搜尋動作
  3. 解析搜尋返回的html元素構造目標資料
  4. 將目標資料儲存

編寫程式碼

'use strict';
const puppeteer = require('puppeteer');
const csv = require('fast-csv');
const fs = require('fs');

(async () => {
  const startUrl = 'https://www.infoq.cn/';
  const keyWord = 'CQRS';
  const browser = await puppeteer.launch({
    slowMo: 100, // 放慢速度
    headless: false, // 是否有頭
    defaultViewport: {// 介面設定
      width: 1820,
      height: 1080,
    },
    ignoreHTTPSErrors: false, // 忽略 https 報錯
    args: ['--start-maximized', '--no-sandbox', '--disable-setuid-sandbox'],
  });

  const page = await browser.newPage();
  await page.goto(startUrl).catch(error => console.log(error));
  await page.waitFor(1 * 1000);
  await page.click('.search,.iconfont');
  await page.type('.search-input', keyWord, { delay: 100 });
  const newPagePromise = new Promise(x => browser.once('targetcreated', target => x(target.page())));
  await page.click('.search,.iconfont');
  const targetPage = await newPagePromise;
  const dataCount = await targetPage.$eval('.search-body-main-tips span', el => el && el.innerHTML).catch(error => console.error(error));
  if (dataCount && dataCount > 0) {
    const dataEle = await targetPage.$$('.search-item');
    console.log(dataEle.length);
    const stream = fs.createWriteStream('infoq.csv');
    const csvStream = csv.format({ headers: true });
    csvStream.pipe(stream).on('end', process.exit);
    for (let index = 0; index < dataEle.length; index++) {
      const element = dataEle[index];
      const title = await element.$eval('a', el => el && el.innerHTML).catch(error => console.error(error))
      const desc = await element.$eval('.desc', el => el && el.innerHTML).catch(error => console.error(error))
      csvStream.write({
        標題: title || '',
        摘要: desc || '',
      });
    }
    csvStream.end(() => { console.log('寫入完畢'); });
  }
  await targetPage.screenshot({ path: 'infoq.png' });
  await browser.close();
})();

具體的如下

視訊

總結

  上面的例子還是比較簡單的,站點本身是資訊站(其實有搜尋介面根本不需要解析html?),例子是一個簡單的爬蟲流程,方便了解puppeteer的能力,下面我也總結一下工作中和自己專案的實際情況。

  1. 上述例子還不能算是一個完整的應用,根據主要業務分析實際應用大概是這個樣子: 爬蟲的主要業務變化部分有目標站點、篩選條件、站點操作、資料解析、資料落地,這些都可以通過程式碼搞定,也就是程式碼是變化的,所以一個可用的爬蟲的應用是程式碼是可動態調整的,根據上面的動態點將一個爬蟲業務抽象成一個Task,
  • 它要具備一個引數輸入介面(動態輸入目標站點、篩選條件等指令碼所需引數),每個task的業務不一樣,指令碼引數不一樣,就需要一個動態表單可配置指令碼所需引數(Ant design搞一個也夠用了);
  • 站點操作會根據實際情況而希望能夠動態調整指令碼程式碼,那就引入一個線上的vscode編輯器(實際開發中通常都是測試好的指令碼才會寫進去,這個地方引入有些牽強,主要是線上發現一些小bug需要快速解決);
  • 資料解析完了後需要落地,這包括儲存到本地、推到api介面、或是下載檔案成功推送標識等皆可動態引數和指令碼實現;
  1. Task主要流程還有一個重要的點就是觸發需要一個定時任務方便使用者設定各種週期性的需求,不管是考慮使用者需求還是躲避爬蟲頻率限制都很有必要。
  2. Task主要流程完成後還有很重要的基礎設施:
  • 應用快速部署,當然要用到docker了。因為puppeteer其實是依賴了Chromium的能力,所以你需要在容器裡部署一套可執行的chromium,這裡面牽扯到字型、環境問題等麻煩事情,這裡我推薦一個使用的映象browserless/chrome,這個映象大家可以根據自己需求定製,視訊中的介面演示即FullHead,容器環境往往不是完整的作業系統(應該沒人用完整的windows環境吧)而是儘量小的linux版本(我本人使用的是egg引入puppeteer),那麼就不可能有介面,即無法實現FullHead,FullHead可以幫我們規避一些站點(部分站點檢測手段比較強)的反爬手段,這裡使用的是xvfb實現了FullHead。
  • 網路檢測,無論是切換容器網路還是在應用中使用ip代理我都沒搞過就不說了。分散式爬蟲我目前沒實現所以這裡就不說了。
  1. 實際使用中遇到的一些問題,資訊站渲染的比較隨意,解析比較複雜,有些資料只是展示而無固定標籤或規律展示,puppeteer的元素選擇器有css和xpath,但是這個和大家平常用jquery不太一樣,最好去看看標準,它的xpath解析就不完整(比如string(.),因為puppeteer要包返回裝型別所以就完蛋了,我提了issue,答覆目前也只能遍歷了,或者有誰知道可以提醒我下)。網路問題,訪問時渲染有時候會超時這個要自己根據需求搞了。

大致就到這吧。

相關文章