java爬蟲框架webmagic-core-0.5.3源碼分析

webmagic框架使用java語言,抽象出爬蟲常用操作,封裝了爬蟲中使用頻繁的庫類,使整個爬取邏輯更加清晰有條理。

關於webmagic的使用指南在官網描述的很清楚,簡單介紹下項目結構。

先看下官網的一個流程圖介紹:


這裏展現了爬蟲的4個組件,Scheduler、Downloader、PageProcessor、Pipeline,它們相互獨立,協同合作。它們直接通過Request、Page、ResultItem來進行通信。

再看看webmagic-core源碼的結構:

  1. Page.java
  2. Request.java
  3. ResultItems.java
  4. Site.java
  5. Spider.java
  6. SpiderListener.java
  7. Task.java

Downloader

    • AbstractDownloader.java
    • Downloader.java
    • HttpClientDownloader.java
    • HttpClientGenerator.java
Scheduler
    • DuplicateRemovedScheduler.java
    • MonitorableScheduler.java
    • PriorityScheduler.java
    • QueueScheduler.java
    • Scheduler.java
Pipeline
    • CollectorPipeline.java
    • ConsolePipeline.java
    • FilePipeline.java        
    • Pipeline.java                
    • ResultItemsCollectorPipeline.java
Processor
    • PageProcessor.java
    • SimplePageProcessor.java
Proxy
    • Proxy.java                
    • ProxyPool.java        
    • SimpleProxyPool.java
Selector
    • AbstractSelectable.java        
    • CssSelector.java                
    • HtmlNode.java                        
    • OrSelector.java                        
    • RegexSelector.java                
    • Selector.java                        
    • XpathSelector.java
    • AndSelector.java                
    • ElementSelector.java                
    • Json.java                        
    • PlainText.java                        
    • ReplaceSelector.java                
    • Selectors.java                
    • BaseElementSelector.java        
    • Html.java                        
    • JsonPathSelector.java                
    • RegexResult.java                
    • Selectable.java                        
    • SmartContentSelector.java
Thread
    • CountableThreadPool.java
Utils
    • Experimental.java        
    • FilePersistentBase.java        
    • HttpConstant.java        
    • NumberUtils.java        
    • ProxyUtils.java                
    • UrlUtils.java

Spider是實現了Task,創建一個Spider就是新建一個任務,這個類中調用其他組件,是整體爬取流程的核心。

Pagedownloader下載頁面後的保存結果,之後要將Page傳入processor中根據自己需求進行處理。

Site是對爬取頁面進行配置,如設置起始頁地址,重試次數,重試間隔等等。

ResultItemsPage處理之後的結果,之後要將ResultItems傳入Pipeline中進行結果處理。

SpiderListener是對Spider進行監聽,處理成功還是失敗。利用這個功能,你可以查看爬蟲的執行情況——已經下載了多少頁面、還有多少頁面、啓動了多少線程等信息。該功能通過JMX實現,你可以使用Jconsole等JMX工具查看本地或者遠程的爬蟲信息。

Request是對一次請求的抽象,其中包含請求的url和method等屬性。

downloader下載器,默認HttpClientDownloader。

secheduler負責待抓取urls隊列的維護。

pipeline主要負責對結果ResultItems的處理,默認ConsolePipeline,(可由用戶自定義)。

processor負責解析頁面,抽取結果保存到ResultItems,(可由用戶自定義)。

proxy代理相關配置。

selector頁面元素選擇器,包括css、正則、xpath等等。

thread線程池管理。

utils一些工具類。


這裏以maven安裝方式,舉一個簡單的例子來描述各個類的作用。

以爬取這個網址的列表爲例http://www.dailianmeng.com/p2pblacklist/index.html,我們需要把這個列表全部爬取並保存,使用webmagic只需要新建三個文件。

App.java

public class App{
    Logger logger;

    App() {
        InputStream inputStream = App.class.getResourceAsStream("/log4j.properties");
        if (inputStream != null)
            PropertyConfigurator.configure(inputStream);
        else
            BasicConfigurator.configure();

        logger = LoggerFactory.getLogger(getClass());
    }

    public static void main(String[] args) {
        App app = new App();
        app.run();
    }

    void run() {

        logger.info("spider for dlm start....");

        Spider.create(new DLMPageprocessor())
                .addUrl(DLMPageprocessor.START_PAGE)
                .addPipeline(new DLMPipeline())
                .addPipeline(new ConsolePipeline())
                .thread(5)
                .run();
    }
}


DLMPageprocessor.java

package com.xxx.dailianmeng;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Selectable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class DLMPageprocessor implements PageProcessor {
    private Site site = Site.me().setRetryTimes(3).setSleepTime(1000);
    static Logger logger = LoggerFactory.getLogger(DLMPageprocessor.class);
    public static String START_PAGE = "http://www.dailianmeng.com/p2pblacklist/index.html";

    @Override
    public void process(Page page) {
        page.addTargetRequests(page.getHtml().links().regex("/p2pblacklist/index.html\\?P2pBlacklist_page=\\w+").all());

        ArrayList list = new ArrayList();

        Selectable selectable = page.getHtml().xpath("//div[@class='table-responsive']//table//tbody//tr");
        for (Selectable sel : selectable.nodes()) {
            String[] str = new String[10];
            sel.xpath("//td/html()").all().toArray(str);

            Map map = new HashMap<>();
            map.put("name", str[0].replace(" ", ""));
            map.put("idcard", str[1].replace(" ", ""));
            map.put("mobile", str[2].replace(" ", ""));
            map.put("addr", str[3].replace(" ", ""));
            map.put("totalAmount", str[4].replace(" ", "0.00"));
            map.put("paidAmount", str[5].replace(" ", "0.00"));
            map.put("unpaidAmount", str[6].replace(" ", "0.00"));
            map.put("debtDate", str[7].replace(" ", "0000-00-00"));
            map.put("debtTerms", str[8].replace(" ", ""));
            map.put("extId", sel.xpath("//td//a").links().get());

            list.add(map);
        }

        page.putField("mysql", list);
    }

    @Override
    public Site getSite() {
        return site;
    }
}


DLMPipeline.java

package com.xxx.dailianmeng;

import com.u51.xsg.mysql.Mysql;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;


public class DLMPipeline implements Pipeline {
    Logger logger = LoggerFactory.getLogger(DLMPipeline.class);

    public void process(ResultItems resultItems, Task task) {
        Iterator iterator = resultItems.getAll().entrySet().iterator();

        while (iterator.hasNext()) {
            //處理結果
        }
    }
}



爬取操作非常簡單,App.java中一個main函數,Pageprocessor對爬取的頁面進行處理,Pipeline對處理結果進行操作,用戶只需關心業務邏輯,而不用關心爬蟲實現。

那麼在簡單操作的背後,具體是怎麼實現的呢?

首先看App.java中創建了一個Spider對象,Spider也控制着整個爬取流程。所以我們可以看下Spider的run方法裏都做了什麼。

@Override
    public void run() {
        checkRunningStat();
        initComponent();
        logger.info("Spider " + getUUID() + " started!");
        while (!Thread.currentThread().isInterrupted() && stat.get() == STAT_RUNNING) {
            Request request = scheduler.poll(this);
            if (request == null) {
                if (threadPool.getThreadAlive() == 0 && exitWhenComplete) {
                    break;
                }
                // wait until new url added
                waitNewUrl();
            } else {
                final Request requestFinal = request;
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            processRequest(requestFinal);
                            onSuccess(requestFinal);
                        } catch (Exception e) {
                            onError(requestFinal);
                            logger.error("process request " + requestFinal + " error", e);
                        } finally {
                            if (site.getHttpProxyPool()!=null && site.getHttpProxyPool().isEnable()) {
                                site.returnHttpProxyToPool((HttpHost) requestFinal.getExtra(Request.PROXY), (Integer) requestFinal
                                        .getExtra(Request.STATUS_CODE));
                            }
                            pageCount.incrementAndGet();
                            signalNewUrl();
                        }
                    }
                });
            }
        }
        stat.set(STAT_STOPPED);
        // release some resources
        if (destroyWhenExit) {
            close();
        }
    }
run方法總大致分成倆步操作,先初始化各個組件initComponent(),再從線程池中取一個Request出來處理。

1、initComponent():

protected void initComponent() {
        if (downloader == null) {
            this.downloader = new HttpClientDownloader();
        }
        if (pipelines.isEmpty()) {
            pipelines.add(new ConsolePipeline());
        }
        downloader.setThread(threadNum);
        if (threadPool == null || threadPool.isShutdown()) {
            if (executorService != null && !executorService.isShutdown()) {
                threadPool = new CountableThreadPool(threadNum, executorService);
            } else {
                threadPool = new CountableThreadPool(threadNum);
            }
        }
        if (startRequests != null) {
            for (Request request : startRequests) {
                scheduler.push(request, this);
            }
            startRequests.clear();
        }
        startTime = new Date();
    }
初始化默認的downloader,添加pipelines默認爲ConsolePipeline(),初始化線程池,如果設置了startRequests就會把裏面的request推入schedule隊列中。
2、如果當前線程沒有被打斷,而且狀態是RUNNING那麼就從schedule去一個request出來處理,如果schedule取出的request是空,那麼等待新的請求加進來,如果設置了沒任務就結束爬取進程那麼此時就break結束。

在線程池中啓用一個線程來進行爬取,執行processRequest()方法。

protected void processRequest(Request request) {
        Page page = downloader.download(request, this);
        if (page == null) {
            sleep(site.getRetrySleepTime());
            onError(request);
            return;
        }
        // for cycle retry
        if (page.isNeedCycleRetry()) {
            extractAndAddRequests(page, true);
            sleep(site.getRetrySleepTime());
            return;
        }
        pageProcessor.process(page);
        extractAndAddRequests(page, spawnUrl);
        if (!page.getResultItems().isSkip()) {
            for (Pipeline pipeline : pipelines) {
                pipeline.process(page.getResultItems(), this);
            }
        }
        //for proxy status management
        request.putExtra(Request.STATUS_CODE, page.getStatusCode());
        sleep(site.getSleepTime());
    }

首先調用downloader下載頁面,如果需要週期下載,那麼將頁面中的地址提取出來添加到待抓取隊列。

然後將page丟給pageProcessor的process方法,上面這個例子我們的pageProcessor = new DLMPageprocessor(),在process方法中

page.addTargetRequests(page.getHtml().links().regex("/p2pblacklist/index.html\\?P2pBlacklist_page=\\w+").all());

我們將下一步需要爬取的地址存入targetRequests變量中,這裏再用extractAndAddRequests()方法從targetRequests變量中提取地址,放入到schedule中。

process方法中的

page.putField("mysql", list);

會將處理的結果存入到ResultItems變量裏,這裏循環pipelines將Resultem和當前Spider丟個各個註冊的pipeline進行處理,這裏便是能註冊多個pipeline的原因。

最後將請求狀態碼存入Request.extras變量中, 對代理管理有用。


到這裏簡單的介紹了一下爬取的流程,現在總結以下幾個點。

1、所謂的pageprocessor、pipeline組件化就是將這些對象設爲可變的變量,定義統一接口。

2、爬取的流程都在Spider類中,邏輯比較清晰不復雜。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章