Java多線(xiàn)程及分布式爬蟲(chóng)架構(gòu)原理解析
這是 Java 爬蟲(chóng)系列博文的第五篇,在上一篇Java 爬蟲(chóng)服務(wù)器被屏蔽的解決方案中,我們簡(jiǎn)單的聊反爬蟲(chóng)策略和反反爬蟲(chóng)方法,主要針對(duì)的是 IP 被封及其對(duì)應(yīng)辦法。前面幾篇文章我們把爬蟲(chóng)相關(guān)的基本知識(shí)都講的差不多啦。這一篇我們來(lái)聊一聊爬蟲(chóng)架構(gòu)相關(guān)的內(nèi)容。
前面幾章內(nèi)容我們的爬蟲(chóng)程序都是單線(xiàn)程,在我們調(diào)試爬蟲(chóng)程序的時(shí)候,單線(xiàn)程爬蟲(chóng)沒(méi)什么問(wèn)題,但是當(dāng)我們?cè)诰€(xiàn)上環(huán)境使用單線(xiàn)程爬蟲(chóng)程序去采集網(wǎng)頁(yè)時(shí),單線(xiàn)程就暴露出了兩個(gè)致命的問(wèn)題:
- 采集效率特別慢,單線(xiàn)程之間都是串行的,下一個(gè)執(zhí)行動(dòng)作需要等上一個(gè)執(zhí)行完才能執(zhí)行
- 對(duì)服務(wù)器的CUP等利用率不高,想想我們的服務(wù)器都是 8核16G,32G 的只跑一個(gè)線(xiàn)程會(huì)不會(huì)太浪費(fèi)啦
線(xiàn)上環(huán)境不可能像我們本地測(cè)試一樣,不在乎采集效率,只要能正確提取結(jié)果就行。在這個(gè)時(shí)間就是金錢(qián)的年代,不可能給你時(shí)間去慢慢的采集,所以單線(xiàn)程爬蟲(chóng)程序是行不通的,我們需要將單線(xiàn)程改成多線(xiàn)程的模式,來(lái)提升采集效率和提高計(jì)算機(jī)利用率。
多線(xiàn)程的爬蟲(chóng)程序設(shè)計(jì)比單線(xiàn)程就要復(fù)雜很多,但是與其他業(yè)務(wù)在高并發(fā)下要保證數(shù)據(jù)安全又不同,多線(xiàn)程爬蟲(chóng)在數(shù)據(jù)安全上到要求不是那么的高,因?yàn)槊總€(gè)頁(yè)面都可以被看作是一個(gè)獨(dú)立體。要做好多線(xiàn)程爬蟲(chóng)就必須做好兩點(diǎn):第一點(diǎn)就是統(tǒng)一的待采集 URL 維護(hù),第二點(diǎn)就是 URL 的去重, 下面我們簡(jiǎn)單的來(lái)聊一聊這兩點(diǎn)。
維護(hù)待采集的 URL
多線(xiàn)程爬蟲(chóng)程序就不能像單線(xiàn)程那樣,每個(gè)線(xiàn)程獨(dú)自維護(hù)這自己的待采集 URL,如果這樣的話(huà),那么每個(gè)線(xiàn)程采集的網(wǎng)頁(yè)將是一樣的,你這就不是多線(xiàn)程采集啦,你這是將一個(gè)頁(yè)面采集的多次?;谶@個(gè)原因我們就需要將待采集的 URL 統(tǒng)一維護(hù),每個(gè)線(xiàn)程從統(tǒng)一 URL 維護(hù)處領(lǐng)取采集 URL ,完成采集任務(wù),如果在頁(yè)面上發(fā)現(xiàn)新的 URL 鏈接則添加到 統(tǒng)一 URL 維護(hù)的容器中。下面是幾種適合用作統(tǒng)一 URL 維護(hù)的容器:
- JDK 的安全隊(duì)列,例如 LinkedBlockingQueue
- 高性能的 NoSQL,比如 Redis、Mongodb
- MQ 消息中間件
URL 的去重
URL 的去重也是多線(xiàn)程采集的關(guān)鍵一步,因?yàn)槿绻蝗ブ氐脑?huà),那么我們將采集到大量重復(fù)的 URL,這樣并沒(méi)有提升我們的采集效率,比如一個(gè)分頁(yè)的新聞列表,我們?cè)诓杉谝豁?yè)的時(shí)候可以得到 2、3、4、5 頁(yè)的鏈接,在采集第二頁(yè)的時(shí)候又會(huì)得到 1、3、4、5 頁(yè)的鏈接,待采集的 URL 隊(duì)列中將存在大量的列表頁(yè)鏈接,這樣就會(huì)重復(fù)采集甚至進(jìn)入到一個(gè)死循環(huán)當(dāng)中,所以就需要 URL 去重。URL 去重的方法就非常多啦,下面是幾種常用的 URL 去重方式:
- 將 URL 保存到數(shù)據(jù)庫(kù)進(jìn)行去重,比如 redis、MongoDB
- 將 URL 放到哈希表中去重,例如 hashset
- 將 URL 經(jīng)過(guò) MD5 之后保存到哈希表中去重,相比于上面一種,能夠節(jié)約空間
- 使用 布隆過(guò)濾器(Bloom Filter)去重,這種方式能夠節(jié)約大量的空間,就是不那么準(zhǔn)確。
關(guān)于多線(xiàn)程爬蟲(chóng)的兩個(gè)核心知識(shí)點(diǎn)我們都知道啦,下面我畫(huà)了一個(gè)簡(jiǎn)單的多線(xiàn)程爬蟲(chóng)架構(gòu)圖,如下圖所示:
上面我們主要了解了多線(xiàn)程爬蟲(chóng)的架構(gòu)設(shè)計(jì),接下來(lái)我們不妨來(lái)試試 Java 多線(xiàn)程爬蟲(chóng),我們以采集虎撲新聞為例來(lái)實(shí)戰(zhàn)一下 Java 多線(xiàn)程爬蟲(chóng),Java 多線(xiàn)程爬蟲(chóng)中設(shè)計(jì)到了 待采集 URL 的維護(hù)和 URL 去重,由于我們這里只是演示,所以我們就使用 JDK 內(nèi)置的容器來(lái)完成,我們使用 LinkedBlockingQueue 作為待采集 URL 維護(hù)容器,HashSet 作為 URL 去重容器。下面是 Java 多線(xiàn)程爬蟲(chóng)核心代碼,詳細(xì)代碼以上傳 GitHub,地址在文末:
/** * 多線(xiàn)程爬蟲(chóng) */ public class ThreadCrawler implements Runnable { // 采集的文章數(shù) private final AtomicLong pageCount = new AtomicLong(0); // 列表頁(yè)鏈接正則表達(dá)式 public static final String URL_LIST = "https://voice.hupu.com/nba"; protected Logger logger = LoggerFactory.getLogger(getClass()); // 待采集的隊(duì)列 LinkedBlockingQueue<String> taskQueue; // 采集過(guò)的鏈接列表 HashSet<String> visited; // 線(xiàn)程池 CountableThreadPool threadPool; /** * * @param url 起始頁(yè) * @param threadNum 線(xiàn)程數(shù) * @throws InterruptedException */ public ThreadCrawler(String url, int threadNum) throws InterruptedException { this.taskQueue = new LinkedBlockingQueue<>(); this.threadPool = new CountableThreadPool(threadNum); this.visited = new HashSet<>(); // 將起始頁(yè)添加到待采集隊(duì)列中 this.taskQueue.put(url); } @Override public void run() { logger.info("Spider started!"); while (!Thread.currentThread().isInterrupted()) { // 從隊(duì)列中獲取待采集 URL final String request = taskQueue.poll(); // 如果獲取 request 為空,并且當(dāng)前的線(xiàn)程采已經(jīng)沒(méi)有線(xiàn)程在運(yùn)行 if (request == null) { if (threadPool.getThreadAlive() == 0) { break; } } else { // 執(zhí)行采集任務(wù) threadPool.execute(new Runnable() { @Override public void run() { try { processRequest(request); } catch (Exception e) { logger.error("process request " + request + " error", e); } finally { // 采集頁(yè)面 +1 pageCount.incrementAndGet(); } } }); } } threadPool.shutdown(); logger.info("Spider closed! {} pages downloaded.", pageCount.get()); } /** * 處理采集請(qǐng)求 * @param url */ protected void processRequest(String url) { // 判斷是否為列表頁(yè) if (url.matches(URL_LIST)) { // 列表頁(yè)解析出詳情頁(yè)鏈接添加到待采集URL隊(duì)列中 processTaskQueue(url); } else { // 解析網(wǎng)頁(yè) processPage(url); } } /** * 處理鏈接采集 * 處理列表頁(yè),將 url 添加到隊(duì)列中 * * @param url */ protected void processTaskQueue(String url) { try { Document doc = Jsoup.connect(url).get(); // 詳情頁(yè)鏈接 Elements elements = doc.select(" div.news-list > ul > li > div.list-hd > h4 > a"); elements.stream().forEach((element -> { String request = element.attr("href"); // 判斷該鏈接是否存在隊(duì)列或者已采集的 set 中,不存在則添加到隊(duì)列中 if (!visited.contains(request) && !taskQueue.contains(request)) { try { taskQueue.put(request); } catch (InterruptedException e) { e.printStackTrace(); } } })); // 列表頁(yè)鏈接 Elements list_urls = doc.select("div.voice-paging > a"); list_urls.stream().forEach((element -> { String request = element.absUrl("href"); // 判斷是否符合要提取的列表鏈接要求 if (request.matches(URL_LIST)) { // 判斷該鏈接是否存在隊(duì)列或者已采集的 set 中,不存在則添加到隊(duì)列中 if (!visited.contains(request) && !taskQueue.contains(request)) { try { taskQueue.put(request); } catch (InterruptedException e) { e.printStackTrace(); } } } })); } catch (Exception e) { e.printStackTrace(); } } /** * 解析頁(yè)面 * * @param url */ protected void processPage(String url) { try { Document doc = Jsoup.connect(url).get(); String title = doc.select("body > div.hp-wrap > div.voice-main > div.artical-title > h1").first().ownText(); System.out.println(Thread.currentThread().getName() + " 在 " + new Date() + " 采集了虎撲新聞 " + title); // 將采集完的 url 存入到已經(jīng)采集的 set 中 visited.add(url); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { try { new ThreadCrawler("https://voice.hupu.com/nba", 5).run(); } catch (InterruptedException e) { e.printStackTrace(); } } }
我們用 5 個(gè)線(xiàn)程去采集虎撲新聞列表頁(yè)看看效果如果?運(yùn)行該程序,得到如下結(jié)果:
多線(xiàn)程采集結(jié)果
結(jié)果中可以看出,我們啟動(dòng)了 5 個(gè)線(xiàn)程采集了 61 頁(yè)頁(yè)面,一共耗時(shí) 2 秒鐘,可以說(shuō)效果還是不錯(cuò)的,我們來(lái)跟單線(xiàn)程對(duì)比一下,看看差距有多大?我們將線(xiàn)程數(shù)設(shè)置為 1 ,再次啟動(dòng)程序,得到如下結(jié)果:
單線(xiàn)程運(yùn)行結(jié)果
可以看出單線(xiàn)程采集虎撲 61 條新聞花費(fèi)了 7 秒鐘,耗時(shí)差不多是多線(xiàn)程的 4 倍,你想想這可只是 61 個(gè)頁(yè)面,頁(yè)面更多的話(huà),差距會(huì)越來(lái)越大,所以多線(xiàn)程爬蟲(chóng)效率還是非常高的。
分布式爬蟲(chóng)架構(gòu)
分布式爬蟲(chóng)架構(gòu)是一個(gè)大型采集程序才需要使用的架構(gòu),一般情況下使用單機(jī)多線(xiàn)程就可以解決業(yè)務(wù)需求,反正我是沒(méi)有分布式爬蟲(chóng)項(xiàng)目的經(jīng)驗(yàn),所以這一塊我也沒(méi)什么可以講的,但是我們作為技術(shù)人員,我們需要對(duì)技術(shù)保存熱度,雖然不用,但是了解了解也無(wú)妨,我查閱了不少資料得出了如下結(jié)論:
分布式爬蟲(chóng)架構(gòu)跟我們多線(xiàn)程爬蟲(chóng)架構(gòu)在思路上來(lái)說(shuō)是一樣的,我們只需要在多線(xiàn)程的基礎(chǔ)上稍加改進(jìn)就可以變成一個(gè)簡(jiǎn)單的分布式爬蟲(chóng)架構(gòu)。因?yàn)榉植际脚老x(chóng)架構(gòu)中爬蟲(chóng)程序部署在不同的機(jī)器上,所以我們待采集的 URL 和 采集過(guò)的 URL 就不能存放在爬蟲(chóng)程序機(jī)器的內(nèi)存中啦,我們需要將它統(tǒng)一在某臺(tái)機(jī)器上維護(hù)啦,比如存放在 Redis 或者 MongoDB 中,每臺(tái)機(jī)器都從這上面獲取采集鏈接,而不是從 LinkedBlockingQueue 這樣的內(nèi)存隊(duì)列中取鏈接啦,這樣一個(gè)簡(jiǎn)單的分布式爬蟲(chóng)架構(gòu)就出現(xiàn)了,當(dāng)然這里面還會(huì)有很多細(xì)節(jié)問(wèn)題,因?yàn)槲覜](méi)有分布式架構(gòu)的經(jīng)驗(yàn),我也無(wú)從說(shuō)起,如果你有興趣的話(huà),歡迎交流。
源代碼:源代碼
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Java中JDBC事務(wù)與JTA分布式事務(wù)總結(jié)與區(qū)別
- 詳解Java分布式系統(tǒng)中session一致性問(wèn)題
- 詳解Java分布式系統(tǒng)中一致性哈希算法
- Java中實(shí)現(xiàn)分布式定時(shí)任務(wù)的方法
- Java實(shí)現(xiàn)Twitter的分布式自增ID算法snowflake
- 淺談Java分布式架構(gòu)下如何實(shí)現(xiàn)分布式鎖
- Java redisson實(shí)現(xiàn)分布式鎖原理詳解
- Java注解如何基于Redission實(shí)現(xiàn)分布式鎖
- Java Redis分布式鎖的正確實(shí)現(xiàn)方式詳解
- Javas使用Redlock實(shí)現(xiàn)分布式鎖過(guò)程解析
- Java高級(jí)架構(gòu)之FastDFS分布式文件集群詳解
- 詳解java解決分布式環(huán)境中高并發(fā)環(huán)境下數(shù)據(jù)插入重復(fù)問(wèn)題
- 詳解Java TCC分布式事務(wù)實(shí)現(xiàn)原理
相關(guān)文章
Java中File文件操作類(lèi)的超詳細(xì)使用教程
File類(lèi)在包java.io.File下、代表操作系統(tǒng)的文件對(duì)象(文件、文件夾),File類(lèi)提供了諸如:定位文件,獲取文件本身的信息、刪除文件、創(chuàng)建文件(文件夾)等功能,下面這篇文章主要給大家介紹了關(guān)于Java中File文件操作類(lèi)的超詳細(xì)使用教程,需要的朋友可以參考下2023-01-01詳解Spring與Mybatis整合方法(基于IDEA中的Maven整合)
這篇文章主要介紹了Spring與Mybatis整合方法(基于IDEA中的Maven整合),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10SpringBoot配置文件bootstrap和application區(qū)別及說(shuō)明
這篇文章主要介紹了SpringBoot配置文件bootstrap和application區(qū)別及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-0630w+數(shù)據(jù)使用RedisTemplate?pipeline空指針NullPointerException異常分析
這篇文章主要為大家介紹了30w+數(shù)據(jù)使用RedisTemplate?pipeline空指針NullPointerException異常分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08