適合C# Actor的訊息執行方式(5):一個簡單的網路爬蟲
之前的幾篇文章大都在擺一些“小道理”,有經驗的朋友容易想象出來其中的含義,不過對於那些還不瞭解Actor模型的朋友來說,這些內容似乎有些太過了。此外,乒乓測試雖然經典,但是不太容易說明問題。因此,今天我們就來看一個簡單的有些簡陋的網路爬蟲,對於Actor模型的使用來說,它至少比乒乓測試能夠說明問題。對了,我們先來使用那“中看不中用”的訊息執行方式。
<div xmlns:cc="http://creativecommons.org/ns#" about="http://www.cnblogs.com/JeffreyZhao/archive/2009/07/02/embarrassed-mvp.html"><a rel="cc:attributionURL" property="cc:attributionName" href="http://www.cnblogs.com/JeffreyZhao/">趙劼a> / <a rel="license" href="http://creativecommons.org/licenses/by/2.5/cn/">CC BY 2.5a>div>
功能簡介
這個網路爬蟲的功能還是用於演示,先來列舉出它的實現目標吧:
- 給出一個初始連結,然後抓取它的HTML並分析出所有html連結,然後繼續爬,不斷爬,直到爬完所有連結為止。
- 多執行緒執行,我們可以指定由多少個爬蟲同時工作。
- 多個爬蟲組成一個“工作單元”,程式中可以同時出現多個工作單元,工作單元之間互相獨立。
- 能簡化的地方便簡化,如一切不涉及任何永久性儲存(也就是說,只使用記憶體),沒有太複雜的容錯機制。
的確很簡單吧?那麼,現在您不妨先在腦海中想象一下,在不用Actor模型的時候您會怎麼實現這個功能。然後,我們就要動手使用ActorLite這個小類庫了。
協議制定
正如我們不斷強調的那樣,在Actor模型中唯一的通訊方式便是互相傳送訊息。於是使用Actor模型的第一步往往便是設計Actor型別,以及它們之間傳遞的訊息。在這個簡單的場景中,我們會定義兩種Actor型別。一是Monitor,二是Crawler。一個Monitor便代表一個“工作單元”,它管理了多個爬蟲,即Crawler。
Monitor將負責在合適的時候建立Crawler,並向其傳送一個訊息,讓其開始工作。在我們的系統中,我們使用ICrawlRequestHandler介面來表示這個訊息:
public interface ICrawlRequestHandler { void Crawl(Monitor monitor, string url); }
在接受到上面的Crawl訊息後,Crawler將去抓取指定的url物件,並將結果發還給Monitor。在這裡我們要求報告Cralwer向Monitor報告“成功”和“失敗”兩種訊息1:
public interface ICrawlResponseHandler { void Succeeded(Crawler crawler, string url, List<string> links); void Failed(Crawler crawler, string url, Exception ex); }
我們使用“介面”這種方式定義了“訊息組”,把Succeeded和Failed兩種關係密切的訊息繫結在一起。如果抓取成功,則Crawler會從抓取內容中獲得額外的連結,併發還給Monitor——失敗的時候自然就發還一個異常物件。此外,無論是成功還是失敗,我們都會把Crawler物件交給Monitor,Monitor會安排給Crawler新的抓取任務。
因此,Monitor和Cralwer類的定義大約應該是這樣的:
public class Monitor : Actor<Action<ICrawlResponseHandler>>, ICrawlResponseHandler { protected override void Receive(Action<ICrawlResponseHandler> 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<Action<ICrawlRequestHandler>>, ICrawlRequestHandler { protected override void Receive(Action<ICrawlRequestHandler> message) { message(this); } #region ICrawlRequestHandler Members void ICrawlRequestHandler.Crawl(Monitor monitor, string url) { ... } #endregion }
Crawler實現
我們先從簡單的Crawler類的實現開始。Crawler類只需要實現ICrawlRequestHandler介面的Crawl方法即可:
void ICrawlRequestHandler.Crawl(Monitor monitor, string url) { try { string content = new WebClient().DownloadString(url); var matches = Regex.Matches(content, @"href=""(http://[^""]+)""").Cast<Match>(); 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)); } }
沒錯,使用WebClient下載頁面內容只需要一行程式碼就可以了。然後便是使用正規表示式提取出頁面上所有的連結。很顯然這裡是有問題的,因為我們我只分析出以“http://”開頭的地址,但是無視其他的“相對地址”——不過作為一個小實驗來說已經足夠說明問題了。最後自然是使用Post方法將結果發還給Monitor。在丟擲異常的情況下,這幾行程式碼的邏輯也非常自然。
Monitor實現
Monitor相對來說便略顯複雜了一些。我們知道,Monitor要負責控制Crawler的數量,那麼必然需要負責維護一些必要的欄位:
private HashSet<string> m_allUrls; // 所有待爬或爬過的url private Queue<string> m_readyToCrawl; // 待爬的url public int MaxCrawlerCount { private set; get; } // 最大爬蟲數目 public int WorkingCrawlerCount { private set; get; } // 正在工作的爬蟲數目 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); }
在抓取成功時,Monitor將遍歷links列表中的所有地址,如果發現新的url,則加入相關集合中。在抓取失敗的情況下,我們也只是簡單的繼續下去而已。而“繼續”則是由DispatchCrawlingTasks方法實現的,我們需要傳入一個“可複用”的Crawler物件:
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++; } }
如果已經沒有需要抓取的內容了,則直接拋棄Crawler物件即可,否則則分派一個新任務。接著便不斷建立新的爬蟲,分配新的抓取任務,直到爬蟲數額用滿,或者沒有需要抓取的內容位置。
使用
我們使用區區幾十行程式碼遍實現了一個簡單的多執行緒爬蟲,其中一個關鍵便是使用了Actor模型。使用Actor模型,物件之間通過訊息傳遞進行互動。而且對於單個Actor物件來說,訊息的執行完全是執行緒安全的。因此,我們只要作用最直接的邏輯便可以完成整個實現,從而回避了記憶體共享的並行模式中所使用的互斥體、鎖等各類元件。
不過有沒有發現,我們沒有一個入口可以“開啟”一個抓取任務啊,Monitor類中還缺少了點什麼。好吧,那麼我們補上一個Start方法:
public class Monitor : Actor<Action<ICrawlResponseHandler>>, ICrawlResponseHandler { ... public void Start(string url) { this.m_allUrls.Add(url); this.WorkingCrawlerCount++; new Crawler().Post(c => c.Crawl(this, url)); } }
於是,我們便可以這樣開啟一個或多個抓取任務:
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(); } }
這裡我們新建兩個工作單元,也就是啟動了兩個抓取任務。一是使用5個爬蟲抓取cnblogs.com,二是使用10個爬蟲抓取csdn.net。
缺陷
這裡的缺陷是什麼?其實很明顯,您發現了嗎?
使用Actor模型可以保證訊息執行的執行緒安全,不過很明顯Start方法並非如此,我們只能用它來“開啟”一個抓取任務。但是如果我們想再次“手動”提交一個需要抓取的URL怎麼辦?所以理想的方法,其實也應該是向Monitor傳送一個訊息來啟動第一個URL抓取任務。需要補充,則傳送多個URL即可。可是,這個訊息定義在什麼地方才合適呢?我們的Monitor類已經實現了Actor
這就是一個致命的限制:一個Actor雖然可以實現多個介面,但只能接受其中一個作為訊息。同樣的,如果我們要為Monitor提供其他功能,例如“查詢”某個URL的抓取狀態,也因為同樣的原因而無法實現。還有,便是在前幾篇文章中談到的問題了。Crawler和Monitor直接耦合,我們向Crawler傳送的訊息只能攜帶一個Monitor物件。
最後,便是一個略顯特別的問題了。我們這裡使用WebClient的DownloadString方法來獲取網頁的內容,但是這是個同步IO操作,理想的做法中我們應該使用非同步的方法。所以,我們可以這麼寫:
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<Match>(); 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)); }
如果您還記得老趙在最近一篇文章中關於IO執行緒池的討論,就可以瞭解到DownloadStringCompleted事件的處理方法會在統一的IO執行緒池中執行,這樣我們無法控制其運算能力。因此,我們應該在回撥函式中向Crawler自己傳送一條訊息表示抓取完畢……呃,但是我們現在做不到啊。
嗯,下次再說吧。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-610622/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 適合C# Actor的訊息執行方式(2):C# Actor的尷尬C#
- 一種適合C# Actor的訊息執行方式(中):C# Actor的尷尬C#
- 適合C# Actor的訊息執行方式(1):Erlang中的模式匹配C#模式
- 一種適合C# Actor的訊息執行方式(上):Erlang中的模式匹配C#模式
- 適合C# Actor的訊息執行方式(3):中看不中用的解決方案C#
- 爬蟲學習之一個簡單的網路爬蟲爬蟲
- 簡單網路爬蟲Ruby版爬蟲
- nodeJS做一個簡單的爬蟲NodeJS爬蟲
- 一個簡單的python爬蟲程式Python爬蟲
- 一個簡單的爬蟲 頭部構造爬蟲
- 最簡單的網路圖片的爬取 --Pyhon網路爬蟲與資訊獲取爬蟲
- 用PYTHON爬蟲簡單爬取網路小說Python爬蟲
- Python爬蟲學習(5): 簡單的爬取Python爬蟲
- 精通Scrapy網路爬蟲【一】第一個爬蟲專案爬蟲
- 教你如何編寫第一個簡單的爬蟲爬蟲
- java實現一個簡單的爬蟲小程式Java爬蟲
- 使用nodeJS寫一個簡單的小爬蟲NodeJS爬蟲
- Goutte 一個簡單易用的 PHP 爬蟲類庫GoPHP爬蟲
- 用Python寫一個簡單的微博爬蟲Python爬蟲
- python網路爬蟲_Python爬蟲:30個小時搞定Python網路爬蟲視訊教程Python爬蟲
- 《用Python寫網路爬蟲》--編寫第一個網路爬蟲Python爬蟲
- 網路爬蟲之關於爬蟲 http 代理的常見使用方式爬蟲HTTP
- 簡單的爬蟲程式爬蟲
- 網路爬蟲——爬蟲實戰(一)爬蟲
- C#網路爬蟲開發C#爬蟲
- 基於python3的簡單網路爬蟲示例Python爬蟲
- 用asio擼了一個簡單的Actor模型模型
- 如何自己寫一個網路爬蟲爬蟲
- python爬蟲:爬蟲的簡單介紹及requests模組的簡單使用Python爬蟲
- 適合男孩子的python爬蟲Python爬蟲
- 網路爬蟲的原理爬蟲
- 傻傻的網路爬蟲爬蟲
- Java網路爬蟲實操(5)Java爬蟲
- 使用Scrapy構建一個網路爬蟲爬蟲
- 用LoadRunner做一個網路爬蟲爬蟲
- 如何訓練一個簡單的音訊識別網路音訊
- 如何設計一個簡單的訊息中介軟體
- python多執行緒爬蟲與單執行緒爬蟲效率效率對比Python執行緒爬蟲