使用异步CTP同时下载HTML页面

本文关键字:下载 HTML 页面 异步 CTP | 更新日期: 2023-09-27 18:24:12

尝试使用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执行第一次(也是唯一一次迭代)时,我只有一个项。我能想到的最简单的方法是使ForEach递归,但我不能(我不认为)这样做,因为我会很快耗尽堆栈空间。

有人能告诉我如何重组这段代码,以创建我所说的递归延续,添加项目,直到达到MaxStackSize或系统内存不足?

使用异步CTP同时下载HTML页面

我认为使用C#5/.Net 4.5进行类似操作的最佳方法是使用TPL数据流。甚至还有一个关于如何使用它实现网络爬虫的演练

基本上,您创建一个"块",负责下载一个URL并从中获取链接:

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

然后创建一个块,以某种方式处理所有下载的链接,就像将它们全部存储在列表中一样。它还可以决定何时取消下载:

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

由于我们没有设置MaxDegreeOfParallelism,因此它默认为1。这意味着在这里使用非线程安全的集合应该是可以的。

我们再创建一个块:它将从linkFinderBlock获取一个链接,并将其传递给storeBlocklinkFinderBlock

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

其构造函数中的lambda是一个"克隆函数"。如果您愿意,可以使用它来创建项目的克隆,但这里不需要它,因为我们在创建后不会修改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;
}