手把手教你如何用Crawlab構建技術文章聚合平臺(一)

MarvinZhang發表於2019-03-15

背景

說到爬蟲,大多數程式設計師想到的是scrapy這樣受人歡迎的框架。scrapy的確不錯,而且有很強大的生態圈,有gerapy等優秀的視覺化介面。但是,它還是有一些不能做到的事情,例如在頁面上做翻頁點選操作、移動端抓取等等。對於這些新的需求,可以用Selenium、Puppeteer、Appium這些自動化測試框架繞開繁瑣的動態內容,直接模擬使用者操作進行抓取。可惜的是,這些框架不是專門的爬蟲框架,不能對爬蟲進行集中管理,因此對於一個多達數十個爬蟲的大型專案來說有些棘手。

Crawlab是一個基於Celery的分散式通用爬蟲管理平臺,擅長將不同程式語言編寫的爬蟲整合在一處,方便監控和管理。Crawlab有精美的視覺化介面,能對多個爬蟲進行執行和管理。任務排程引擎是本身支援分散式架構的Celery,因此Crawlab可以天然整合分散式爬蟲。有一些朋友認為Crawlab只是一個任務排程引擎,其實這樣認為並不完全正確。Crawlab是類似Gerapy這樣的專注於爬蟲的管理平臺。

本文將介紹如何使用Crawlab和Puppeteer抓取主流的技術部落格文章,然後用Flask+Vue搭建一個小型的技術文章聚合平臺。

Crawlab

在前一篇文章《分散式通用爬蟲管理平臺Crawlab》已介紹了Crawlab的架構以及安裝使用,這裡快速介紹一下如何安裝、執行、使用Crawlab。

安裝

到Crawlab的Github Repo用克隆一份到本地。

git clone https://github.com/tikazyq/crawlab
複製程式碼

安裝相應的依賴包和庫。

cd crawlab

# 安裝python依賴
pip install -r crawlab/requirements

# 安裝前端依賴
cd frontend
npm install
複製程式碼

安裝mongodb和redis-server。Crawlab將用MongoDB作為結果集以及執行操作的儲存方式,Redis作為Celery的任務佇列,因此需要安裝這兩個資料庫。

執行

在執行之前需要對Crawlab進行一些配置,配置檔案為config.py

# project variables
PROJECT_SOURCE_FILE_FOLDER = '/Users/yeqing/projects/crawlab/spiders' # 爬蟲原始碼根目錄
PROJECT_DEPLOY_FILE_FOLDER = '/var/crawlab'  # 爬蟲部署根目錄
PROJECT_LOGS_FOLDER = '/var/logs/crawlab'  # 日誌目錄
PROJECT_TMP_FOLDER = '/tmp'  # 臨時檔案目錄

# celery variables
BROKER_URL = 'redis://192.168.99.100:6379/0'  # 中間者URL,連線redis
CELERY_RESULT_BACKEND = 'mongodb://192.168.99.100:27017/'  # CELERY後臺URL
CELERY_MONGODB_BACKEND_SETTINGS = {
    'database': 'crawlab_test',
    'taskmeta_collection': 'tasks_celery',
}
CELERY_TIMEZONE = 'Asia/Shanghai'
CELERY_ENABLE_UTC = True

# flower variables
FLOWER_API_ENDPOINT = 'http://localhost:5555/api'  # Flower服務地址

# database variables
MONGO_HOST = '192.168.99.100'
MONGO_PORT = 27017
MONGO_DB = 'crawlab_test'

# flask variables
DEBUG = True
FLASK_HOST = '127.0.0.1'
FLASK_PORT = 8000
複製程式碼

啟動後端API,也就是一個Flask App,可以直接啟動,或者用gunicorn代替。

cd ../crawlab
python app.py
複製程式碼

啟動Flower服務(抱歉目前整合Flower到App服務中,必須單獨啟動來獲取節點資訊,後面的版本不需要這個操作)。

python ./bin/run_flower.py
複製程式碼

啟動本地Worker。在其他節點中如果想只是想執行任務的話,只需要啟動這一個服務就可以了。

python ./bin/run_worker.py
複製程式碼

啟動前端伺服器。

cd ../frontend
npm run serve
複製程式碼

使用

首頁Home中可以看到總任務數、總爬蟲數、線上節點數和總部署數,以及過去30天的任務執行數量。

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

點選側邊欄的Spiders或者上方到Spiders數,可以進入到爬蟲列表頁。

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

這些是爬蟲原始碼根目錄PROJECT_SOURCE_FILE_FOLDER下的爬蟲。Crawlab會自動掃描該目錄下的子目錄,將子目錄看作一個爬蟲。Action列下有一些操作選項,點選部署Deploy按鈕將爬蟲部署到所有線上節點中。部署成功後,點選執行Run按鈕,觸發抓取任務。這時,任務應該已經在執行了。點選側邊欄的Tasks到任務列表,可以看到已經排程過的爬蟲任務。

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

基本使用就是這些,但是Crawlab還能做到更多,大家可以進一步探索,詳情請見Github

Puppeteer

Puppeteer是谷歌開源的基於Chromium和NodeJS的自動化測試工具,可以很方便的讓程式模擬使用者的操作,對瀏覽器進行程式化控制。Puppeteer有一些常用操作,例如點選,滑鼠移動,滑動,截圖,下載檔案等等。另外,Puppeteer很類似Selenium,可以定位瀏覽器中網頁元素,將其資料抓取下來。因此,Puppeteer也成為了新的爬蟲利器。

相對於Selenium,Puppeteer是新的開源專案,而且是谷歌開發,可以使用很多新的特性。對於爬蟲來說,如果前端知識足夠的話,寫資料抓取邏輯簡直不能再簡單。正如其名字一樣,我們是在操作木偶人來幫我們抓取資料,是不是很貼切?

掘金上已經有很多關於Puppeteer的教程了(爬蟲利器 Puppeteer 實戰Puppeteer 與 Chrome Headless —— 從入門到爬蟲),這裡只簡單介紹一下Puppeteer的安裝和使用。

安裝

安裝很簡單,就一行npm install命令,npm會自動下載Chromium並安裝,這個時間會比較長。為了讓安裝好的puppeteer模組能夠被所有nodejs爬蟲所共享,我們在PROJECT_DEPLOY_FILE_FOLDER目錄下安裝node的包。

# PROJECT_DEPLOY_FILE_FOLDER變數值
cd /var/crawlab

# 安裝puppeteer
npm i puppeteer

# 安裝mongodb
npm i mongodb
複製程式碼

安裝mongodb是為了後續的資料庫操作。

使用

以下是Copy/Paste的一段用Puppeteer訪問簡書然後截圖的程式碼,非常簡潔。

const puppeteer = require('puppeteer');

(async () => {
  const browser = await (puppeteer.launch());
  const page = await browser.newPage();
  await page.goto('https://www.jianshu.com/u/40909ea33e50');
  await page.screenshot({
    path: 'jianshu.png',
    type: 'png',
    // quality: 100, 只對jpg有效
    fullPage: true,
    // 指定區域截圖,clip和fullPage兩者只能設定一個
    // clip: {
    //   x: 0,
    //   y: 0,
    //   width: 1000,
    //   height: 40
    // }
  });
  browser.close();
})();
複製程式碼

關於Puppeteer的常用操作,請移步《我常用的puppeteer爬蟲api》

編寫爬蟲

囉嗦了這麼久,終於到了萬眾期待的爬蟲時間了。Talk is cheap, show me the code!咦?我們不是已經Show了不少程式碼了麼...

由於我們的目標是建立一個技術文章聚合平臺,我們需要去各大技術網站抓取文章。資源當然是越多越好。作為展示用,我們將抓取下面幾個具有代表性的網站:

  • 掘金
  • SegmentFault
  • CSDN

研究發現這三個網站都是由Ajax獲取文章列表,生成動態內容以作為傳統的分頁替代。這對於Puppeteer來說很容易處理,因為Puppeteer繞開了解析Ajax這一部分,瀏覽器會自動處理這樣的操作和請求,我們只著重關注資料獲取就行了。三個網站的抓取策略基本相同,我們以掘金為例著重講解。

掘金

首先是引入Puppeteer和開啟網頁。

const puppeteer = require('puppeteer');
const MongoClient = require('mongodb').MongoClient;

(async () => {
  // browser
  const browser = await (puppeteer.launch({
    headless: true
  }));

  // define start url
  const url = 'https://juejin.im';

  // start a new page
  const page = await browser.newPage();
  
  ...
  
})();
複製程式碼

headless設定為true可以讓瀏覽器以headless的方式執行,也就是指瀏覽器不用在介面中開啟,它會在後臺執行,使用者是看不到瀏覽器的。browser.newPage()將新生成一個標籤頁。後面的操作基本就圍繞著生成的page來進行。

接下來我們讓瀏覽器導航到start url。

  ...
  
  // navigate to url
  try {
    await page.goto(url, {waitUntil: 'domcontentloaded'});
    await page.waitFor(2000);
  } catch (e) {
    console.error(e);

    // close browser
    browser.close();

    // exit code 1 indicating an error happened
    code = 1;
    process.emit("exit ");
    process.reallyExit(code);

    return
  }
  
  ...
複製程式碼

這裡try catch的操作是為了處理瀏覽器訪問超時的錯誤。當訪問超時時,設定exit code1表示該任務失敗了,這樣Crawlab會將該任務狀態設定為FAILURE

然後我們需要下拉頁面讓瀏覽器可以讀取下一頁。

  ...
  
  // scroll down to fetch more data
  for (let i = 0; i < 100; i++) {
    console.log('Pressing PageDown...');
    await page.keyboard.press('PageDown', 200);
    await page.waitFor(100);
  }
  
  ...
複製程式碼

翻頁完畢後,就開始抓取資料了。

  ...
  // scrape data
  const results = await page.evaluate(() => {
    let results = [];
    document.querySelectorAll('.entry-list > .item').forEach(el => {
      if (!el.querySelector('.title')) return;
      results.push({
        url: 'https://juejin.com' + el.querySelector('.title').getAttribute('href'),
        title: el.querySelector('.title').innerText
      });
    });
    return results;
  });
  ...
複製程式碼

page.evaluate可以在瀏覽器Console中進行JS操作。這段程式碼其實可以直接在瀏覽器Console中直接執行。除錯起來是不是方便到爽?前端工程師們,開始歡呼吧!

獲取了資料,接下來我們需要將其儲存在資料庫中。

  ...
  
  // open database connection
  const client = await MongoClient.connect('mongodb://192.168.99.100:27017');
  let db = await client.db('crawlab_test');
  const colName = process.env.CRAWLAB_COLLECTION || 'results_juejin';
  const taskId = process.env.CRAWLAB_TASK_ID;
  const col = db.collection(colName);

  // save to database
  for (let i = 0; i < results.length; i++) {
    // de-duplication
    const r = await col.findOne({url: results[i]});
    if (r) continue;

    // assign taskID
    results[i].task_id = taskId;

    // insert row
    await col.insertOne(results[i]);
  }
  
  ...
複製程式碼

這樣,我們就將掘金最新的文章資料儲存在了資料庫中。其中,我們用url欄位做了去重處理。CRAWLAB_COLLECTIONCRAWLAB_TASK_ID是Crawlab傳過來的環境變數,分別是儲存的collection和任務ID。任務ID需要以task_id為鍵儲存起來,這樣在Crawlab中就可以將資料與任務關聯起來了。

整個爬蟲程式碼如下。

const puppeteer = require('puppeteer');
const MongoClient = require('mongodb').MongoClient;

(async () => {
  // browser
  const browser = await (puppeteer.launch({
    headless: true
  }));

  // define start url
  const url = 'https://juejin.im';

  // start a new page
  const page = await browser.newPage();

  // navigate to url
  try {
    await page.goto(url, {waitUntil: 'domcontentloaded'});
    await page.waitFor(2000);
  } catch (e) {
    console.error(e);

    // close browser
    browser.close();

    // exit code 1 indicating an error happened
    code = 1;
    process.emit("exit ");
    process.reallyExit(code);

    return
  }

  // scroll down to fetch more data
  for (let i = 0; i < 100; i++) {
    console.log('Pressing PageDown...');
    await page.keyboard.press('PageDown', 200);
    await page.waitFor(100);
  }

  // scrape data
  const results = await page.evaluate(() => {
    let results = [];
    document.querySelectorAll('.entry-list > .item').forEach(el => {
      if (!el.querySelector('.title')) return;
      results.push({
        url: 'https://juejin.com' + el.querySelector('.title').getAttribute('href'),
        title: el.querySelector('.title').innerText
      });
    });
    return results;
  });

  // open database connection
  const client = await MongoClient.connect('mongodb://192.168.99.100:27017');
  let db = await client.db('crawlab_test');
  const colName = process.env.CRAWLAB_COLLECTION || 'results_juejin';
  const taskId = process.env.CRAWLAB_TASK_ID;
  const col = db.collection(colName);

  // save to database
  for (let i = 0; i < results.length; i++) {
    // de-duplication
    const r = await col.findOne({url: results[i]});
    if (r) continue;

    // assign taskID
    results[i].task_id = taskId;

    // insert row
    await col.insertOne(results[i]);
  }

  console.log(`results.length: ${results.length}`);

  // close database connection
  client.close();

  // shutdown browser
  browser.close();
})();
複製程式碼

SegmentFault & CSDN

這兩個網站的爬蟲程式碼基本與上面的爬蟲一樣,只是一些引數不一樣而已。我們的爬蟲專案結構如下。

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

執行爬蟲

在Crawlab中開啟Spiders,我們可以看到剛剛編寫好的爬蟲。

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

點選各個爬蟲的View檢視按鈕,進入到爬蟲詳情。

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

在Execute Command中輸入爬蟲執行命令。對掘金爬蟲來說,是node juejin_spider.js。輸入完畢後點選Save儲存。然後點選Deploy部署爬蟲。最後點選Run執行爬蟲。

點選左上角到重新整理按鈕可以看到剛剛執行的爬蟲任務已經在執行了。點選Create Time後可以進入到任務詳情。Overview標籤中可以看到任務資訊,Log標籤可以看到日誌資訊,Results資訊中可以看到抓取結果。目前在Crawlab結果列表中還不支援資料匯出,但是不久的版本中肯定會將匯出功能加入進來。

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

總結

在這一小節,我們已經能夠將Crawlab執行起來,並且能用Puppeteer編寫抓取三大網站技術文章的爬蟲,並且能夠用Crawlab執行爬蟲,並且讀取抓取後的資料。下一節,我們將用Flask+Vue做一個簡單的技術文章聚合網站。能看到這裡的都是有耐心的好同學,贊一個。

Github: tikazyq/crawlab

如果感覺Crawlab還不錯的話,請加作者微信拉入開發交流群,大家一起交流關於Crawlab的使用和開發。

手把手教你如何用Crawlab構建技術文章聚合平臺(一)

相關文章