新聞中心
之前的幾篇文章大都在擺一些“小道理”,有經(jīng)驗(yàn)的朋友容易想象出來(lái)其中的含義,不過對(duì)于那些還不了解Actor模型的朋友來(lái)說(shuō),這些內(nèi)容似乎有些太過了。此外,乒乓測(cè)試雖然經(jīng)典,但是不太容易說(shuō)明問題。因此,今天我們就來(lái)看一個(gè)簡(jiǎn)單的有些簡(jiǎn)陋的網(wǎng)絡(luò)爬蟲,對(duì)于Actor模型的使用來(lái)說(shuō),它至少比乒乓測(cè)試能夠說(shuō)明問題。對(duì)了,我們先來(lái)使用那“中看不中用”的消息執(zhí)行方式。

專注于為中小企業(yè)提供網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)臨淄免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了成百上千企業(yè)的穩(wěn)健成長(zhǎng),幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
功能簡(jiǎn)介
這個(gè)網(wǎng)絡(luò)爬蟲的功能還是用于演示,先來(lái)列舉出它的實(shí)現(xiàn)目標(biāo)吧:
◆給出一個(gè)初始鏈接,然后抓取它的HTML并分析出所有html鏈接,然后繼續(xù)爬,不斷爬,直到爬完所有鏈接為止。
◆多線程運(yùn)行,我們可以指定由多少個(gè)爬蟲同時(shí)工作。
◆多個(gè)爬蟲組成一個(gè)“工作單元”,程序中可以同時(shí)出現(xiàn)多個(gè)工作單元,工作單元之間互相獨(dú)立。
◆能簡(jiǎn)化的地方便簡(jiǎn)化,如一切不涉及任何永久性存儲(chǔ)(也就是說(shuō),只使用內(nèi)存),沒有太復(fù)雜的容錯(cuò)機(jī)制。
的確很簡(jiǎn)單吧?那么,現(xiàn)在您不妨先在腦海中想象一下,在不用Actor模型的時(shí)候您會(huì)怎么實(shí)現(xiàn)這個(gè)功能。然后,我們就要?jiǎng)邮质褂肁ctorLite這個(gè)小類庫(kù)了。
協(xié)議制定
正如我們不斷強(qiáng)調(diào)的那樣,在Actor模型中唯一的通信方式便是互相發(fā)送消息。于是使用Actor模型的第一步往往便是設(shè)計(jì)Actor類型,以及它們之間傳遞的消息。在這個(gè)簡(jiǎn)單的場(chǎng)景中,我們會(huì)定義兩種Actor類型。一是Monitor,二是Crawler。一個(gè)Monitor便代表一個(gè)“工作單元”,它管理了多個(gè)爬蟲,即Crawler。
Monitor將負(fù)責(zé)在合適的時(shí)候創(chuàng)建Crawler,并向其發(fā)送一個(gè)消息,讓其開始工作。在我們的系統(tǒng)中,我們使用ICrawlRequestHandler接口來(lái)表示這個(gè)消息:
C# Actor模型·創(chuàng)建網(wǎng)絡(luò)爬蟲:
- public interface ICrawlRequestHandler
- {
- void Crawl(Monitor monitor, string url);
- }
在接受到上面的Crawl消息后,Crawler將去抓取指定的url對(duì)象,并將結(jié)果發(fā)還給Monitor。在這里我們要求報(bào)告Cralwer向Monitor報(bào)告“成功”和“失敗”兩種消息1:
C# Actor模型·爬蟲報(bào)告消息
- public interface ICrawlResponseHandler
- {
- void Succeeded(Crawler crawler, string url, List<string> links);
- void Failed(Crawler crawler, string url, Exception ex);
- }
我們使用“接口”這種方式定義了“消息組”,把Succeeded和Failed兩種關(guān)系密切的消息綁定在一起。如果抓取成功,則Crawler會(huì)從抓取內(nèi)容中獲得額外的鏈接,并發(fā)還給Monitor——失敗的時(shí)候自然就發(fā)還一個(gè)異常對(duì)象。此外,無(wú)論是成功還是失敗,我們都會(huì)把Crawler對(duì)象交給Monitor,Monitor會(huì)安排給Crawler新的抓取任務(wù)。
因此,Monitor和Cralwer類的定義大約應(yīng)該是這樣的:
C# Actor模型·爬蟲和監(jiān)控類的定義
- public class Monitor : Actor
>, ICrawlResponseHandler - {
- protected override void Receive(Action
message) - {
- message(this);
- }
- #region ICrawlResponseHandler Members
- void ICrawlResponseHandler.Succeeded(Crawler crawler, string url, List<string> links)
- {
- ...
- }
- void ICrawlResponseHandler.Failed(Crawler crawler, string url, Exception ex)
- {
- ...
- }
- #endregion
- }
- public class Crawler : Actor
>, ICrawlRequestHandler - {
- protected override void Receive(Action
message) - {
- message(this);
- }
- #region ICrawlRequestHandler Members
- void ICrawlRequestHandler.Crawl(Monitor monitor, string url)
- {
- ...
- }
- #endregion
- }
Crawler實(shí)現(xiàn)
我們先從簡(jiǎn)單的Crawler類的實(shí)現(xiàn)開始。Crawler類只需要實(shí)現(xiàn)ICrawlRequestHandler接口的Crawl方法即可:
C# Actor模型·爬蟲的實(shí)現(xiàn)
- void ICrawlRequestHandler.Crawl(Monitor monitor, string url)
- {
- try
- {
- string content = new WebClient().DownloadString(url);
- var matches = Regex.Matches(content, @"href=""(http://[^""]+)""").Cast
(); - var links = matches.Select(m => m.Groups[1].Value).Distinct().ToList();
- monitor.Post(m => m.Succeeded(this, url, links));
- }
- catch (Exception ex)
- {
- monitor.Post(m => m.Failed(this, url, ex));
- }
- }
沒錯(cuò),使用WebClient下載頁(yè)面內(nèi)容只需要一行代碼就可以了。然后便是使用正則表達(dá)式提取出頁(yè)面上所有的鏈接。很顯然這里是有問題的,因?yàn)槲覀兾抑环治龀鲆浴癶ttp://”開頭的地址,但是無(wú)視其他的“相對(duì)地址”——不過作為一個(gè)小實(shí)驗(yàn)來(lái)說(shuō)已經(jīng)足夠說(shuō)明問題了。最后自然是使用Post方法將結(jié)果發(fā)還給Monitor。在拋出異常的情況下,這幾行代碼的邏輯也非常自然。
Monitor實(shí)現(xiàn)
Monitor相對(duì)來(lái)說(shuō)便略顯復(fù)雜了一些。我們知道,Monitor要負(fù)責(zé)控制Crawler的數(shù)量,那么必然需要負(fù)責(zé)維護(hù)一些必要的字段:
C# Actor模型·監(jiān)控的實(shí)現(xiàn)
- private HashSet<string> m_allUrls; // 所有待爬或爬過的url
- private Queue<string> m_readyToCrawl; // 待爬的url
- public int MaxCrawlerCount { private set; get; } // 最大爬蟲數(shù)目
- public int WorkingCrawlerCount { private set; get; } // 正在工作的爬蟲數(shù)目
- public Monitor(int maxCrawlerCount)
- {
- this.m_allUrls = new HashSet<string>();
- this.m_readyToCrawl = new Queue<string>();
- this.MaxCrawlerCount = maxCrawlerCount;
- this.WorkingCrawlerCount = 0;
- }
Monitor要處理的自然是ICrawlResponseHandler中的Succeeded或Failed方法:
- void ICrawlResponseHandler.Succeeded(Crawler crawler, string url, List<string> links)
- {
- Console.WriteLine("{0} crawled, {1} link(s).", url, links.Count);
- foreach (var newUrl in links)
- {
- if (!this.m_allUrls.Contains(newUrl))
- {
- this.m_allUrls.Add(newUrl);
- this.m_readyToCrawl.Enqueue(newUrl);
- }
- }
- this.DispatchCrawlingTasks(crawler);
- }
- void ICrawlResponseHandler.Failed(Crawler crawler, string url, Exception ex)
- {
- Console.WriteLine("{0} error occurred: {1}.", url, ex.Message);
- this.DispatchCrawlingTasks(crawler);
- }
在抓取成功時(shí),Monitor將遍歷links列表中的所有地址,如果發(fā)現(xiàn)新的url,則加入相關(guān)集合中。在抓取失敗的情況下,我們也只是簡(jiǎn)單的繼續(xù)下去而已。而“繼續(xù)”則是由DispatchCrawlingTasks方法實(shí)現(xiàn)的,我們需要傳入一個(gè)“可復(fù)用”的Crawler對(duì)象:
C# Actor模型·爬蟲的傳入
- private void DispatchCrawlingTasks(Crawler reusableCrawler)
- {
- if (this.m_readyToCrawl.Count <= 0)
- {
- this.WorkingCrawlerCount--;
- return;
- }
- var url = this.m_readyToCrawl.Dequeue();
- reusableCrawler.Post(c => c.Crawl(this, url));
- while (this.m_readyToCrawl.Count > 0 &&
- this.WorkingCrawlerCount < this.MaxCrawlerCount)
- {
- var newUrl = this.m_readyToCrawl.Dequeue();
- new Crawler().Post(c => c.Crawl(this, newUrl));
- this.WorkingCrawlerCount++;
- }
- }
如果已經(jīng)沒有需要抓取的內(nèi)容了,則直接拋棄Crawler對(duì)象即可,否則則分派一個(gè)新任務(wù)。接著便不斷創(chuàng)建新的爬蟲,分配新的抓取任務(wù),直到爬蟲數(shù)額用滿,或者沒有需要抓取的內(nèi)容位置。
使用
我們使用區(qū)區(qū)幾十行代碼遍實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的多線程爬蟲,其中一個(gè)關(guān)鍵便是使用了Actor模型。使用Actor模型,對(duì)象之間通過消息傳遞進(jìn)行交互。而且對(duì)于單個(gè)Actor對(duì)象來(lái)說(shuō),消息的執(zhí)行完全是線程安全的。因此,我們只要作用最直接的邏輯便可以完成整個(gè)實(shí)現(xiàn),從而回避了內(nèi)存共享的并行模式中所使用的互斥體、鎖等各類組件。
不過有沒有發(fā)現(xiàn),我們沒有一個(gè)入口可以“開啟”一個(gè)抓取任務(wù)啊,Monitor類中還缺少了點(diǎn)什么。好吧,那么我們補(bǔ)上一個(gè)Start方法:
C# Actor模型·爬蟲開始工作
- public class Monitor : Actor
>, ICrawlResponseHandler - {
- ...
- public void Start(string url)
- {
- this.m_allUrls.Add(url);
- this.WorkingCrawlerCount++;
- new Crawler().Post(c => c.Crawl(this, url));
- }
- }
于是,我們便可以這樣打開一個(gè)或多個(gè)抓取任務(wù):
- static class Program
- {
- static void Main(string[] args)
- {
- new Monitor(5).Start("http://www.cnblogs.com/");
- new Monitor(10).Start("http://www.csdn.net/");
- Console.ReadLine();
- }
- }
這里我們新建兩個(gè)工作單元,也就是啟動(dòng)了兩個(gè)抓取任務(wù)。一是使用5個(gè)爬蟲抓取cnblogs.com,二是使用10個(gè)爬蟲抓取csdn.net。
缺陷
這里的缺陷是什么?其實(shí)很明顯,您發(fā)現(xiàn)了嗎?
使用Actor模型可以保證消息執(zhí)行的線程安全,不過很明顯Start方法并非如此,我們只能用它來(lái)“開啟”一個(gè)抓取任務(wù)。但是如果我們想再次“手動(dòng)”提交一個(gè)需要抓取的URL怎么辦?所以理想的方法,其實(shí)也應(yīng)該是向Monitor發(fā)送一個(gè)消息來(lái)啟動(dòng)第一個(gè)URL抓取任務(wù)。需要補(bǔ)充,則發(fā)送多個(gè)URL即可??墒牵@個(gè)消息定義在什么地方才合適呢?我們的Monitor類已經(jīng)實(shí)現(xiàn)了Actor
這就是一個(gè)致命的限制:一個(gè)Actor雖然可以實(shí)現(xiàn)多個(gè)接口,但只能接受其中一個(gè)作為消息。同樣的,如果我們要為Monitor提供其他功能,例如“查詢”某個(gè)URL的抓取狀態(tài),也因?yàn)橥瑯拥脑蚨鵁o(wú)法實(shí)現(xiàn)。還有,便是在前幾篇文章中談到的問題了。Crawler和Monitor直接耦合,我們向Crawler發(fā)送的消息只能攜帶一個(gè)Monitor對(duì)象。
最后,便是一個(gè)略顯特別的問題了。我們這里使用WebClient的DownloadString方法來(lái)獲取網(wǎng)頁(yè)的內(nèi)容,但是這是個(gè)同步IO操作,理想的做法中我們應(yīng)該使用異步的方法。所以,我們可以這么寫:
- void ICrawlRequestHandler.Crawl(Monitor monitor, string url)
- {
- WebClient webClient = new WebClient();
- webClient.DownloadStringCompleted += (sender, e) =>
- {
- if (e.Error == null)
- {
- var matches = Regex.Matches(e.Result, @"href=""(http://[^""]+)""").Cast
(); - var links = matches.Select(m => m.Groups[1].Value).Distinct().ToList();
- monitor.Post(m => m.Succeeded(this, url, links));
- }
- else
- {
- monitor.Post(m => m.Failed(this, url, e.Error));
- }
- };
- webClient.DownloadStringAsync(new Uri(url));
- }
如果您還記得老趙在最近一篇文章中關(guān)于IO線程池的討論,就可以了解到DownloadStringCompleted事件的處理方法會(huì)在統(tǒng)一的IO線程池中運(yùn)行,這樣我們無(wú)法控制其運(yùn)算能力。因此,我們應(yīng)該在回調(diào)函數(shù)中向Crawler自己發(fā)送一條消息表示抓取完畢……呃,但是我們現(xiàn)在做不到埃
嗯,下次再說(shuō)吧。
【編輯推薦】
- 強(qiáng)類型和Actor:ActorLite的演示
- C#的Tag Message回顧:繁瑣而危險(xiǎn)
- Erlang的Actor回顧:將消息轉(zhuǎn)化為邏輯執(zhí)行
- Actor模型的本質(zhì):究竟是要解決什么問題
- 順暢的使用C# Actor:另一個(gè)解決方案
新聞名稱:C#Actor模型開發(fā)實(shí)例:網(wǎng)絡(luò)爬蟲
本文URL:http://fisionsoft.com.cn/article/dphspps.html


咨詢
建站咨詢
