1. 程式人生 > >從零開始打造一個新聞訂閱APP之爬蟲篇(二、實現一個簡單的爬蟲系統)

從零開始打造一個新聞訂閱APP之爬蟲篇(二、實現一個簡單的爬蟲系統)

前景提要:如何開發一個新聞訂閱APP之爬蟲篇(一、背景介紹&需求分析)
做一個特定的爬蟲系統,首先考慮它要做什麼?
從網際網路上抓取指定的N個站點資訊,解析提取需要的內容,按照特定的結構儲存;
系統結構圖如下:
這裡寫圖片描述
下面是主要的程式碼結構;
這裡寫圖片描述
這裡寫圖片描述
首先,定義一個CrawlerBootStrap類,作為整個系統的主入口。

public void init(){
     crawlerList = new ArrayList <Crawler >() ;
     for( String name : crawlerNameList ){
          Crawler c = (Crawler ) SpringUtil. getObject( name) ;
          if
( c. init()) crawlerList .add (c ); } start() ; } //爬蟲執行緒池,儲存多個爬蟲,控制同一時間正在執行爬蟲個數 public void start(){ //爬蟲池 int corePoolSize = 20 ; int maximumPoolSize = 1000 ; long keepAliveTime = 100000L ; BlockingQueue <Runnable > runnableTaskQueue = new PriorityBlockingQueue <Runnable >( 20
) ; ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor (corePoolSize , maximumPoolSize , keepAliveTime, TimeUnit .MILLISECONDS , runnableTaskQueue ); logger. info( "爬蟲池啟動" ); for( Crawler crawler : crawlerList ){ threadPoolExecutor .execute (crawler ); } //入庫執行緒
logger. info( "入庫執行緒啟動" ); Thread t = new Thread (newsConsumer ); t. start() ; }

程式碼解釋:
1、由於無法預估每一個待抓取的網站結構和資訊更新方式是不是一成不變的,因此,為每一個站點生成一個具體的實現類;
2、這裡採用了生產者-消費者模式,由阻塞佇列(BlockingQueue)來實現。每一個抓取執行緒都是生產者,消費者則只有一個入庫執行緒;
工作流程如下:

  1. 迴圈從初始化列表中獲取待抓取類,例項化,並新增到抓取列表;
  2. 初始化執行緒池,由執行緒池來管理每一個抓取執行緒(一個執行緒對應一個站點抓取例項);
  3. 啟動入口執行緒;

對於那些幾乎都是同一類模式(列表+正文,排序方式按照更新時間倒序排列)的站點,可以抽象出一個抽象類實現共性的抓取邏輯,而每一個子類只實現一些細節點:
每一個Crawler都是一個執行緒,
下面介紹CommonNewsListSiteCrawler的實現邏輯

public void run() {
     try {
          crawl() ;
     } catch (Exception e ) {
          logger. error( "crawl error" , e) ;
     }
}

protected void crawl(){
      boolean first = true ;
      while( true ){
            try{
                 if( first){
                      index = 30;
                      first = false ;
                 } else{
                      index = 5;
                 }
                 level = 0;
                 duplicateNum = 0 ;
                 NewsResult <List <PublishNewsModuleParams >> newsRes = NewsResult . getSuccessInstance() ;
                 newsRes. setNextUrl( String .format (crawlUrl ,index ));
                 do{
                      newsRes = parseHtml( newsRes. getNextUrl()) ;
                      if( newsRes. isSuccess()){
                            int num = addToNewsQueue (newsRes );
                            Jedis jedis = null;
                            try{
                                 jedis = jedisPool. getResource ();
                                 jedis. incrBy( RedisConstant .CRAWLER_NAMESPACE + namespace + "crawlTotalNum" , num) ;
                                 jedis. incrBy( RedisConstant .CRAWLER_NAMESPACE + namespace + "todayCrawlTotalNum" , num) ;
                                 jedis. set( RedisConstant .CRAWLER_NAMESPACE + namespace + "currentCrawlUrl" , currentCrawlUrl) ;
                            } catch( Exception e){
                                 logger. error( "Jedis error" , e) ;
                            } finally{
                                 jedisPool. returnResource (jedis );
                            }
                      }
                      else {
                            logger. error( "解析抓取的url頁面失敗 :" + newsRes. getMessage()) ;
                      }
                      generateNextUrl (newsRes );
                 } while( !newsRes .isEndCrawlFlag () && index > 0) ;
//                   }while(!newsRes.isEndCrawlFlag());
            } catch( Exception e){
                 logger. error( "failed!" ,e );
                 intervalTime = 60 *60 *1000 ;
                 Jedis jedis = null;
                 try{
                      jedis = jedisPool. getResource ();
                      jedis. set( RedisConstant .CRAWLER_NAMESPACE + namespace + "state" , String .valueOf (2 ));
                 } catch( Exception e1){
                      logger. error( "Jedis error" , e1) ;
                 } finally{
                      jedisPool. returnResource (jedis );
                 }
            }
            try {
                 state = StateEnum .Sleeping ;
                 Thread .sleep (intervalTime );
            } catch ( InterruptedException e) {
                 e. printStackTrace ();
            }
      }
}

程式碼解釋:
crawl方法是這一類爬蟲的核心程式碼,每隔一段指定時間,它會抓取一次,考慮更新時間的問題,爬蟲是從後往前抓取,即先抓取比較早的資料,首次抓取和第二次抓取設定不同的抓取深度即可(PS:採用這種簡單粗暴的方式,能夠滿足我的實際需求,關注的是較新的內容,並且允許可能存在的丟失問題)
整個步驟如下:

  1. 抓取當前列表頁;
  2. 分析頁面結構,用Jsoup解析出待抓取資訊列表,迴圈處理列表每一條資訊;
    2.1、讀取當前資訊url地址,根據去重表判斷是否已抓取過,如果沒有,繼續執行下一步;否則,處理下一條記錄;
    2.2、解析獲取它的標題,並通過它的連結抓取正文資訊,並解析其中的圖片地址,呼叫圖片伺服器介面下載、儲存該圖片,並返回圖片url;
    2.3、將解析到的內容包裝成一個寫入物件,加入阻塞佇列中;
    2.4、迴圈2.1 - 2.3,直到列表末尾;
  3. 判斷是否滿足結束迴圈條件,是,則結束本次抓取過程,讓執行緒進入睡眠狀態;否則,生成下一條抓取記錄;

上述程式碼邏輯很簡單,不過它能夠滿足當前的需求。同時,由於開銷小,重複訪問站點頻次低,幾乎不會被目標站點遮蔽(一直執行,目前尚未被任何一個站點遮蔽),並且能夠保證更新內容在數分鐘內同步抓取。結構簡單使得程式碼維護起來非常輕鬆;

在實際應用中還會遇到一些小問題,也必須要妥善解決,例如:
1、如果某個站點的資料因為網站結構,程式bug等等導致資料失效,你要能夠及時的修復;
2、由於目標站點可能會出現各種網路異常,結構更改等等,因此,需要一個實時監控頁面,檢視各個爬蟲的實時資料是否正常(這裡可以有很多優化的地方,不過由於太耗時間,現在只是把一些資料展現出來);

總結:
在實際的專案中,初期,根據業務需要,儘可能考慮寫一個滿足當前業務需要的系統。不要追求各種高效能;
當然,我說的簡單,以滿足當前需求為前提,但是一些良好的工程習慣,如面向介面程式設計,設計類或者模組考慮“高內聚,低耦合”原則,以及程式碼的可擴充套件性,可讀性等等都是需要你考慮的,這既是對專案負責,也可以讓自己養成一個良好程式設計習慣。
用程式碼中的去重表實現來舉例:
我在專案中將去重表作為一個類實現,因為去重表看似簡單,但根據業務需求還是有很多不同的實現方式;如果你抓取的內容不多,那簡單的一個hash表就滿足你的要求了;為了避免程式重啟或崩潰導致hash表資料丟失,你可能會用redis或其它nosql來實現;如果到後來你發現抓取的內容非常多,記憶體空間已經不夠用了,可以更進一步,使用bloomfilter來實現,它可以大大減少記憶體開銷,代價是存在小概率的誤判,並且實現一個穩定可用的bloomfilter需要花費一定的時間成本;