1. 程式人生 > >[原創]一款小巧、靈活的Java多執行緒爬蟲框架(AiPa)

[原創]一款小巧、靈活的Java多執行緒爬蟲框架(AiPa)

1.作品簡介

AiPa 是一款小巧,靈活,擴充套件性高的多執行緒爬蟲框架。

AiPa 依賴當下最簡單的HTML解析器Jsoup。

AiPa 只需要使用者提供網址集合,即可在多執行緒下自動爬取,並對一些異常進行處理。

2.下載安裝

AiPa是一個小巧的、只有390KB的jar包。

下載該Jar包匯入到你的專案中即可使用。

請前往Git下載該jar包。

Github:

Giree:

3.如何使用

先來看下一個簡單完整的示例程式:

public class MyAiPaWorker implements AiPaWorker {

    @Override
    public String run(Document doc) {
        //使用JSOUP進行HTML解析獲取想要的div節點和屬性
        //儲存在資料庫或本地檔案中
        return doc.title() + doc.body().text();
    }

    @Override
    public Boolean fail(String link) {
        //任務執行失敗
        //可以記錄失敗網址
        //記錄日誌
        return false;
    }
}
    public static void main(String[] args) throws InstantiationException, IllegalAccessException, ExecutionException, InterruptedException {
        //準備網址集合
        List<String> linkList = new ArrayList<>();
        linkList.add("http://jb39.com/jibing/FeiQiZhong265988.htm");
        linkList.add("http://jb39.com/jibing/XiaoErGuoDu262953.htm");
        linkList.add("http://jb39.com/jibing/XinShengErShiFei250995.htm");
        linkList.add("http://jb39.com/jibing/GaoYuanFeiShuiZhong260310.htm");
        linkList.add("http://jb39.com/zhengzhuang/LuoYin337449.htm");
        //第一步:新建AiPa例項
        AiPaExecutor aiPaExecutor = AiPa.newInstance(new MyAiPaWorker()).setCharset(Charset.forName("GBK"));
        //第二步:提交任務
        for (int i = 0; i < 10; i++) {
            aiPaExecutor.submit(linkList);
        }
        //第三步:讀取返回值
        List<Future> futureList = aiPaExecutor.getFutureList();
        for (int i = 0; i < futureList.size(); i++) {
            //get() 方法會阻塞當前執行緒直到獲取返回值
            System.out.println(futureList.get(i).get());
        }
        //第四步:關閉執行緒池
        aiPaExecutor.shutdown();
    }

通過AiPa.newInstance()方法直接建立一個新的AiPa例項,該方法必須要傳入 AiPaWorker 介面的實現類。

3.1 AiPaWorker介面

AiPaWorker 介面是使用者必須要實現的業務類。

該介面方法如下:

public interface AiPaWorker<T,S> {
    /**
     * 如何解析爬下來的HTML文件?
     * @param doc JSOUP提供的文件
     * @return
     */
    T run(Document doc);

    /**
     * run方法異常則執行fail方法
     * @param link 網址
     * @return
     */
    S fail(String link);
}

run()方法是使用者自定義處理爬取的HTML內容,一般是利用Jsoup的Document類進行解析,獲取節點或屬性等,然後儲存到資料庫或本地檔案中。

fail()方法是當run()方法出現異常或爬取網頁時異常,多次處理無效的情況下進入的方法,該方法的引數為此次出錯的網址。一般是對其進行日誌記錄等操作。

3.2 解碼,最多失敗次數,請求頭

通過AiPa獲取例項後,可以直接在後面跟著設定一大堆屬性,比如:setCharset、setThreads、setMaxFailCount等,這些屬性啥意思,下面以表格的形式說明一下:

方法 說明
setThreads 工作執行緒數,預設CPU數量+1,你也可以設定CPU*2等等
setMaxFailCount 最大失敗次數,也就是爬網站出現異常,再次爬一共嘗試多少次,預設5
setCharset 網頁的編碼,碰到亂碼設定這個,預設UTF-8
setHeader 設定請求頭,只接受Map<String,String>型別,預設null
setMethod 設定請求方法,預設Method.GET
setTimeout 請求解析的等待時間,預設30秒。
setUserAgent 設定請求的UA,預設電腦版。

上面的一般情況下夠用了,如果對這些不滿意,嫌太少啥的,下面給了更優秀的解決方案。

3.3 自定義爬蟲類

在上面的演示程式中,我們使用了submit()方法進行提交任務,預設是使用了Jsoup+上面的那些非加粗屬性進行爬取,一般情況下夠用,如果要一個一個的擴充套件Jsoup的方法太累了,於是我想了個偷懶的方法,我把爬蟲方法設定為可繼承的,提供給使用者傳入自定義類的介面,讓使用者自己去擴充套件,想用什麼爬,想設定什麼屬性都可以。

下面看下使用Demo:

public class MyCallable extends AiPaCallable {

    @Override
    protected Document getHtmlDocument(String link) throws IOException {
        // 你可以不用JSOUP,可以使用其它方法進行HTTP請求,但最後需要轉為Document格式
        // 你也可以使用Jsoup實現定製屬性
        Connection connection = Jsoup.connect(link).method(Connection.Method.GET);
        String body = connection.execute().charset("GBK").body();
        
        return Jsoup.parse(body);
    }

}

然後,再呼叫submit方法提交任務,程式碼示例:

aiPaExecutor.submit(linkList, MyCallable.class);

注意:當你重寫爬蟲方法後,3.2小節的非加粗屬性都會失效。

3.3 讀取返回值與獲取執行緒池

如果你想要讀取返回值來看下任務是否執行成功,你可以使用看下上面的程示例序是如何做的。

public List<Future> getFutureList()

getFutureList()方法會返回任務執行之後的結果集合,集合中的成員都是Future類。呼叫Future物件的 get() 方法會等待當前任務執行完成再返回結果值,也就是會阻塞當前執行緒。該類還有很多方法,比如get(long timeout, TimeUnit unit),設定等待時間等等。

public ExecutorService getExecutor()

該方法會返回AiPa當前使用的Executor執行緒池,你獲取到該執行緒池後,需要一些使用執行緒池的一些方法可以自行使用。

3.4 如何應對爬取網頁時的異常

對於網頁爬取時的異常,這真的是個痛點。原因真的很多,你的網路不行,網站伺服器的網路不行,在網上有說把請求頭中Connection設定為close,不用keep-alive。這個以我爬取幾百兆資料的經驗告訴你,然並卵。

於是我想出了一種無賴打法,反覆爬。爬一次不行就兩次,爬兩次不行就三次,只要網頁是可以正常響應的,基本這個策略沒多少問題。當然,萬一真的是某個網頁就那麼獨樹一幟呢,所以我們設定一個最大值,對於爬取超過最大值的,放棄記錄下來,看看啥子情況。在我的這個框架中,也給出了fail()方法專門處理這個問題。

4.測試用例

在Java SE測試中。沒有使用資料庫等,直接控制檯列印是沒問題的。

在Spring Boot中寫了個測試用例,爬取資料儲存到資料庫,執行也沒問題。

@RunWith(SpringRunner.class)
@SpringBootTest
public class InterApplicationTests {

    @Autowired
    private DemoResponse demoResponse;

    @Test
    public void context() throws ExecutionException, InterruptedException {
        AiPaExecutor executor = AiPa.newInstance(new AiPaWorker() {
            @Override
            public Boolean run(Document document) {
                String title = document.title();
                demoResponse.save(new DemoEntity(title));
                return true;
            }

            @Override
            public Boolean fail(String s) {
                demoResponse.save(new DemoEntity(s));
                return false;
            }
        }).setCharset(Charset.forName("GBK"));

        List<String> linkList = new ArrayList<>();
        linkList.add("http://jb39.com/jibing/FeiQiZhong265988.htm");
        linkList.add("http://jb39.com/jibing/XiaoErGuoDu262953.htm");
        linkList.add("http://jb39.com/jibing/XinShengErShiFei250995.htm");
        linkList.add("http://jb39.com/jibing/GaoYuanFeiShuiZhong260310.htm");
        linkList.add("http://jb39.com/zhengzhuang/LuoYin337449.htm");
        executor.submit(linkList);

        List<Future> list = executor.getFutureList();
        for (int i = 0; i < list.size(); i++) {
            //get() 方法會阻塞當前執行緒直到獲取返回值
            System.out.println(list.get(i).get());
        }
        executor.shutdown();
    }

}

執行結果:

Hibernate: insert into demo (title) values (?)
Hibernate: insert into demo (title) values (?)
Hibernate: insert into demo (title) values (?)
Hibernate: insert into demo (title) values (?)
Hibernate: insert into demo (title) values (?)

5.關於作者

由於作者水平有限,框架一定存在一些漏洞或不足,希望各位專家、大佬提出批評指正!