AiPa 是一款小巧,靈活,擴展性高的多線程爬蟲框架。git
AiPa 依賴當下最簡單的HTML解析器Jsoup。github
AiPa 只須要使用者提供網址集合,便可在多線程下自動爬取,並對一些異常進行處理。數據庫
直接引入服務器
<dependency> <groupId>cn.yueshutong</groupId> <artifactId>AiPa</artifactId> <version>1.0.0.RELEASE</version> </dependency>
先來看下一個簡單完整的示例程序:網絡
必須實現的接口多線程
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
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()方法出現異常或爬取網頁時異常,屢次處理無效的狀況下進入的方法,該方法的參數爲這次出錯的網址。通常是對其進行日誌記錄等操做。
經過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,默認電腦版。 |
上面的通常狀況下夠用了,若是對這些不滿意,嫌太少啥的,下面給了更優秀的解決方案。
在上面的演示程序中,咱們使用了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小節的非加粗屬性都會失效。
若是你想要讀取返回值來看下任務是否執行成功,你可使用看下上面的程示例序是如何作的。
public List<Future> getFutureList()
getFutureList()方法會返回任務執行以後的結果集合,集合中的成員都是Future類。調用Future對象的 get() 方法會等待當前任務執行完成再返回結果值,也就是會阻塞當前線程。該類還有不少方法,好比get(long timeout, TimeUnit unit),設置等待時間等等。
public ExecutorService getExecutor()
該方法會返回AiPa當前使用的Executor線程池,你獲取到該線程池後,須要一些使用線程池的一些方法能夠自行使用。
對於網頁爬取時的異常,這真的是個痛點。緣由真的不少,你的網絡不行,網站服務器的網絡不行,在網上有說把請求頭中Connection設置爲close,不用keep-alive。這個以我爬取幾百兆數據的經驗告訴你,然並卵。
因而我想出了一種無賴打法,反覆爬。爬一次不行就兩次,爬兩次不行就三次,只要網頁是能夠正常響應的,基本這個策略沒多少問題。固然,萬一真的是某個網頁就那麼獨樹一幟呢,因此咱們設置一個最大值,對於爬取超過最大值的,放棄記錄下來,看看啥子狀況。在個人這個框架中,也給出了fail()方法專門處理這個問題。
在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 (?)
因爲做者水平有限,框架必定存在一些漏洞或不足,但願各位專家、大佬提出批評指正!
個人博客:https://yueshutong.cnblogs.com/
Github:https://github.com/yueshutong/AIPa
Giree:https://gitee.com/zyzpp/AIPa
交流QQ羣:781927207