C#Actorに適したメッセージ実行方法(5):簡単なネットワーク爬虫類

15851 ワード

これまでのいくつかの文章は「小さな道理」を並べていたが、経験のある友达はその意味を想像しやすいが、Actorモデルをまだ知らない友达にとっては、これらの内容はあまりにも多すぎるようだ.また、卓球のテストは古典的だが、問題を説明するのは容易ではない.そこで、今日は、Actorモデルの使用にとって、少なくとも卓球テストよりも問題を説明できる簡単なネットワーク爬虫類を見てみましょう.ところで、まず、その「中では役に立たない」メッセージの実行方法を使用します.

機能の概要


このネットワーク爬虫類の機能はやはりプレゼンテーションに使われています.まず、その実現目標を挙げましょう.
  • は、初期リンクを与え、HTMLをキャプチャしてすべてのhtmlリンクを分析し、すべてのリンクが完了するまで登り続けます.
  • マルチスレッドで実行され、複数の爬虫類が同時に動作するかを指定できます.
  • 複数の爬虫類が1つの「作業ユニット」を構成し、プログラムには複数の作業ユニットが同時に現れ、作業ユニット間は互いに独立している.
  • は、永続的なストレージ(すなわち、メモリのみを使用する)に関係なく、あまり複雑なフォールトトレランスメカニズムがないなど、簡単に簡略化できます.

  • 確かに簡単ですよね?では、まず、Actorモデルを使わないときにどのようにこの機能を実現するかを頭の中で想像してみてください.そして、ActorLiteという小さなクラスライブラリの使用に着手します.

    協議の作成


    我々が絶えず強調しているように,Actorモデルにおける唯一の通信方式は互いにメッセージを送信することである.したがって、Actorモデルを使用する最初のステップは、Actorタイプと、それらの間で伝達されるメッセージを設計することであることが多い.この簡単なシーンでは、2つのActorタイプを定義します.一つはMonitor、二つはCrawlerです.1つのMonitorは、複数の爬虫類、すなわちCrawlerを管理する「作業ユニット」を表します.
    Monitorは、適切なタイミングでCrawlerを作成し、作業を開始するメッセージを送信します.私たちのシステムでは、ICrawlRequestHandlerインタフェースを使用してこのメッセージを表します.
    public interface ICrawlRequestHandler
    {
        void Crawl(Monitor monitor, string url);
    }
    

    上記のCrawlメッセージを受信すると、Crawlerは指定したurlオブジェクトをキャプチャし、結果をMonitorに返します.ここでは、CralwerがMonitorに「成功」と「失敗」の2つのメッセージ1を報告するように要求します.
    public interface ICrawlResponseHandler
    {
        void Succeeded(Crawler crawler, string url, List<string> links);
        void Failed(Crawler crawler, string url, Exception ex);
    }
    

    「インタフェース」という方法で「メッセージグループ」を定義し,SucceededとFailedの2つの密接な関係にあるメッセージをバインドした.キャプチャに成功すると、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を使ってページの内容をダウンロードするには、1行のコードだけでいいです.次に、正規表現を使用してページ上のすべてのリンクを抽出します.明らかにここには問題がある.私たちは「http://」で始まるアドレスしか分析していないが、他の「相対アドレス」を無視しているからだ.しかし、小さな実験としては問題を説明するのに十分だ.最後に、Postメソッドを使用して結果を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オブジェクトを直接破棄します.そうしないと、新しいタスクが割り当てられます.次に、新しい爬虫類を作成し続け、爬虫類の数がいっぱいになるまで、または捕まえる必要のないコンテンツの位置まで、新しい捕獲タスクを割り当てます.

    使用


    ゾーン数十行のコードを用いて単純なマルチスレッド爬虫類を実装し,その1つの鍵はActorモデルを用いることである.Actorモデルを使用すると、オブジェクト間でメッセージングが行われます.また、メッセージの実行は、単一のActorオブジェクトにとって完全にスレッドセキュリティです.したがって,最も直接的な論理を作用させるだけで実現全体を達成することができ,メモリ共有のパラレルモードで使用される反発体,ロックなどの各種コンポーネントを回避することができる.
    しかし、私たちが捕まえるタスクを「開く」ことができる入り口はありません.Monitorクラスには何か欠けています.ではスターメソッドを追加します
    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));
        }
    }
    

    これにより、1つ以上のキャプチャタスクを開くことができます.
    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();
        }
    }
    

    ここでは、2つのワークユニットを新規作成します.つまり、2つのキャプチャタスクを開始します.一つは5つの爬虫類を用いてcnblogsを捕まえることである.com,二つ目は10個の爬虫類を用いてcsdnを捕まえる.net.

    けっかん


    ここの欠陥は何ですか.実は明らかですが、気づきましたか?
    Actorモデルを使用すると、メッセージ実行のスレッドが安全になりますが、Startメソッドはそうではないことは明らかです.キャプチャタスクを「オン」にするしかありません.しかし、私たちが再び「手動」でキャプチャする必要があるURLを提出したい場合はどうすればいいですか?理想的な方法は、Monitorに最初のURLキャプチャタスクを開始するメッセージを送信することです.補足が必要な場合は、複数のURLを送信すればよい.しかし、このメッセージの定義はどこが適切なのでしょうか.私たちのMonitorクラスはすでにActor>を実現しており、もう別のインタフェースをメッセージとして受け入れることはできませんよね?
    これは致命的な制限です.1つのActorは複数のインタフェースを実装できますが、メッセージとして1つしか受け入れられません.同様に、Monitorに「クエリー」のURLのキャプチャステータスなどの他の機能を提供する場合も、同じ理由で実現できません.また、前の文章で述べた問題です.CrawlerとMonitorは直接結合されており,我々がCrawlerに送信したメッセージは1つのMonitorオブジェクトしか持ち込めない.
    最後に、少し特別な問題です.ここではWebClientのDownloadStringメソッドを使用してWebページのコンテンツを取得しますが、これは同期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自身にキャプチャが完了したことを示すメッセージを送るべきだ......ええ、しかし、私たちは今できません.
    うん、また今度ね.

    関連記事

  • C#Actorに適したメッセージ実行方式(1):Erlangにおけるモードマッチング
  • C#Actorに適したメッセージ実行方式(2):C#Actorの気まずさ
  • C#Actorに適したメッセージ実行方式(3):中には見られないソリューション
  • C#Actorに適したメッセージ実行方式(4):段階的総括
  • C#Actorに適したメッセージ実行方式(5):簡単なネットワーク爬虫類
  • C#Actorに適したメッセージ実行方式(6):コヒーレントとインバータ
  •  
    本明細書の完全なコード:http://gist.github.com/154815