A lite distributed Java spider framework.
這是一個輕量級的分佈式java爬蟲框架
特點
這是一個強大,但又輕量級的分佈式爬蟲框架。jlitespider天生具有分佈式的特點,各個worker之間需要通過一個或者多個消息隊列來連接。消息隊列我的選擇是rabbitmq。worker和消息之間可以是一對一,一對多,多對一或多對多的關係,這些都可以自由而又簡單地配置。消息隊列中存儲的消息分爲四種:url,頁面源碼,解析後的結果以及自定義的消息。同樣的,worker的工作也分爲四部分:下載頁面,解析頁面,數據持久化和自定義的操作。
用戶只需要在配置文件中,規定好worker和消息隊列之間的關係。接着在代碼中,定義好worker的四部分工作。即可完成爬蟲的編寫。
總體的使用流程如下:
-
啓動rabbitmq。
-
在配置文件中定義worker和消息隊列之間的關係。
-
在代碼中編寫worker的工作。
-
最後,啓動爬蟲。
安裝
使用maven:
<dependency> <groupId>com.github.luohaha</groupId> <artifactId>jlitespider</artifactId> <version>0.4.3</version> </dependency>
直接下載jar包:
點擊下載。
設計思想
雖然JLiteSpider將抓取流程抽象成了幾個部分,但這並不意味着你就必須遵從這種抽象,你應該根據自己的應用場景,來作出最符合效率最大化的使用決策。比如,如果你抓取的網頁源碼較大,如果把網頁源碼也存入消息隊列,會導致消息隊列負擔過大。所以這個時候比較好的做法是將下載和解析的流程合併,直接向消息隊列輸出解析後的結果。
所以,雖然JLiteSpider幫你抽象出了抓取過程中的不同階段,但這完全是選擇性的,用戶完全是自由的。我在設計JLiteSpider的時候,盡力保障了自由。後面要介紹到的Worker和消息隊列的自由配置,以及添加了freeman
,同樣是這種設計思路的體現。
Worker和消息隊列之間關係
worker和消息隊列之間的關係可以是一對一,多對一,一對多,多對多,都是可以配置的。在配置文件中,寫上要監聽的消息隊列和要發送的消息隊列。例如:
{ "workerid" : 2, "mq" : [{ "name" : "one", "host" : "localhost", "port" : 5672, "qos" : 3 , "queue" : "url" }, { "name" : "two", "host" : "localhost", "port" : 5672, "qos" : 3 , "queue" : "hello" }], "sendto" : ["two"], "recvfrom" : ["one", "two"] }
workerid : worker的id號
mq : 各個消息隊列所在的位置,和配置信息。name
字段爲這個消息隊列的唯一標識符,供消息隊列的獲取使用。host
爲消息隊列所在的主機ip,port
爲消息隊列的監聽端口號(rabbitmq中默認爲5672)。qos
爲消息隊列每次將消息發給worker時的消息個數。queue
爲消息隊列的名字。host
+port
+queue
可以理解爲是消息隊列的唯一地址。
sendto : 要發送到的消息隊列,填入的信息爲mq
中的name
字段中的標識符。
recvfrom : 要監聽的消息隊列,消息隊列會把消息分發到這個worker中。填入的信息同樣爲mq
中的name
字段中的標識符。
消息的設計
在消息隊列中,消息一共有四種類型。分別是url,page,result和自定義類型。在worker的程序中,可以通過messagequeue的四種方法(sendUrl, sendPage, sendResult, send)來插入消息。worker的downloader會處理url消息,processor會處理page消息,saver會處理result消息,freeman會處理所有的自定義的消息。我們所要做的工作,就是實現好worker中的這四個函數。
Worker接口的設計
JLiteSpider將整個的爬蟲抓取流程抽象成四個部分,由四個接口來定義。分別是downloader,processor,saver和freeman。它們分別處理上述提到的四種消息。
你所需要做的是,實現這個接口,並將想要抓取的url鏈表返回。具體的實現細節,可以由你高度定製。
1. Downloader:
這部分實現的是頁面下載的任務,將想要抓取的url鏈表,轉化(下載後存儲)爲相應的頁面數據鏈表。
接口設計如下:
public interface Downloader { /** * 下載url所指定的頁面。 * @param url * 收到的由消息隊列傳過來的消息 * @param mQueue * 提供把消息發送到各個消息隊列的方法 * @throws IOException */ public void download(Object url, Map<String, MessageQueue> mQueue) throws IOException; }
你同樣可以實現這個接口,具體的實現可由你自由定製,只要實現download
函數。url
是消息隊列推送過來的消息,裏面不一定是一條url
,具體是什麼內容,是由你當初傳入消息隊列時決定的。mQueue
提供了消息發送到各個消息隊列的方法,通過mQueue.get("...")
選取消息隊列,然後執行messagequeue的四種方法(sendUrl, sendPage, sendResult, send)來插入消息。
2. Processor:
Processor
是解析器的接口,這裏會從網頁的原始文件中提取出有用的信息。
接口設計:
public interface Processor{ /** * 處理下載下來的頁面源代碼 * @param page * 消息隊列推送過來的頁面源代碼數據消息 * @param mQueue * 提供把消息發送到各個消息隊列的方法 * @throws IOException */ public void process(Object page, Map<String, MessageQueue> mQueue) throws IOException; }
實現這個接口,完成對頁面源碼的解析處理。page
是由消息隊列推送過來的消息,具體格式同樣是由你在傳入時決定好的。mQueue
使用同上。
3. Saver:
Saver
實現的是對解析得到結果的處理,可以將你解析後得到的數據存入數據庫,文件等等。或者將url重新存入消息隊列,實現迭代抓取。
接口的設計:
public interface Saver { /** * 處理最終解析得到的結果 * @param result * 消息隊列推送過來的結果消息 * @param mQueue * 提供把消息發送到各個消息隊列的方法 * @throws IOException */ public void save(Object result, Map<String, MessageQueue> mQueue) throws IOException; }
通過實現這個接口,可以完成對結果的處理。你同樣可以實現這個接口,具體的實現可由你自由定製,只要實現download
函數。result
是消息隊列推送過來的結果消息,具體的格式是由你當初傳入消息隊列時決定的。mQueue
的使用同上。
說到這裏,也給大家推薦一個架構交流學習羣:835544715,裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系。還能領取免費的學習資源,相信對於已經工作和遇到技術瓶頸的碼友,在這個羣裏會有你需要的內容。
4. Freeman:
通過上述的三個流程,可以實現爬蟲抓取的一個正常流程。但是
jlitespider
同樣提供了自定義的功能,你可以完善,加強,改進甚至顛覆上述的抓取流程。freeman
就是一個處理自定義消息格式的接口,實現它就可以定義自己的格式,以至於定義自己的流程。
接口的設計:
public interface Freeman { /** * 自定義的處理函數 * @param key * key爲自定義的消息標記 * @param msg * 消息隊列推送的消息 * @param mQueue * 提供把消息發送到各個消息隊列的方法 * @throws IOException */ public void doSomeThing(String key, Object msg, Map<String, MessageQueue> mQueue) throws IOException; }
通過實現doSomeThing
函數,你就可以處理來自消息隊列的自定義消息。key
爲消息的標記,msg
爲消息的內容。同樣,通過mQueue
的send
方法,可以實現向消息隊列發送自定義消息的操作。(需要注意,自定義的消息標記不能爲:url
,page
,result
。否則會被認爲是jlitespider
的保留消息,也就是由上述的三個接口函數來處理。)
總結說明
jlitespider
的設計可能會讓您有些疑惑,不過等您熟悉這一整套的設計之後,您就會發現jlitespider
是多麼的靈活和易於使用。
###使用方法
JLiteSpider使用:
//worker的啓動Spider.create() //創建實例 .setDownloader(...) //設置實現了Downloader接口的下載器 .setProcessor(...) //設置實現了Processor接口的解析器 .setSaver(...) //設置實現了Saver接口的數據持久化方法 .setFreeman(...) //設置自定義消息的處理函數 .setSettingFile(...) //設置配置文件 .begin(); //開始爬蟲//消息隊列中初始消息添加器的使用。只有向消息隊列中添加初始的消息後,整個爬蟲系統才能啓動,因此稱其爲spider的lighter(點火器)。SpiderLighter.locateMQ("localhost", 5672, "MQ's name") // 定位到要訪問的消息隊列 .addUrl(...) //向消息隊列添加url類型的消息 .addPage(...) //向消息隊列添加page類型的消息 .addResult(...) //向消息隊列添加result類型的消息 .add(..., ...) //向消息隊列添加自定義類型的消息 .close() //關閉連接,一定要記得在最後調用!
以豆瓣電影的頁面爲例子,假設我們要抓取豆瓣電影的愛情分類中的所有電影名稱,並存入txt文件中:
-
首先,需要設計消息隊列和worker之間的關係。我的設計是有兩個worker和兩個消息隊列,其中一個worker在main消息隊列上,負責下載,解析並把最終結果傳入data消息隊列。第二個worker從data消息隊列中取數據,並存入txt文件中。兩個worker的配置文件如下:
第一個worker:
{ "workerid" : 1, "mq" : [{ "name" : "main", "host" : "localhost", "port" : 5672, "qos" : 3 , "queue" : "main" }, { "name" : "data", "host" : "localhost", "port" : 5672, "qos" : 3 , "queue" : "data" }], "sendto" : ["main", "data"], "recvfrom" : ["main"] }
第二個worker:
{ "workerid" : 2, "mq" : [{ "name" : "main", "host" : "localhost", "port" : 5672, "qos" : 3 , "queue" : "main" }, { "name" : "data", "host" : "localhost", "port" : 5672, "qos" : 3 , "queue" : "data" }], "sendto" : [], "recvfrom" : ["data"] }
-
接着,編寫第一個worker的代碼,如下:
//下載頁面數據,並存入main隊列。public class DoubanDownloader implements Downloader { private Logger logger = Logger.getLogger("DoubanDownloader"); @Override public void download(Object url, Map<String, MessageQueue> mQueue) throws IOException { // TODO Auto-generated method stub String result = ""; try { result = Network.create() .setUserAgent("...") .setCookie("...") .downloader(url.toString()); //下載成功,將頁面數據放入main消息隊列 mQueue.get("main").sendPage(result); } catch (IOException e) { logger.info("本次下載失敗!重新下載!"); //因爲下載失敗,所以將url重新放入main隊列中 mQueue.get("main").sendUrl(url); } } }
//解析頁面數據,將結果放入main消息隊列。同時,後面頁面的url信息同樣需要放入隊列,以便迭代抓取。public class DoubanProcessor implements Processor {//url去重複 private Set<String> urlset = new HashSet<>(); @Override public void process(Object page, Map<String, MessageQueue> mQueue) throws IOException { // TODO Auto-generated method stub String path = "//[@id=content]/div/div[1]/div[2]/table/tbody/tr/td[1]/a/@title"; List<String> result = Xsoup.compile(path).evaluate(Jsoup.parse(page.toString())).list(); //將結果放入main消息隊列 mQueue.get("main").sendResult(result); path = "//[@id=content]/div/div[1]/div[3]/a/@href"; List<String> url = Xsoup.compile(path).evaluate(Jsoup.parse(page.toString())).list(); for (String each : url) { if (!urlset.contains(each)) { //如果url之前並未抓取過,則加入main隊列,作爲接下來要抓取的url mQueue.get("main").sendUrl(each); urlset.add(each); } } } }
//把最終的數據放入data消息隊列public class DoubanSaver implements Saver { @Override public void save(Object result, Map<String, MessageQueue> mQueue) throws IOException { // TODO Auto-generated method stub List<String> rList = (List<String>) result; for (String each : rList) { //把數據發往data消息隊列 mQueue.get("data").send("cc", each); } } }
//啓動worker的主程序public class DoubanSpider { public static void main(String[] args) { try { Spider.create().setDownloader(new DoubanDownloader()) .setProcessor(new DoubanProcessor()) .setSaver(new DoubanSaver()) .setSettingFile("./conf/setting.json") .begin(); } catch (ShutdownSignalException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ConsumerCancelledException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (TimeoutException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SpiderSettingFileException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
-
接下來,還要寫第二個worker的代碼。
//接收data消息隊列中的數據,寫入txtpublic class SaveToFile implements Freeman { @Override public void doSomeThing(String key, Object msg, Map<String, MessageQueue> mQueue) throws IOException { // TODO Auto-generated method stub File file = new File("./output/name.txt"); FileWriter fileWriter = new FileWriter(file, true); fileWriter.write(msg.toString() + "\n"); fileWriter.flush(); fileWriter.close(); } }
//第二個worker的啓動主程序public class SaveToFileSpider { public static void main(String[] args) { try { Spider.create().setFreeman(new SaveToFile()) .setSettingFile("./conf/setting2.json") .begin(); } catch (ShutdownSignalException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ConsumerCancelledException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (TimeoutException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SpiderSettingFileException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
-
還要編寫一個main消息隊列的初始化程序(點火程序),把第一個入口url放入main消息隊列中。
//把入口url放入main消息隊列public class AddUrls { public static void main(String[] args) { try { // 首先定位到要訪問的消息隊列,隊列在localhost:5672/main // 然後向這個消息隊列添加url // 最後關閉lighter SpiderLighter.locateMQ("localhost", 5672, "main") .addUrl("https://movie.douban.com/tag/%E7%88%B1%E6%83%85?start=0&type=T") .close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (TimeoutException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
-
最後,依次啓動程序。啓動的順序是:rabbitmq -> worker1/2 -> 初始化消息程序。關於rabbitmq的使用,它的官方網站上有詳細的安裝和使用文檔,可用於快速搭建rabbitmq的server。
輔助工具
當前版本的
jlitespider
能提供的輔助工具並不多,您在使用jlitespider
的過程中,可以將您實現的輔助工具合併到jlitespider
中來,一起來完善jlitespider
的功能。輔助工具在包com.github.luohaha.jlitespider.extension
中。
-
Network
簡單的網絡下載器,輸入url,返回頁面源代碼。使用如下:
String result = Network.create() .setCookie("...") .setProxy("...") .setTimeout(...) .setUserAgent("...") .downloader(url);
不推薦使用這個網絡下載器,因爲它是同步的,會阻塞進程。
-
AsyncNetwork
異步非阻塞的網絡下載器,推薦使用這個作爲頁面下載器,因爲它不會阻塞進程。
// 創建下載器AsyncNetwork asyncNetwork = new AsyncNetwork();// 設置cookieasyncNetwork.setCookie(cookies);// 設置代理asyncNetwork.setProxy("...");// 設置agentasyncNetwork.setUserAgent("...");// 啓動下載器asyncNetwork.begin();
在異步下載器啓動後,可以隨時往下載器中添加url,和對應的回調處理對象。
// 添加要下載的頁面的url,和下載完成後的處理函數。asyncNetwork.addUrl("...", new DownloadCallback() { @Override public void onReceived(String result, String url) { // 下載成功後,執行這個函數。result爲下載下來的頁面信息,url爲對應的url鏈接。 } @Override public void onFailed(Exception exception, String url) { // 下載失敗時,執行這個函數。exception爲失敗原因。 } });
-
解析工具
項目中依賴了兩個很常用的解析工具:xsoup 和 jsoup。