爬蟲 | 如何構建技能文章聚合平台(一)

尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️

加入LINE好友

來源 | 掘金

本文經作者授權發布,如需轉載請直接聯繫原作者。

博客地址:https://juejin.im/user/5a1ba6def265da430b7af463

背景

說到爬蟲,大多數工程師想到的是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用克隆一份到本地。

  1. git clone https://github.com/tikazyq/crawlab
  2. 復制代碼

安裝相應的依賴包和庫。

  1. cd crawlab
  2. # 安裝python依賴
  3. pip install -r crawlab/requirements
  4. # 安裝前端依賴
  5. cd frontend
  6. npm install
  7. 復制代碼

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

運行

在運行之前需要對Crawlab進行一些配置,配置文件為 config.py

  1. # project variables
  2. PROJECT_SOURCE_FILE_FOLDER = ‘/Users/yeqing/projects/crawlab/spiders’ # 爬蟲源碼根目錄
  3. PROJECT_DEPLOY_FILE_FOLDER = ‘/var/crawlab’ # 爬蟲部署根目錄
  4. PROJECT_LOGS_FOLDER = ‘/var/logs/crawlab’ # 日志目錄
  5. PROJECT_TMP_FOLDER = ‘/tmp’ # 臨時文件目錄
  6. # celery variables
  7. BROKER_URL = ‘redis://192.168.99.100:6379/0’ # 中間者URL,連接redis
  8. CELERY_RESULT_BACKEND = ‘mongodb://192.168.99.100:27017/’ # CELERY後台URL
  9. CELERY_MONGODB_BACKEND_SETTINGS = {
  10. ‘database’: ‘crawlab_test’,
  11. ‘taskmeta_collection’: ‘tasks_celery’,
  12. }
  13. CELERY_TIMEZONE = ‘Asia/Shanghai’
  14. CELERY_ENABLE_UTC = True
  15. # flower variables
  16. FLOWER_API_ENDPOINT = ‘http://localhost:5555/api’ # Flower服務地址
  17. # database variables
  18. MONGO_HOST = ‘192.168.99.100’
  19. MONGO_PORT = 27017
  20. MONGO_DB = ‘crawlab_test’
  21. # flask variables
  22. DEBUG = True
  23. FLASK_HOST = ‘127.0.0.1’
  24. FLASK_PORT = 8000
  25. 復制代碼

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

  1. cd ../crawlab
  2. python app.py
  3. 復制代碼

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

  1. python ./bin/run_flower.py
  2. 復制代碼

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

  1. python ./bin/run_worker.py
  2. 復制代碼

啟動前端服務器。

  1. cd ../frontend
  2. npm run serve
  3. 復制代碼

使用

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

爬蟲 | 如何構建技能文章聚合平台(一) 科技 第1張

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

爬蟲 | 如何構建技能文章聚合平台(一) 科技 第2張

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

爬蟲 | 如何構建技能文章聚合平台(一) 科技 第3張

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

Puppeteer

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

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

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

安裝

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

  1. # PROJECT_DEPLOY_FILE_FOLDER變量值
  2. cd /var/crawlab
  3. # 安裝puppeteer
  4. npm i puppeteer
  5. # 安裝mongodb
  6. npm i mongodb
  7. 復制代碼

安裝mongodb是為了後續的數據庫操作。

使用

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

  1. const puppeteer = require(‘puppeteer’);
  2. (async () => {
  3. const browser = await (puppeteer.launch());
  4. const page = await browser.newPage();
  5. await page.goto(‘https://www.jianshu.com/u/40909ea33e50’);
  6. await page.screenshot({
  7. path: ‘jianshu.png’,
  8. type: ‘png’,
  9. // quality: 100, 只對jpg有效
  10. fullPage: true,
  11. // 指定區域截圖,clip和fullPage兩者只能設置一個
  12. // clip: {
  13. // x: 0,
  14. // y: 0,
  15. // width: 1000,
  16. // height: 40
  17. // }
  18. });
  19. browser.close();
  20. })();
  21. 復制代碼

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

編寫爬蟲

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

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

  • 掘金
  • SegmentFault
  • CSDN

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

掘金

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

  1. const puppeteer = require(‘puppeteer’);
  2. const MongoClient = require(‘mongodb’).MongoClient;
  3. (async () => {
  4. // browser
  5. const browser = await (puppeteer.launch({
  6. headless: true
  7. }));
  8. // define start url
  9. const url = ‘https://juejin.im’;
  10. // start a new page
  11. const page = await browser.newPage();
  12. })();
  13. 復制代碼

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

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

  1. // navigate to url
  2. try {
  3. await page.goto(url, {waitUntil: ‘domcontentloaded’});
  4. await page.waitFor(2000);
  5. } catch (e) {
  6. console.error(e);
  7. // close browser
  8. browser.close();
  9. // exit code 1 indicating an error happened
  10. code = 1;
  11. process.emit(“exit “);
  12. process.reallyExit(code);
  13. return
  14. }
  15. 復制代碼

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

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

  1. // scroll down to fetch more data
  2. for (let i = 0; i < 100; i++) {
  3. console.log(‘Pressing PageDown…’);
  4. await page.keyboard.press(‘PageDown’, 200);
  5. await page.waitFor(100);
  6. }
  7. 復制代碼

翻頁完畢後,就開始抓取數據了。

  1. // scrape data
  2. const results = await page.evaluate(() => {
  3. let results = [];
  4. document.querySelectorAll(‘.entry-list > .item’).forEach(el => {
  5. if (!el.querySelector(‘.title’)) return;
  6. results.push({
  7. url: ‘https://juejin.com’ + el.querySelector(‘.title’).getAttribute(‘href’),
  8. title: el.querySelector(‘.title’).innerText
  9. });
  10. });
  11. return results;
  12. });
  13. 復制代碼

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

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

  1. // open database connection
  2. const client = await MongoClient.connect(‘mongodb://192.168.99.100:27017’);
  3. let db = await client.db(‘crawlab_test’);
  4. const colName = process.env.CRAWLAB_COLLECTION || ‘results_juejin’;
  5. const taskId = process.env.CRAWLAB_TASK_ID;
  6. const col = db.collection(colName);
  7. // save to database
  8. for (let i = 0; i < results.length; i++) {
  9. // de-duplication
  10. const r = await col.findOne({url: results[i]});
  11. if (r) continue;
  12. // assign taskID
  13. results[i].task_id = taskId;
  14. // insert row
  15. await col.insertOne(results[i]);
  16. }
  17. 復制代碼

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

整個爬蟲代碼如下。

  1. const puppeteer = require(‘puppeteer’);
  2. const MongoClient = require(‘mongodb’).MongoClient;
  3. (async () => {
  4. // browser
  5. const browser = await (puppeteer.launch({
  6. headless: true
  7. }));
  8. // define start url
  9. const url = ‘https://juejin.im’;
  10. // start a new page
  11. const page = await browser.newPage();
  12. // navigate to url
  13. try {
  14. await page.goto(url, {waitUntil: ‘domcontentloaded’});
  15. await page.waitFor(2000);
  16. } catch (e) {
  17. console.error(e);
  18. // close browser
  19. browser.close();
  20. // exit code 1 indicating an error happened
  21. code = 1;
  22. process.emit(“exit “);
  23. process.reallyExit(code);
  24. return
  25. }
  26. // scroll down to fetch more data
  27. for (let i = 0; i < 100; i++) {
  28. console.log(‘Pressing PageDown…’);
  29. await page.keyboard.press(‘PageDown’, 200);
  30. await page.waitFor(100);
  31. }
  32. // scrape data
  33. const results = await page.evaluate(() => {
  34. let results = [];
  35. document.querySelectorAll(‘.entry-list > .item’).forEach(el => {
  36. if (!el.querySelector(‘.title’)) return;
  37. results.push({
  38. url: ‘https://juejin.com’ + el.querySelector(‘.title’).getAttribute(‘href’),
  39. title: el.querySelector(‘.title’).innerText
  40. });
  41. });
  42. return results;
  43. });
  44. // open database connection
  45. const client = await MongoClient.connect(‘mongodb://192.168.99.100:27017’);
  46. let db = await client.db(‘crawlab_test’);
  47. const colName = process.env.CRAWLAB_COLLECTION || ‘results_juejin’;
  48. const taskId = process.env.CRAWLAB_TASK_ID;
  49. const col = db.collection(colName);
  50. // save to database
  51. for (let i = 0; i < results.length; i++) {
  52. // de-duplication
  53. const r = await col.findOne({url: results[i]});
  54. if (r) continue;
  55. // assign taskID
  56. results[i].task_id = taskId;
  57. // insert row
  58. await col.insertOne(results[i]);
  59. }
  60. console.log(`results.length: ${results.length}`);
  61. // close database connection
  62. client.close();
  63. // shutdown browser
  64. browser.close();
  65. })();
  66. 復制代碼

SegmentFault & CSDN

這兩個網站的爬蟲代碼基本與上面的爬蟲一樣,只是一些參數不一樣而已。我們的爬蟲項目結構如下。

爬蟲 | 如何構建技能文章聚合平台(一) 科技 第4張

運行爬蟲

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

爬蟲 | 如何構建技能文章聚合平台(一) 科技 第5張

點擊各個爬蟲的View查看按鈕,進入到爬蟲詳情。

爬蟲 | 如何構建技能文章聚合平台(一) 科技 第6張

在Execute Command中輸入爬蟲執行命令。對掘金爬蟲來說,是 node juejin_spider.js

點擊左上角到刷新按鈕可以看到剛剛運行的爬蟲任務已經在運行了。點擊Create Time後可以進入到任務詳情。Overview標籤中可以看到任務信息,Log標籤可以看到日志信息,Results信息中可以看到抓取結果。目前在Crawlab結果列表中還不支持數據導出,但是不久的版本中肯定會將導出功能加入進來。

爬蟲 | 如何構建技能文章聚合平台(一) 科技 第7張

爬蟲 | 如何構建技能文章聚合平台(一) 科技 第8張

爬蟲 | 如何構建技能文章聚合平台(一) 科技 第9張

總結

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

— END —

題圖:pexels,CC0 授權。

About 尋夢園
尋夢園是台灣最大的聊天室及交友社群網站。 致力於發展能夠讓會員們彼此互動、盡情分享自我的平台。 擁有數百間不同的聊天室 ,讓您隨時隨地都能找到志同道合的好友!