大量下载网页C#

时间:2011-09-19 16:46:50

标签: c# web-crawler

我的应用程序要求我将大量网页下载到内存中以进行进一步的解析和处理。最快的方法是什么?我目前的方法(如下所示)似乎太慢,偶尔会导致超时。

for (int i = 1; i<=pages; i++)
{
    string page_specific_link = baseurl + "&page=" + i.ToString();

    try
    {    
        WebClient client = new WebClient();
        var pagesource = client.DownloadString(page_specific_link);
        client.Dispose();
        sourcelist.Add(pagesource);
    }
    catch (Exception)
    {
    }
}

7 个答案:

答案 0 :(得分:4)

您解决此问题的方式很大程度上取决于您要下载的网页数量以及您引用的网站数量。

我会使用像1,000这样的好轮数。如果您想从单个网站下载那么多页面,那么与您要下载分布在数十个或数百个网站上的1,000个网页相比,这需要花费更长的时间。原因是,如果您使用大量并发请求访问单个站点,您可能最终会被阻止。

所以你必须实施一种&#34;礼貌政策,&#34;在单个站点上发出多个请求之间的延迟。延迟的长度取决于许多事情。如果网站的robots.txt文件包含crawl-delay条目,您应该尊重这一点。如果他们不希望您每分钟访问多个页面,那么这就像您应该抓取的速度一样快。如果没有crawl-delay,您应该根据网站响应的时间延迟。例如,如果您可以在500毫秒内从站点下载页面,则将延迟设置为X.如果需要一整秒,请将延迟设置为2X。您可以将延迟限制为60秒(除非crawl-delay更长),我建议您将延迟设置为5到10秒。

我不建议使用Parallel.ForEach。我的测试表明它没有做好。有时它会对连接过度征税,而且通常它不允许足够的并发连接。我会创建一个WebClient个实例的队列,然后编写类似的东西:

// Create queue of WebClient instances
BlockingCollection<WebClient> ClientQueue = new BlockingCollection<WebClient>();
// Initialize queue with some number of WebClient instances

// now process urls
foreach (var url in urls_to_download)
{
    var worker = ClientQueue.Take();
    worker.DownloadStringAsync(url, ...);
}

初始化进入队列的WebClient个实例时,将其OnDownloadStringCompleted事件处理程序设置为指向已完成的事件处理程序。该处理程序应该将字符串保存到文件中(或者您应该只使用DownloadFileAsync),然后客户端将自己添加回ClientQueue

在我的测试中,我已经能够使用此方法支持10到15个并发连接。除此之外,我遇到了DNS解析的问题(`DownloadStringAsync&#39;没有异步进行DNS解析)。你可以获得更多的联系,但这样做是很多工作。

这是我过去采用的方法,它可以很快地下载数千页。但这绝对不是我用高性能网络爬虫所采用的方法。

我还应该注意,这两个代码块之间的资源使用存在巨大的差异:

WebClient MyWebClient = new WebClient();
foreach (var url in urls_to_download)
{
    MyWebClient.DownloadString(url);
}

---------------

foreach (var url in urls_to_download)
{
    WebClient MyWebClient = new WebClient();
    MyWebClient.DownloadString(url);
}

第一个分配用于所有请求的单个WebClient实例。第二个为每个请求分配一个WebClient。差异很大。 WebClient使用大量系统资源,在相对较短的时间内分配数千个资源会影响性能。相信我......我遇到了这个问题。您最好只分配10或20 WebClient个(并行处理所需的数量),而不是每个请求分配一个。

答案 1 :(得分:4)

为什么不使用网络抓取框架。它可以处理所有你喜欢的东西(多线程,httprequests,解析链接,调度,礼貌等)。

Abot(https://code.google.com/p/abot/)为你处理所有这些东西,并用c#编写。

答案 2 :(得分:2)

除了@Davids perfectly valid answer之外,我还希望为他的方法添加一个稍微清晰的“版本”。

var pages = new List<string> { "http://bing.com", "http://stackoverflow.com" };
var sources = new BlockingCollection<string>();

Parallel.ForEach(pages, x =>
{
    using(var client = new WebClient())
    {
        var pagesource = client.DownloadString(x);
        sources.Add(pagesource);
    }
});

另一种使用异步的方法:

static IEnumerable<string> GetSources(List<string> pages)
{
    var sources = new BlockingCollection<string>();
    var latch = new CountdownEvent(pages.Count);

    foreach (var p in pages)
    {
        using (var wc = new WebClient())
        {
            wc.DownloadStringCompleted += (x, e) =>
            {
                sources.Add(e.Result);
                latch.Signal();
            };

            wc.DownloadStringAsync(new Uri(p));
        }
    }

    latch.Wait();

    return sources;
}

答案 3 :(得分:1)

您应该为此目的使用并行编程。

有很多方法可以达到你想要的效果;最简单的就是这样:

var pageList = new List<string>();

for (int i = 1; i <= pages; i++)
{
  pageList.Add(baseurl + "&page=" + i.ToString());
}


// pageList  is a list of urls
Parallel.ForEach<string>(pageList, (page) =>
{
  try
    {
      WebClient client = new WebClient();
      var pagesource = client.DownloadString(page);
      client.Dispose();
      lock (sourcelist)
      sourcelist.Add(pagesource);
    }

    catch (Exception) {}
});

答案 4 :(得分:0)

我有一个类似的案例,这就是我解决的问题

using System;
    using System.Threading;
    using System.Collections.Generic;
    using System.Net;
    using System.IO;

namespace WebClientApp
{
class MainClassApp
{
    private static int requests = 0;
    private static object requests_lock = new object();

    public static void Main() {

        List<string> urls = new List<string> { "http://www.google.com", "http://www.slashdot.org"};
        foreach(var url in urls) {
            ThreadPool.QueueUserWorkItem(GetUrl, url);
        }

        int cur_req = 0;

        while(cur_req<urls.Count) {

            lock(requests_lock) {
                cur_req = requests; 
            }

            Thread.Sleep(1000);
        }

        Console.WriteLine("Done");
    }

private static void GetUrl(Object the_url) {

        string url = (string)the_url;
        WebClient client = new WebClient();
        Stream data = client.OpenRead (url);

        StreamReader reader = new StreamReader(data);
        string html = reader.ReadToEnd ();

        /// Do something with html
        Console.WriteLine(html);

        lock(requests_lock) {
            //Maybe you could add here the HTML to SourceList
            requests++; 
        }
    }
}

你应该考虑使用Paralel,因为速度慢是因为你的软件正在等待I / O,为什么不在我等待I / O的另一个线程开始的时候。

答案 5 :(得分:0)

虽然其他答案完全有效,但所有这些答案(在撰写本文时)都忽略了一些非常重要的事情:对网络的调用是IO bound,让一个线程等待这样的操作压缩系统资源并对系统资源产生影响。

您真正想要做的是利用WebClient class上的异步方法(正如有些人指出的那样)以及Task Parallel Library处理Event-Based Asynchronous Pattern的能力。

首先,您将获得要下载的网址:

IEnumerable<Uri> urls = pages.Select(i => new Uri(baseurl + 
    "&page=" + i.ToString(CultureInfo.InvariantCulture)));

然后,您将为每个URL创建一个新的WebClient实例,使用TaskCompletionSource<T> class异步处理调用(这不会刻录线程):

IEnumerable<Task<Tuple<Uri, string>> tasks = urls.Select(url => {
    // Create the task completion source.
    var tcs = new TaskCompletionSource<Tuple<Uri, string>>();

    // The web client.
    var wc = new WebClient();

    // Attach to the DownloadStringCompleted event.
    client.DownloadStringCompleted += (s, e) => {
        // Dispose of the client when done.
        using (wc)
        {
            // If there is an error, set it.
            if (e.Error != null) 
            {
                tcs.SetException(e.Error);
            }
            // Otherwise, set cancelled if cancelled.
            else if (e.Cancelled) 
            {
                tcs.SetCanceled();
            }
            else 
            {
                // Set the result.
                tcs.SetResult(new Tuple<string, string>(url, e.Result));
            }
        }
    };

    // Start the process asynchronously, don't burn a thread.
    wc.DownloadStringAsync(url);

    // Return the task.
    return tcs.Task;
});

现在您有一个IEnumerable<T>可以转换为数组并使用Task.WaitAll等待所有结果:

// Materialize the tasks.
Task<Tuple<Uri, string>> materializedTasks = tasks.ToArray();

// Wait for all to complete.
Task.WaitAll(materializedTasks);

然后,您可以在Task<T>个实例上使用Result property来获取网址和内容对:

// Cycle through each of the results.
foreach (Tuple<Uri, string> pair in materializedTasks.Select(t => t.Result))
{
    // pair.Item1 will contain the Uri.
    // pair.Item2 will contain the content.
}

请注意,上述代码有一个没有错误处理的警告。

如果您想获得更多的吞吐量,而不是等待整个列表完成,您可以在完成下载后处理单个页面的内容; Task<T>意味着像管道一样使用,当你完成了你的工作单元,让它继续下一个,而不是等待所有的项目完成(如果它们可以在一个异步方式)。

答案 6 :(得分:0)

我正在使用活动的线程数和任意限制:

private static volatile int activeThreads = 0;

public static void RecordData()
{
  var nbThreads = 10;
  var source = db.ListOfUrls; // Thousands urls
  var iterations = source.Length / groupSize; 
  for (int i = 0; i < iterations; i++)
  {
    var subList = source.Skip(groupSize* i).Take(groupSize);
    Parallel.ForEach(subList, (item) => RecordUri(item)); 
    //I want to wait here until process further data to avoid overload
    while (activeThreads > 30) Thread.Sleep(100);
  }
}

private static async Task RecordUri(Uri uri)
{
   using (WebClient wc = new WebClient())
   {
      Interlocked.Increment(ref activeThreads);
      wc.DownloadStringCompleted += (sender, e) => Interlocked.Decrement(ref iterationsCount);
      var jsonData = "";
      RootObject root;
      jsonData = await wc.DownloadStringTaskAsync(uri);
      var root = JsonConvert.DeserializeObject<RootObject>(jsonData);
      RecordData(root)
    }
}