3

Async CTP を使用して HTML クローラーを作成しようとして、これを達成するための再帰のないメソッドを作成する方法に行き詰まりました。

これは私がこれまでに持っているコードです。

private readonly ConcurrentStack<LinkItem> _LinkStack;
private readonly Int32 _MaxStackSize;
private readonly WebClient client = new WebClient();

Func<string, string, Task<List<LinkItem>>> DownloadFromLink = async (BaseURL, uri) => 
{
    string html = await client.DownloadStringTaskAsync(uri);
    return LinkFinder.Find(html, BaseURL);
};

Action<LinkItem> DownloadAndPush = async (o) => 
{
    List<LinkItem> result = await DownloadFromLink(o.BaseURL, o.Href);
    if (this._LinkStack.Count() + result.Count <= this._MaxStackSize)
    {
        this._LinkStack.PushRange(result.ToArray());
        o.Processed = true;
    }  
};

Parallel.ForEach(this._LinkStack, (o) => 
{
    DownloadAndPush(o);
});

しかし、最初の (そして唯一の反復) を実行する時点でParallel.ForEachアイテムが 1 つしかないため、明らかにこれは期待どおりには機能しません。再帰的に考えることができる最も簡単なアプローチですがForEach、スタックスペースがすぐに不足するため、これを行うことはできません(私は考えていません)。

このコードを再構築する方法を教えてください。にMaxStackSize到達するか、システムがメモリ不足になるまでアイテムを追加する再帰的な継続として説明するものを作成しますか?

4

1 に答える 1

10

C#5 / .Net 4.5を使用してこのようなことを行う最良の方法は、 TPLDataflowを使用することだと思います。それを使用してWebクローラーを実装する方法についてのウォークスルーもあります。

基本的に、1つのURLをダウンロードし、そこからリンクを取得する1つの「ブロック」を作成します。

var cts = new CancellationTokenSource();

Func<LinkItem, Task<IEnumerable<LinkItem>>> downloadFromLink =
    async link =>
            {
                // WebClient is not guaranteed to be thread-safe,
                // so we shouldn't use one shared instance
                var client = new WebClient();
                string html = await client.DownloadStringTaskAsync(link.Href);

                return LinkFinder.Find(html, link.BaseURL);
            };

var linkFinderBlock = new TransformManyBlock<LinkItem, LinkItem>(
    downloadFromLink,
    new ExecutionDataflowBlockOptions
    { MaxDegreeOfParallelism = 4, CancellationToken = cts.Token });

任意の値に設定できMaxDegreeOfParallelismます。最大でいくつのURLを同時にダウンロードできるかを示します。制限したくない場合は、に設定できますDataflowBlockOptions.Unbounded

次に、ダウンロードしたすべてのリンクをリストに保存するなど、何らかの方法で処理する1つのブロックを作成します。また、ダウンロードをいつキャンセルするかを決定することもできます。

var links = new List<LinkItem>();

var storeBlock = new ActionBlock<LinkItem>(
    linkItem =>
    {
        links.Add(linkItem);
        if (links.Count == maxSize)
            cts.Cancel();
    });

を設定しなかったためMaxDegreeOfParallelism、デフォルトで1に設定されています。つまり、ここではスレッドセーフではないコレクションを使用しても問題ありません。

もう1つのブロックを作成します。これは、からのリンクを取得しlinkFinderBlock、に渡しstoreBlockますlinkFinderBlock

var broadcastBlock = new BroadcastBlock<LinkItem>(li => li);

コンストラクターのラムダは「クローン関数」です。必要に応じてアイテムのクローンを作成するために使用できますが、作成後に変更しないため、ここでは必要ありませんLinkItem

これで、ブロックを接続できます。

linkFinderBlock.LinkTo(broadcastBlock);
broadcastBlock.LinkTo(storeBlock);
broadcastBlock.LinkTo(linkFinderBlock);

次に、最初のアイテムをに渡すことで処理を開始できますlinkFinderBlock(またはbroadcastBlock、も送信したい場合はstoreBlock):

linkFinderBlock.Post(firstItem);

そして最後に、処理が完了するまで待ちます。

try
{
    linkFinderBlock.Completion.Wait();
}
catch (AggregateException ex)
{
    if (!(ex.InnerException is TaskCanceledException))
        throw;
}
于 2012-02-13T14:35:19.887 に答える