爬蟲框架Webmagic原始碼分析之Spider

xbynet發表於2017-03-24

本系列文章,針對Webmagic 0.6.1版本

一個普通爬蟲啟動程式碼

public static void main(String[] args) {
    Spider.create(new GithubRepoPageProcessor())
            從https:github.com/code4craft開始抓    
            .addUrl("https://github.com/code4craft")
            //設定Scheduler,使用Redis來管理URL佇列
            .setScheduler(new RedisScheduler("localhost"))
            //設定Pipeline,將結果以json方式儲存到檔案
            .addPipeline(new JsonFilePipeline("D:\\data\\webmagic"))
            //開啟5個執行緒同時執行
            .thread(5)
            //啟動爬蟲
            .run();
}

10214101_11Dn.png

1、spider可配置插拔元件:

Downloader 提供自定義的Downloader,預設為HttpClientDownloader
Pipeline 提供自定義的Pipeline,可以配置多個,多個Pipeline鏈式處理結果。預設為ConsolePipeline
Scheduler 提供自定義的排程器,預設為QueueScheduler
PageProcessor 頁面處理元件,開發者爬蟲的實現
ExecutorService 可以用於提供自己實現的執行緒池來監控,預設為Fixed ExecutorService
SpiderListener 頁面狀態監聽器,提供每個頁面成功和錯誤的回撥。可配置多個。

其中有:WebMagic四大元件:Pipeline,Scheduler,Downloader和PageProcesser 。這和Python中的Scrapy的理念是一致的。但是Scrapy還有一些中介軟體的概念,從結構圖中便可以看出區別

scrapy_backbone.png

2、狀態變數:

stat 0,初始化;1,執行中;2,已停止
pageCount 已經抓取的頁面數。注意:這裡統計的是GET請求的頁面,POST請求的頁面不在統計的範圍之內。具體原因見DuplicateRemovedScheduler類
startTime:開始時間,可用於計算耗時。
emptySleepTime 最大空閒等待時間,預設30s。如果抓取佇列為空,且url佇列為空的最大等待時長,超過該時間,就認為爬蟲抓取完成,停止執行。
threadNum : 啟用的執行緒數,預設1.
threadPool:這是Webmagic提供的CountableThreadPool例項,內部封裝了ExecutorService,CountableThreadPool 提供了額外的獲取執行緒執行數的方法,此外為防止大量urls入池等待,提供了阻塞方式管理urls入池。(後續細說)
destroyWhenExit:預設true。是否在呼叫stop()時立即停止所有任務並退出。
spawUrl : 預設為true,是否抓取除了入口頁面starturls之外的其他頁面(targetRequests).

3、需要配置的項:

Site 全域性站點配置,如UA,timeout,sleep等
PageProcessor 頁面處理元件,開發者爬蟲的實現
Request 配置入口頁面url,可以多個。
uuid ,可選,Spider的名字,用於分析和日誌。

需要注意的是:每個修改配置的方法都進行了checkIfRunning檢查,如果檢查當前Spider正在執行,它會丟擲IllegalStateException。

所有配置方法都return this,便於鏈式呼叫,類似於builder模式。

4、執行方式:

Spider實現了Runnable介面(還有一個Webmagic自己的Task介面)。
run(),跟普通的Runnable一樣,阻塞式執行,會阻塞當前執行緒直至Spider執行結束。
runAsync(),就是new一個Thread來執行當前Spider這個Runnable,非同步執行。
start(),runAsync()的別名方法,非同步執行。

5、狀態相關方法

stop(),結束當前爬蟲的執行,內部只是簡單地修改一下狀態,如果設定了destroyWhenExit=true(預設就是true)那麼會立即停止所有任務並清除資源,否則並不會停止正線上程池中執行的執行緒,也不會銷燬執行緒池。

getThreadAlive() 獲取正在執行的執行緒數,用於狀態監控。

6、核心程式碼分析

public void run() {
        checkRunningStat();
        initComponent();
        logger.info("Spider " + getUUID() + " started!");
        while (!Thread.currentThread().isInterrupted() && stat.get() == STAT_RUNNING) {
            final Request request = scheduler.poll(this);
            if (request == null) {
                if (threadPool.getThreadAlive() == 0 && exitWhenComplete) {
                    break;
                }
                // wait until new url added
                waitNewUrl();
            } else {
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            processRequest(request);
                            onSuccess(request);
                        } catch (Exception e) {
                            onError(request);
                            logger.error("process request " + request + " error", e);
                        } finally {
                            pageCount.incrementAndGet();
                            signalNewUrl();
                        }
                    }
                });
            }
        }
        stat.set(STAT_STOPPED);
        // release some resources
        if (destroyWhenExit) {
            close();
        }
    }

首先通過checkRunningStat()來檢查並設定執行狀態,如果已經在執行了,那麼會丟擲IllegalStateException。之後初始化元件(主要是初始化Downloader、執行緒池、將starturls push到Scheduler中,初始化開始時間等)。之後進入迴圈,從scheduler中poll出Request給執行緒池去執行。如果scheduler中沒有request了:繼而判斷是否有執行緒在執行和是否設定了立即退出標誌,如果設定了立即退出迴圈,否則呼叫waitNewUrl()等待有新的url被加入。
waitNewUrl()採用RetreentLock和Condition來進行超時阻塞,一旦阻塞時間超過emptySleepTime就返回。如果執行緒池中執行執行緒數量為0,並且exitWhenComplete=true(預設),那麼就停止退出,結束爬蟲。如果exitWhenComplete=false,那麼需要開發者手動呼叫stop()來停止退出爬蟲,並呼叫close()來清理資源。

通過processRequest來處理抓取url的整個流程,程式碼如下:

protected void processRequest(Request request) {
        Page page = downloader.download(request, this);
        if (page == null) {
            sleep(site.getSleepTime());
            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(Page代表了一個頁面),然後判斷是否需要重試(needCycleRetry標誌會在downloader下載頁面發生異常時被設定為true,同時會把自己本身request加到targetRequests當中),如果需要,則抽取targetRequests到scheduler當中。如果都沒問題,繼續呼叫我們實現的頁面處理器進行處理,之後再抽取我們在頁面處理器中放入的targetRequests(即需要繼續抓取的url)到scheduler當中。之後便是呼叫pipeline進行處理(一般做持久化操作,寫到資料庫、檔案之類的),但是如果我們在頁面處理器中為page設定了skip標誌,那麼就不會呼叫pipeline進行處理。
當然其中還包括一些重試休眠時間、繼續抓取等待時間等來更好地控制爬蟲抓取頻率。

說完processRequest,我們回到run()繼續分析,處理完之後,就是呼叫監聽器,告訴其成功還是失敗,最後抓取數加+1,然後通知新url被加入(通知waitNewUrl()可以返回繼續了)。

需要說明的一點是,Spider類中的狀態管理大量用到了Jdk Atomic原子包下的CAS併發原子類。

7、CountableThreadPool

前面說過Spider採用的執行緒池物件CountableThreadPool內部封裝了ExecutorService,CountableThreadPool 提供了額外的獲取執行緒執行數的方法,此外為防止大量urls入池等待,提供了阻塞方式管理urls入池。
阻塞方式的實現是通過ReentrantLock和它的Condition來實現的。具體程式碼如下:

public void execute(final Runnable runnable) {
        if (threadAlive.get() >= threadNum) {
            try {
                reentrantLock.lock();
                while (threadAlive.get() >= threadNum) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                    }
                }
            } finally {
                reentrantLock.unlock();
            }
        }
        threadAlive.incrementAndGet();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    runnable.run();
                } finally {
                    try {
                        reentrantLock.lock();
                        threadAlive.decrementAndGet();
                        condition.signal();
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            }
        });
    }

邏輯是這樣的,如果正在執行的執行緒數threadAlive超過允許的執行緒數,就阻塞等待,直至收到某個執行緒結束通知。

羅嗦一句,這裡的執行緒安全控制,主要是用到了JDK atomic包來表示狀態和ReentrantLock、Condition來控制達到類似生產者消費者的阻塞機制。

關於Spider就分析到這裡,後續主題待定。

相關文章