1. 程式人生 > >Java網路爬蟲(十)--使用多執行緒提升爬蟲效能的思路小結

Java網路爬蟲(十)--使用多執行緒提升爬蟲效能的思路小結

在開始說正事之前我先給大家介紹一下這份程式碼的背景,以免大家有一種霧裡看花的感覺。在本系列的前幾篇部落格中有一篇是用多執行緒進行百度圖片的抓取,但是當時使用的多執行緒是非常粗略的,只是開了幾個執行緒讓抓取的速度提升了一些(其實提升了很多),初步的使用了一下執行緒,這篇部落格將執行緒的使用進行了一些深入。

專案背景

博主這次的需求是抓取一些淘寶的資料,在此之前我們需要掌握基本的並行爬蟲的相關知識。在這裡我要先吐槽一下《自己動手寫網路爬蟲》這本書,不得不說,這本書讓我認識到了什麼叫做:有一本好書,真的會提升很多學習的效率。反正這本書不適合入門,而且非常的老,程式碼都不能用!!雖然其中有一些值得學習的思想,但。。。

本次程式碼仍有很多不完善的地方,不得不說Java網路爬蟲的學習曲線還是很陡峭的,但我認為開發一個好爬蟲是需要紮實的語言功底,所以學習Java爬蟲還是很值得的。

程式碼思想與實現

對於並行爬蟲而言,處理空佇列要比處理序列爬蟲更加複雜,空的佇列並不意味著爬蟲已經完成工作,因為此刻其他的程序或執行緒可能依然在解析網頁,並且馬上會產生新的URL。程序或者執行緒管理者需要給報告佇列為空的執行緒傳送臨時的休眠訊號,執行緒管理員需要不斷追蹤休眠執行緒的數目,只有當所有的執行緒都休眠的時候,爬蟲才可以終止。

接下來就看一下具體的程式碼:

我們假設從Redis資料庫中的爬蟲佇列裡取待解析的URL。

主執行緒:

package multithreading;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by hg_yi on 17-6-13.
 *
 * 執行緒計數器是一個共享變數
 */

public class MultithreadCrawler{
    public static void main(String[] args) throws InterruptedException {
        //建立一個收集執行緒的列表
        List<Thread> threadList = new
ArrayList<Thread>(); //建立執行緒的個數 int threadNum = 5; RunThread run = new RunThread(); run.setThreads(threadNum); //建立5個執行緒,並對其進行收集 for (int i = 0; i < threadNum; i++) { Thread thread = new Thread(run); thread.start(); threadList.add(thread); } //main執行緒需要等待所有子執行緒退出 Thread.currentThread().join(); } }

程式碼分析:

可以看到,我建立了5個執行緒。我使用了一個容器將所有建立的執行緒進行了收集,然後為了防止主執行緒提前退出而讓所有子執行緒結束,我告知主執行緒需要等待每一個子執行緒執行完畢之後,你主執行緒才可以結束。

然後在for迴圈中我讓每個子執行緒都執行RunThread類中的run方法,這樣操作的目的主要是考慮到了執行緒之間資料共享的問題。

執行執行緒:

package multithreading;

import redisqueue.RedisQueue;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by hg_yi on 17-6-13.
 *
 * 我們只保證在給資料庫中寫入URL,還有改變執行緒執行緒計數器的值的時候,是需要同步的。
 *
 * 執行緒計數器threads是所有執行緒共享的。
 */

public class RunThread extends Thread {
    //執行緒計數器需要對所有執行緒可見,是共享變數
    int threads = 0;
    //Redis佇列的物件,也是所有物件共享的變數
    RedisQueue redisQueue = new RedisQueue();
    //建立執行緒鎖
    private static Object lock = new Object();

    public void setThreads(int threads) {
        this.threads = threads;
    }

    public void parseToVisitUrltoRedis() throws Exception {
        //用來儲存新提取出來的url列表
        List<String> urlList = new ArrayList<String>();

        while (true) {
            //從爬蟲佇列中取出待抓取的url
            if (!redisQueue.toVisitIsEmpty()) {
                String url = redisQueue.getToVisit();

                /**
                 * 對此url進行解析,提取出新的url列表解析出來的url順便就寫進urlList中了
                 * 在這個過程中不要求保證同步,每個執行緒都負責解析自己所屬的url,解析完成
                 * 之後將url寫入自己的urlList之中,當在解析過程中發生阻塞,則切換到其他
                 * 執行緒,保證程式的高併發性。
                 */

                /**
                 * 在此同步塊中主要進行提取出來的URL的寫操作,必須是同步操作,保證一個同一時間只有一個執行緒在對Redis資料庫進行寫操作。
                 */
            } else {
                //在改變執行緒計數器的值的時候必須保證執行緒的同步性
                synchronized (lock) {
                    threads--;
                    //如果仍然有其他執行緒在活動,則通知此執行緒進行等待
                    if (threads > 0) {
                        /*呼叫執行緒的wait方法會將此執行緒掛起,直到有其他執行緒呼叫notify\notifyAll將此執行緒進行喚醒*/
                        wait();
                        threads++;
                    } else {
                        //如果其他的執行緒都在等待,說明待抓取佇列已空,則通知所有執行緒進行退出
                        notifyAll();
                        return;
                    }
                }
            }
        }
    }

    public void run() { 
        try {
            parseToVisitUrltoRedis();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在run方法中我們主要實現的就是在從Redis資料庫中的URL佇列中提取到當前需要抓取的URL並對其進行解析,將新URL再新增到佇列中。可以看到,通過引入一個執行緒計數器,我們解決了上述問題。