[原創]一款小巧、靈活的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.關於作者
由於作者水平有限,框架一定存在一些漏洞或不足,希望各位專家、大佬提出批評指正!