03.28 如何構建技術文章聚合平臺(一)

或許是 Scrapy 之外的一個新選擇,尤其對於比較熟悉 JavaScript 的同學來說。

作者 | MarvinZhang

來源 | 掘金

博客地址: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. <code>git clone https://github.com/tikazyq/crawlab/<code>

  2. <code>複製代碼/<code>

安裝相應的依賴包和庫。

  1. <code>cd crawlab/<code>


  2. <code># 安裝python依賴/<code>

  3. <code>pip install -r crawlab/requirements/<code>


  4. <code># 安裝前端依賴/<code>

  5. <code>cd frontend/<code>

  6. <code>npm install/<code>

  7. <code>複製代碼/<code>

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

運行

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

  1. <code># project variables/<code>

  2. <code>PROJECT_SOURCE_FILE_FOLDER = '/Users/yeqing/projects/crawlab/spiders' # 爬蟲源碼根目錄/<code>

  3. <code>PROJECT_DEPLOY_FILE_FOLDER = '/var/crawlab' # 爬蟲部署根目錄/<code>

  4. <code>PROJECT_LOGS_FOLDER = '/var/logs/crawlab' # 日誌目錄/<code>

  5. <code>PROJECT_TMP_FOLDER = '/tmp' # 臨時文件目錄/<code>


  6. <code># celery variables/<code>

  7. <code>BROKER_URL = 'redis://192.168.99.100:6379/0' # 中間者URL,連接redis/<code>

  8. <code>CELERY_RESULT_BACKEND = 'mongodb://192.168.99.100:27017/' # CELERY後臺URL/<code>

  9. <code>CELERY_MONGODB_BACKEND_SETTINGS = {/<code>

  10. <code>'database': 'crawlab_test',/<code>

  11. <code>'taskmeta_collection': 'tasks_celery',/<code>

  12. <code>}/<code>

  13. <code>CELERY_TIMEZONE = 'Asia/Shanghai'/<code>

  14. <code>CELERY_ENABLE_UTC = True/<code>


  15. <code># flower variables/<code>

  16. <code>FLOWER_API_ENDPOINT = 'http://localhost:5555/api' # Flower服務地址/<code>


  17. <code># database variables/<code>

  18. <code>MONGO_HOST = '192.168.99.100'/<code>

  19. <code>MONGO_PORT = 27017/<code>

  20. <code>MONGO_DB = 'crawlab_test'/<code>


  21. <code># flask variables/<code>

  22. <code>DEBUG = True/<code>

  23. <code>FLASK_HOST = '127.0.0.1'/<code>

  24. <code>FLASK_PORT = 8000/<code>

  25. <code>複製代碼/<code>

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

  1. <code>cd ../crawlab/<code>

  2. <code>python app.py/<code>

  3. <code>複製代碼/<code>

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

  1. <code>python ./bin/run_flower.py/<code>

  2. <code>複製代碼/<code>

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

  1. <code>python ./bin/run_worker.py/<code>

  2. <code>複製代碼/<code>

啟動前端服務器。

  1. <code>cd ../frontend/<code>

  2. <code>npm run serve/<code>

  3. <code>複製代碼/<code>

使用

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

爬虫 | 如何构建技术文章聚合平台(一)

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

爬虫 | 如何构建技术文章聚合平台(一)

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

爬虫 | 如何构建技术文章聚合平台(一)

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

Puppeteer

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

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

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

安裝

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

  1. <code># PROJECT_DEPLOY_FILE_FOLDER變量值/<code>

  2. <code>cd /var/crawlab/<code>


  3. <code># 安裝puppeteer/<code>

  4. <code>npm i puppeteer/<code>


  5. <code># 安裝mongodb/<code>

  6. <code>npm i mongodb/<code>

  7. <code>複製代碼/<code>

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

使用

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

  1. <code>const puppeteer = require('puppeteer');/<code>


  2. <code>(async => {/<code>

  3. <code>const browser = await (puppeteer.launch);/<code>

  4. <code>const page = await browser.newPage;/<code>

  5. <code>await page.goto('https://www.jianshu.com/u/40909ea33e50');/<code>

  6. <code>await page.screenshot({/<code>

  7. <code>path: 'jianshu.png',/<code>

  8. <code>type: 'png',/<code>

  9. <code>// quality: 100, 只對jpg有效/<code>

  10. <code>fullPage: true,/<code>

  11. <code>// 指定區域截圖,clip和fullPage兩者只能設置一個/<code>

  12. <code>// clip: {/<code>

  13. <code>// x: 0,/<code>

  14. <code>// y: 0,/<code>

  15. <code>// width: 1000,/<code>

  16. <code>// height: 40/<code>

  17. <code>// }/<code>

  18. <code>});/<code>

  19. <code>browser.close;/<code>

  20. <code>});/<code>

  21. <code>複製代碼/<code>

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

編寫爬蟲

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

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

  • 掘金

  • SegmentFault

  • CSDN

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

掘金

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

  1. <code>const puppeteer = require('puppeteer');/<code>

  2. <code>const MongoClient = require('mongodb').MongoClient;/<code>


  3. <code>(async => {/<code>

  4. <code>// browser/<code>

  5. <code>const browser = await (puppeteer.launch({/<code>

  6. <code>headless: true/<code>

  7. <code>}));/<code>


  8. <code>// define start url/<code>

  9. <code>const url = 'https://juejin.im';/<code>


  10. <code>// start a new page/<code>

  11. <code>const page = await browser.newPage;/<code>


  12. <code>.../<code>


  13. <code>});/<code>

  14. <code>複製代碼/<code>

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

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

  1. <code>.../<code>


  2. <code>// navigate to url/<code>

  3. <code>try {/<code>

  4. <code>await page.goto(url, {waitUntil: 'domcontentloaded'});/<code>

  5. <code>await page.waitFor(2000);/<code>

  6. <code>} catch (e) {/<code>

  7. <code>console.error(e);/<code>


  8. <code>// close browser/<code>

  9. <code>browser.close;/<code>


  10. <code>// exit code 1 indicating an error happened/<code>

  11. <code>code = 1;/<code>

  12. <code>process.emit("exit ");/<code>

  13. <code>process.reallyExit(code);/<code>


  14. <code>return/<code>

  15. <code>}/<code>


  16. <code>.../<code>

  17. <code>複製代碼/<code>

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

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

  1. <code>.../<code>


  2. <code>// scroll down to fetch more data/<code>

  3. <code>for (let i = 0; i < 100; i++) {/<code>

  4. <code>console.log('Pressing PageDown...');/<code>

  5. <code>await page.keyboard.press('PageDown', 200);/<code>

  6. <code>await page.waitFor(100);/<code>

  7. <code>}/<code>


  8. <code>.../<code>

  9. <code>複製代碼/<code>

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

  1. <code>.../<code>

  2. <code>// scrape data/<code>

  3. <code>const results = await page.evaluate( => {/<code>

  4. <code>let results = ;/<code>

  5. <code>document.querySelectorAll('.entry-list > .item').forEach(el => {/<code>

  6. <code>if (!el.querySelector('.title')) return;/<code>

  7. <code>results.push({/<code>

  8. <code>url: 'https://juejin.com' + el.querySelector('.title').getAttribute('href'),/<code>

  9. <code>title: el.querySelector('.title').innerText/<code>

  10. <code>});/<code>

  11. <code>});/<code>

  12. <code>return results;/<code>

  13. <code>});/<code>

  14. <code>.../<code>

  15. <code>複製代碼/<code>

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

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

  1. <code>.../<code>


  2. <code>// open database connection/<code>

  3. <code>const client = await MongoClient.connect('mongodb://192.168.99.100:27017');/<code>

  4. <code>let db = await client.db('crawlab_test');/<code>

  5. <code>const colName = process.env.CRAWLAB_COLLECTION || 'results_juejin';/<code>

  6. <code>const taskId = process.env.CRAWLAB_TASK_ID;/<code>

  7. <code>const col = db.collection(colName);/<code>


  8. <code>// save to database/<code>

  9. <code>for (let i = 0; i < results.length; i++) {/<code>

  10. <code>// de-duplication/<code>

  11. <code>const r = await col.findOne({url: results[i]});/<code>

  12. <code>if (r) continue;/<code>


  13. <code>// assign taskID/<code>

  14. <code>results[i].task_id = taskId;/<code>


  15. <code>// insert row/<code>

  16. <code>await col.insertOne(results[i]);/<code>

  17. <code>}/<code>


  18. <code>.../<code>

  19. <code>複製代碼/<code>

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

整個爬蟲代碼如下。

  1. <code>const puppeteer = require('puppeteer');/<code>

  2. <code>const MongoClient = require('mongodb').MongoClient;/<code>


  3. <code>(async => {/<code>

  4. <code>// browser/<code>

  5. <code>const browser = await (puppeteer.launch({/<code>

  6. <code>headless: true/<code>

  7. <code>}));/<code>


  8. <code>// define start url/<code>

  9. <code>const url = 'https://juejin.im';/<code>


  10. <code>// start a new page/<code>

  11. <code>const page = await browser.newPage;/<code>


  12. <code>// navigate to url/<code>

  13. <code>try {/<code>

  14. <code>await page.goto(url, {waitUntil: 'domcontentloaded'});/<code>

  15. <code>await page.waitFor(2000);/<code>

  16. <code>} catch (e) {/<code>

  17. <code>console.error(e);/<code>


  18. <code>// close browser/<code>

  19. <code>browser.close;/<code>


  20. <code>// exit code 1 indicating an error happened/<code>

  21. <code>code = 1;/<code>

  22. <code>process.emit("exit ");/<code>

  23. <code>process.reallyExit(code);/<code>


  24. <code>return/<code>

  25. <code>}/<code>


  26. <code>// scroll down to fetch more data/<code>

  27. <code>for (let i = 0; i < 100; i++) {/<code>

  28. <code>console.log('Pressing PageDown...');/<code>

  29. <code>await page.keyboard.press('PageDown', 200);/<code>

  30. <code>await page.waitFor(100);/<code>

  31. <code>}/<code>


  32. <code>// scrape data/<code>

  33. <code>const results = await page.evaluate( => {/<code>

  34. <code>let results = ;/<code>

  35. <code>document.querySelectorAll('.entry-list > .item').forEach(el => {/<code>

  36. <code>if (!el.querySelector('.title')) return;/<code>

  37. <code>results.push({/<code>

  38. <code>url: 'https://juejin.com' + el.querySelector('.title').getAttribute('href'),/<code>

  39. <code>title: el.querySelector('.title').innerText/<code>

  40. <code>});/<code>

  41. <code>});/<code>

  42. <code>return results;/<code>

  43. <code>});/<code>


  44. <code>// open database connection/<code>

  45. <code>const client = await MongoClient.connect('mongodb://192.168.99.100:27017');/<code>

  46. <code>let db = await client.db('crawlab_test');/<code>

  47. <code>const colName = process.env.CRAWLAB_COLLECTION || 'results_juejin';/<code>

  48. <code>const taskId = process.env.CRAWLAB_TASK_ID;/<code>

  49. <code>const col = db.collection(colName);/<code>


  50. <code>// save to database/<code>

  51. <code>for (let i = 0; i < results.length; i++) {/<code>

  52. <code>// de-duplication/<code>

  53. <code>const r = await col.findOne({url: results[i]});/<code>

  54. <code>if (r) continue;/<code>


  55. <code>// assign taskID/<code>

  56. <code>results[i].task_id = taskId;/<code>


  57. <code>// insert row/<code>

  58. <code>await col.insertOne(results[i]);/<code>

  59. <code>}/<code>


  60. <code>console.log(`results.length: ${results.length}`);/<code>


  61. <code>// close database connection/<code>

  62. <code>client.close;/<code>


  63. <code>// shutdown browser/<code>

  64. <code>browser.close;/<code>

  65. <code>});/<code>

  66. <code>複製代碼/<code>

SegmentFault & CSDN

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

爬虫 | 如何构建技术文章聚合平台(一)

運行爬蟲

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

爬虫 | 如何构建技术文章聚合平台(一)

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

爬虫 | 如何构建技术文章聚合平台(一)

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

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

爬虫 | 如何构建技术文章聚合平台(一)爬虫 | 如何构建技术文章聚合平台(一)
爬虫 | 如何构建技术文章聚合平台(一)

總結

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

-- END --

回覆下方「關鍵詞」,獲取優質資源

回覆關鍵詞「 pybook03」,可立即獲取主頁君與小夥伴一起翻譯的《Think Python 2e》電子版

回覆關鍵詞「pybooks02」,可立即獲取 O'Reilly 出版社推出的免費 Python 相關電子書合集

回覆關鍵詞「書單02」,可立即獲取主頁君整理的 10 本 Python 入門書的電子版

爬虫 | 如何构建技术文章聚合平台(一)

印度小夥寫了套深度學習教程,Github上星標已經5000+

上百個數據文件合併,只能手動複製粘貼?教你一招十秒搞定!

一個提升圖像識別準確率的精妙技巧

一文讀懂:從 Python 打包到 CLI 工具

如何使用 Python 進行時間序列預測?

美亞Kindle排名第一的Python 3入門書,火遍了整個編程圈

十分鐘搭建私有 Jupyter Notebook 服務器

使用 Python 製作屬於自己的 PDF 電子書

12步輕鬆搞定Python裝飾器

200 行代碼實現 2048 遊戲

點擊閱讀原文,查看更多 Python 教程和資源。


分享到:


相關文章: