自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

基于 Node.js 的聲明式可監(jiān)控爬蟲網(wǎng)絡

開發(fā) 開發(fā)工具
爬蟲是數(shù)據(jù)抓取的重要手段之一,而以 Scrapy、Crawler4j、Nutch 為代表的開源框架能夠幫我們快速構建分布式爬蟲系統(tǒng)。

[[189077]]

爬蟲是數(shù)據(jù)抓取的重要手段之一,而以 ScrapyCrawler4j、Nutch 為代表的開源框架能夠幫我們快速構建分布式爬蟲系統(tǒng);就筆者淺見,我們在開發(fā)大規(guī)模爬蟲系統(tǒng)時可能會面臨以下挑戰(zhàn):

  • 網(wǎng)頁抓取:最簡單的抓取就是使用 HTTPClient 或者 fetch 或者 request 這樣的 HTTP 客戶端?,F(xiàn)在隨著單頁應用這樣富客戶端應用的流行,我們可以使用 Selenium、PhantomJS 這樣的 Headless Brwoser 來動態(tài)執(zhí)行腳本進行渲染。
  • 網(wǎng)頁解析:對于網(wǎng)頁內(nèi)容的抽取與解析是個很麻煩的問題,DOM4j、Cherrio、beautifulsoup 這些為我們提供了基本的解析功能。筆者也嘗試過構建全配置型的爬蟲,類似于 Web-Scraper,然而還是輸給了復雜多變,多層嵌套的 iFrame 頁面。這里筆者秉持代碼即配置的理念,對于使用配置來聲明的內(nèi)建復雜度比較低,但是對于那些業(yè)務復雜度較高的網(wǎng)頁,整體復雜度會以幾何倍數(shù)增長。而使用代碼來聲明其內(nèi)建復雜度與門檻相對較高,但是能較好地處理業(yè)務復雜度較高的網(wǎng)頁。筆者在構思未來的交互式爬蟲生成界面時,也是希望借鑒 FaaS 的思路,直接使用代碼聲明整個解析流程,而不是使用配置。
  • 反爬蟲對抗:類似于淘寶這樣的主流網(wǎng)站基本上都有反爬蟲機制,它們會對于請求頻次、請求地址、請求行為與目標的連貫性等多個維度進行分析,從而判斷請求者是爬蟲還是真實用戶。我們常見的方式就是使用多 IP 或者多代理來避免同一源的頻繁請求,或者可以借鑒 GAN 或者增強學習的思路,讓爬蟲自動地針對目標網(wǎng)站的反爬蟲策略進行自我升級與改造。另一個常見的反爬蟲方式就是驗證碼,從最初的混淆圖片到現(xiàn)在常見的拖動式驗證碼都是不小的障礙,我們可以使用圖片中文字提取、模擬用戶行為等方式來嘗試繞過。
  • 分布式調(diào)度:單機的吞吐量和性能總是有瓶頸的,而分布式爬蟲與其他分布式系統(tǒng)一樣,需要考慮分布式治理、數(shù)據(jù)一致性、任務調(diào)度等多個方面的問題。筆者個人的感覺是應該將爬蟲的工作節(jié)點盡可能地無狀態(tài)化,以 Redis 或者 Consul 這樣的能保證高可用性的中心存儲存放整個爬蟲集群的狀態(tài)。
  • 在線有價值頁面預判:Google 經(jīng)典的 PageRank 能夠基于網(wǎng)絡中的連接信息判斷某個 URL 的有價值程度,從而優(yōu)先索引或者抓取有價值的頁面。而像 Anthelion 這樣的智能解析工具能夠基于之前的頁面提取內(nèi)容的有價值程度來預判某個 URL 是否有抓取的必要。
  • 頁面內(nèi)容提取與存儲:對于網(wǎng)頁中的結構化或者非結構化的內(nèi)容實體提取是自然語言處理中的常見任務之一,而自動從海量數(shù)據(jù)中提取出有意義的內(nèi)容也涉及到機器學習、大數(shù)據(jù)處理等多個領域的知識。我們可以使用 Hadoop MapReduce、Spark、Flink 等離線或者流式計算引擎來處理海量數(shù)據(jù),使用詞嵌入、主題模型、LSTM 等等機器學習技術來分析文本,可以使用 HBase、ElasticSearch 來存儲或者對文本建立索引。

筆者本意并非想重新造個輪子,不過在改造我司某個簡單的命令式爬蟲的過程中發(fā)現(xiàn),很多的調(diào)度與監(jiān)控操作應該交由框架完成。Node.js 在開發(fā)大規(guī)模分布式應用程序的一致性(JavaScript 的不規(guī)范)與性能可能不如 Java 或者 Go。但是正如筆者在上文中提及,JavaScript 的優(yōu)勢在于能夠通過同構代碼同時運行在客戶端與服務端,那么未來對于解析這一步完全可以在客戶端調(diào)試完畢然后直接將代碼運行在服務端,這對于構建靈活多變的解析可能有一定意義。

總而言之,我只是想有一個可擴展、能監(jiān)控、簡單易用的爬蟲框架,所以我快速擼了一個 declarative-crawler,目前只是處于原型階段,尚未發(fā)布到 npm 中;希望有興趣的大大不吝賜教,特別是發(fā)現(xiàn)了有同類型的框架可以吱一聲,我看看能不能拿來主義,多多學習。

設計思想與架構概覽

當筆者幾年前編寫***個爬蟲時,整體思路是典型的命令式編程,即先抓取再解析,***持久化存儲,就如下述代碼:

  1. await fetchListAndContentThenIndex( 
  2.     'jsgc'
  3.     section.name
  4.     section.menuCode, 
  5.     section.category 
  6. ).then(() => { 
  7. }).catch(error => { 
  8.     console.log(error); 
  9. }); 

命令式編程相較于聲明式編程耦合度更高,可測試性與可控性更低;就好像從 jQuery 切換到 React、Angular、Vue.js 這樣的框架,我們應該盡可能將業(yè)務之外的事情交由工具,交由框架去管理與解決,這樣也會方便我們進行自定義地監(jiān)控??偨Y而言,筆者的設計思想主要包含以下幾點:

  • 關注點分離,整個架構分為了爬蟲調(diào)度 CrawlerScheduler、Crawler、Spider、dcEmitter、Store、KoaServer、MonitorUI 等幾個部分,盡可能地分離職責。
  • 聲明式編程,每個蜘蛛的生命周期包含抓取、抽取、解析與持久化存儲這幾個部分;開發(fā)者應該獨立地聲明這幾個部分,而完整的調(diào)用與調(diào)度應該由框架去完成。
  • 分層獨立可測試,以爬蟲的生命周期為例,抽取與解析應當聲明為純函數(shù),而抓取與持久化存儲更多的是面向業(yè)務,可以進行 Mock 或者包含副作用進行測試。

整個爬蟲網(wǎng)絡架構如下所示,目前全部代碼參考這里。

自定義蜘蛛與爬蟲

我們以抓取某個在線列表與詳情頁為例,首先我們需要針對兩個頁面構建蜘蛛,注意,每個蜘蛛負責針對某個 URL 進行抓取與解析,用戶應該首先編寫列表爬蟲,其需要聲明 model 屬性、復寫 before_extract、parse 與 persist 方法,各個方法會被串行調(diào)用。另一個需要注意的是,我們爬蟲可能會外部傳入一些配置信息,統(tǒng)一的聲明在了 extra 屬性內(nèi),這樣在持久化時也能用到。

  1. type ExtraType = { 
  2.   module?: string, 
  3.   name?: string, 
  4.   menuCode?: string, 
  5.   category?: string 
  6. }; 
  7.  
  8. export default class UAListSpider extends Spider { 
  9.  
  10.   displayName = "通用公告列表蜘蛛"
  11.  
  12.   extra: ExtraType = {}; 
  13.  
  14.   model = { 
  15.     $announcements: 'tr[height="25"]' 
  16.   }; 
  17.  
  18.   constructor(extra: ExtraType) { 
  19.     super(); 
  20.  
  21.     this.extra = extra; 
  22.   } 
  23.  
  24.   before_extract(pageHTML: string) { 
  25.     return pageHTML.replace(/<TR height=\d*>/gim, "<tr height=25>"); 
  26.   } 
  27.  
  28.   parse(pageElements: Object) { 
  29.     let announcements = []; 
  30.  
  31.     let announcementsLength = pageElements.$announcements.length; 
  32.  
  33.     for (let i = 0; i < announcementsLength; i++) { 
  34.       let $announcement = $(pageElements.$announcements[i]); 
  35.  
  36.       let $a = $announcement.find("a"); 
  37.       let title = $a.text(); 
  38.       let href = $a.attr("href"); 
  39.       let date = $announcement.find('td[align="right"]').text(); 
  40.  
  41.       announcements.push({ title: title, datedate, href: href }); 
  42.     } 
  43.  
  44.     return announcements; 
  45.   } 
  46.  
  47.   /** 
  48.    * @function 對采集到的數(shù)據(jù)進行持久化更新 
  49.    * @param pageObject 
  50.    */ 
  51.   async persist(announcements): Promise<boolean> { 
  52.     let flag = true
  53.  
  54.     // 這里每個 URL 對應一個公告數(shù)組 
  55.     for (let announcement of announcements) { 
  56.       try { 
  57.         await insertOrUpdateAnnouncement({ 
  58.           ...this.extra, 
  59.           ...announcement, 
  60.           infoID: href2infoID(announcement.href) 
  61.         }); 
  62.       } catch (err) { 
  63.         flag = false
  64.       } 
  65.     } 
  66.  
  67.     return flag; 
  68.   } 

我們可以針對這個蜘蛛進行單獨測試,這里使用 Jest。注意,這里為了方便描述沒有對抽取、解析等進行單元測試,在大型項目中我們是建議要加上這些純函數(shù)的測試用例。

  1. var expect = require("chai").expect; 
  2.  
  3. import UAListSpider from "../../src/universal_announcements/UAListSpider.js"
  4.  
  5. let uaListSpider: UAListSpider = new UAListSpider({ 
  6.   module: "jsgc"
  7.   name"房建市政招標公告-服務類"
  8.   menuCode: "001001/001001001/00100100100"
  9.   category: "1" 
  10. }).setRequest( 
  11.   "http://ggzy.njzwfw.gov.cn/njggzy/jsgc/001001/001001001/001001001001/?Paging=1"
  12.   {} 
  13. ); 
  14.  
  15. test("抓取公共列表", async () => { 
  16.   let announcements = await uaListSpider.run(false); 
  17.  
  18.   expect(announcements, "返回數(shù)據(jù)為列表并且長度大于10").to.have.length.above(2); 
  19. }); 
  20.  
  21. test("抓取公共列表 并且進行持久化操作", async () => { 
  22.   let announcements = await uaListSpider.run(true); 
  23.  
  24.   expect(announcements, "返回數(shù)據(jù)為列表并且長度大于10").to.have.length.above(2); 
  25. }); 

同理,我們可以定義對于詳情頁的蜘蛛:

  1. export default class UAContentSpider extends Spider { 
  2.   displayName = "通用公告內(nèi)容蜘蛛"
  3.  
  4.   model = { 
  5.     // 標題 
  6.     $title: "#tblInfo #tdTitle b"
  7.  
  8.     // 時間 
  9.     $time"#tblInfo #tdTitle font"
  10.  
  11.     // 內(nèi)容 
  12.     $content: "#tblInfo #TDContent" 
  13.   }; 
  14.  
  15.   parse(pageElements: Object) { 
  16.     ... 
  17.   } 
  18.  
  19.   async persist(announcement: Object) { 
  20.     ... 
  21.   } 

在定義完蜘蛛之后,我們可以定義負責爬取整個系列任務的 Crawler,注意,Spider 僅負責爬取單個頁面,而分頁等操作是由 Crawler 進行:

  1. /** 
  2.  * @function 通用的爬蟲 
  3.  */ 
  4. export default class UACrawler extends Crawler { 
  5.   displayName = "通用公告爬蟲"
  6.  
  7.   /** 
  8.    * @構造函數(shù) 
  9.    * @param config 
  10.    * @param extra 
  11.    */ 
  12.   constructor(extra: ExtraType) { 
  13.     super(); 
  14.  
  15.     extra && (this.extra = extra); 
  16.   } 
  17.  
  18.   initialize() { 
  19.     // 構建所有的爬蟲 
  20.     let requests = []; 
  21.  
  22.     for (let i = startPage; i < endPage + 1; i++) { 
  23.       requests.push( 
  24.         buildRequest({ 
  25.           ...this.extra, 
  26.           page: i 
  27.         }) 
  28.       ); 
  29.     } 
  30.  
  31.     this.setRequests(requests) 
  32.       .setSpider(new UAListSpider(this.extra)) 
  33.       .transform(announcements => { 
  34.         if (!Array.isArray(announcements)) { 
  35.           throw new Error("爬蟲連接失??!"); 
  36.         } 
  37.         return announcements.map(announcement => ({ 
  38.           url: `http://ggzy.njzwfw.gov.cn/${announcement.href}` 
  39.         })); 
  40.       }) 
  41.       .setSpider(new UAContentSpider(this.extra)); 
  42.   } 

一個 Crawler 最關鍵的就是 initialize 函數(shù),需要在其中完成爬蟲的初始化。首先我們需要構造所有的種子鏈接,這里既是多個列表頁;然后通過 setSpider 方法加入對應的蜘蛛。不同蜘蛛之間通過自定義的 Transformer 函數(shù)來從上一個結果中抽取出所需要的鏈接傳入到下一個蜘蛛中。至此我們爬蟲網(wǎng)絡的關鍵組件定義完畢。

本地運行

定義完 Crawler 之后,我們可以通過將爬蟲注冊到 CrawlerScheduler 來運行爬蟲:

  1. const crawlerScheduler: CrawlerScheduler = new CrawlerScheduler(); 
  2.  
  3. let uaCrawler = new UACrawler({ 
  4.   module: "jsgc"
  5.   name"房建市政招標公告-服務類"
  6.   menuCode: "001001/001001001/00100100100"
  7.   category: "1" 
  8. }); 
  9.  
  10. crawlerScheduler.register(uaCrawler); 
  11.  
  12. dcEmitter.on("StoreChange", () => { 
  13.   console.log("-----------" + new Date() + "-----------"); 
  14.   console.log(store.crawlerStatisticsMap); 
  15. }); 
  16.  
  17. crawlerScheduler.run().then(() => {}); 

這里的 dcEmitter 是整個狀態(tài)的中轉站,如果選擇使用本地運行,可以自己監(jiān)聽 dcEmitter 中的事件:

  1. -----------Wed Apr 19 2017 22:12:54 GMT+0800 (CST)----------- 
  2. { UACrawler:  
  3.    CrawlerStatistics { 
  4.      isRunning: true
  5.      spiderStatisticsList: { UAListSpider: [Object], UAContentSpider: [Object] }, 
  6.      instance:  
  7.       UACrawler { 
  8.         name'UACrawler'
  9.         displayName: '通用公告爬蟲'
  10.         spiders: [Object], 
  11.         transforms: [Object], 
  12.         requests: [Object], 
  13.         isRunning: true
  14.         extra: [Object] }, 
  15.      lastStartTime: 2017-04-19T14:12:51.373Z } } 

服務端運行

我們也可以以服務的方式運行爬蟲:

  1. const crawlerScheduler: CrawlerScheduler = new CrawlerScheduler(); 
  2.  
  3. let uaCrawler = new UACrawler({ 
  4.   module: "jsgc"
  5.   name"房建市政招標公告-服務類"
  6.   menuCode: "001001/001001001/00100100100"
  7.   category: "1" 
  8. }); 
  9.  
  10. crawlerScheduler.register(uaCrawler); 
  11.  
  12. new CrawlerServer(crawlerScheduler).run().then(()=>{},(error)=>{console.log(error)}); 

此時會啟動框架內(nèi)置的 Koa 服務器,允許用戶通過 RESTful 接口來控制爬蟲網(wǎng)絡與獲取當前狀態(tài)。

接口說明

關鍵字段

  • 爬蟲
  1. // 判斷爬蟲是否正在運行 
  2. isRunning: boolean = false
  3.  
  4. // 爬蟲***一次激活時間 
  5. lastStartTime: Date
  6.  
  7. // 爬蟲***一次運行結束時間 
  8. lastFinishTime: Date
  9.  
  10. // 爬蟲***的異常信息 
  11. lastError: Error; 
  • 蜘蛛
  1. // ***一次運行時間 
  2. lastActiveTime: Date
  3.  
  4. // 平均總執(zhí)行時間 / ms 
  5. executeDuration: number = 0; 
  6.  
  7. // 爬蟲次數(shù)統(tǒng)計 
  8. count: number = 0; 
  9.  
  10. // 異常次數(shù)統(tǒng)計 
  11. errorCount: number = 0; 
  12.  
  13. countByTime: { [number]: number } = {}; 

localhost:3001/ 獲取當前爬蟲運行狀態(tài)

  • 尚未啟動
  1.     { 
  2.         name"UACrawler"
  3.         displayName: "通用公告爬蟲"
  4.         isRunning: false
  5.     } 
  • 正常返回
  1.     { 
  2.         name"UACrawler"
  3.         displayName: "通用公告爬蟲"
  4.         isRunning: true
  5.         lastStartTime: "2017-04-19T06:41:55.407Z" 
  6.     } 
  • 出現(xiàn)錯誤
  1.     { 
  2.         name"UACrawler"
  3.         displayName: "通用公告爬蟲"
  4.         isRunning: true
  5.         lastStartTime: "2017-04-19T06:46:05.410Z"
  6.         lastError: { 
  7.             spiderName: "UAListSpider"
  8.             message: "抓取超時"
  9.             url: "http://ggzy.njzwfw.gov.cn/njggzy/jsgc/001001/001001001/001001001001?Paging=1"
  10.             time"2017-04-19T06:47:05.414Z" 
  11.         } 
  12.     } 

localhost:3001/start 啟動爬蟲

  1.     message:"OK" 

localhost:3001/status 返回當前系統(tǒng)狀態(tài)

  1.     "cpu":0, 
  2.     "memory":0.9945211410522461 

localhost:3001/UACrawler 根據(jù)爬蟲名查看爬蟲運行狀態(tài)

  1. [   
  2.    {   
  3.       "name":"UAListSpider"
  4.       "displayName":"通用公告列表蜘蛛"
  5.       "count":6, 
  6.       "countByTime":{   
  7.          "0":0, 
  8.          "1":0, 
  9.          "2":0, 
  10.          "3":0, 
  11.          ... 
  12.          "58":0, 
  13.          "59":0 
  14.       }, 
  15.       "lastActiveTime":"2017-04-19T06:50:06.935Z"
  16.       "executeDuration":1207.4375, 
  17.       "errorCount":0 
  18.    }, 
  19.    {   
  20.       "name":"UAContentSpider"
  21.       "displayName":"通用公告內(nèi)容蜘蛛"
  22.       "count":120, 
  23.       "countByTime":{   
  24.          "0":0, 
  25.          ... 
  26.          "59":0 
  27.       }, 
  28.       "lastActiveTime":"2017-04-19T06:51:11.072Z"
  29.       "executeDuration":1000.1596102359835, 
  30.       "errorCount":0 
  31.    } 

自定義監(jiān)控界面

CrawlerServer 提供了 RESTful API 來返回當前爬蟲的狀態(tài)信息,我們可以利用 React 或者其他框架來快速搭建監(jiān)控界面。

【本文是51CTO專欄作者“張梓雄 ”的原創(chuàng)文章,如需轉載請通過51CTO與作者聯(lián)系】

戳這里,看該作者更多好文

責任編輯:武曉燕 來源: 51CTO專欄
相關推薦

2021-12-25 22:29:57

Node.js 微任務處理事件循環(huán)

2021-12-18 07:42:15

Ebpf 監(jiān)控 Node.js

2013-11-01 09:34:56

Node.js技術

2015-03-10 10:59:18

Node.js開發(fā)指南基礎介紹

2011-10-25 09:28:30

Node.js

2020-05-29 15:33:28

Node.js框架JavaScript

2012-02-03 09:25:39

Node.js

2011-09-09 14:23:13

Node.js

2011-11-01 10:30:36

Node.js

2011-09-08 13:46:14

node.js

2011-09-02 14:47:48

Node

2012-10-24 14:56:30

IBMdw

2011-11-10 08:55:00

Node.js

2015-07-21 16:23:22

Node.js構建分布式

2021-09-26 05:06:04

Node.js模塊機制

2011-11-02 09:04:15

Node.js

2021-11-06 18:40:27

js底層模塊

2011-10-18 10:17:13

Node.js

2017-09-01 08:37:54

Node.jsGhost1.8.1 版本

2019-07-09 14:50:15

Node.js前端工具
點贊
收藏

51CTO技術棧公眾號