一款小巧靈活的Java多線程爬蟲框架(AiPa)

1.簡介

AiPa 是一款小巧,靈活,擴展性高的多線程爬蟲框架。git

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

AiPa 只須要使用者提供網址集合,便可在多線程下自動爬取,並對一些異常進行處理。數據庫

2.Maven

直接引入服務器

<dependency>
    <groupId>cn.yueshutong</groupId>
    <artifactId>AiPa</artifactId>
    <version>1.0.0.RELEASE</version>
</dependency>

3.如何使用

先來看下一個簡單完整的示例程序:網絡

必須實現的接口多線程

public class MyAiPaWorker implements AiPaWorker {

    @Override
    public String run(Document doc, AiPaUtil util) {
        //使用JSOUP進行HTML解析獲取想要的div節點和屬性
        //保存在數據庫或本地文件中
        //新增aiPaUtil工具類能夠再次請求網址
        return doc.title() + doc.body().text();
    }

    @Override
    public Boolean fail(String link) {
        //任務執行失敗
        //能夠記錄失敗網址
        //記錄日誌
        return false;
    }
}

main方法框架

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 接口的實現類。maven

3.1 AiPaWorker接口

AiPaWorker 接口是用戶必需要實現的業務類。ide

該接口方法以下:工具

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

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

run()方法是用戶自定義處理爬取的HTML內容,通常是利用Jsoup的Document類進行解析,獲取節點或屬性等,而後保存到數據庫或本地文件中。若是在業務方法須要再次請求URL,可使用工具類Util。

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 MyAiPaUtil extends AiPaUtil {

    @Override
    public 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, MyAiPaUtil.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, AiPaUtil util) {
                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.關於做者

因爲做者水平有限,框架必定存在一些漏洞或不足,但願各位專家、大佬提出批評指正!

個人博客:https://yueshutong.cnblogs.com/

Github:https://github.com/yueshutong/AIPa

Giree:https://gitee.com/zyzpp/AIPa

交流QQ羣:781927207

相關文章
相關標籤/搜索