2011-09-19 91 views
5

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

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) 
    { 
    } 
} 
+4

你需要一个T1连接 –

+2

由于许多答案都暗示并行抓取,我想提醒你对发送过多的并发请求;如果网站不友好,您可能会被禁止。此外,每增加一个线程会有多大的帮助,并且会超出一定程度会导致性能下降。 –

+0

@Hemal Pandya:这是一个值得关注的问题,那不是*关注的问题; WebClient类最终将使用使用'ServicePointManager'类的'HttpWebRequest' /'HttpWebResponse'类。默认情况下,“ServicePointManager”会将特定域的大多数下载次数限制为两次(按照HTTP 1.1规范中的建议)。 – casperOne

回答

3

您解决这个问题的方式将非常依赖于您要下载的页数,以及您引用的网站数。

我会使用一个好的数字,如1,000。如果您希望从单个网站下载多个网页,则需要花费比您想要下载的跨越数十个或数百个网站的1,000个网页更长的时间。原因是,如果你用一大堆并发请求单击一个站点,你最终可能会被阻止。

因此,您必须实施一种“礼貌策略”,即在单个网站上的多个请求之间发出延迟。该延迟的长度取决于许多事情。如果网站的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'不会异步执行DNS解析)。你可以获得更多的联系,但这样做很多工作。

这就是我过去采用的方法,它可以很快地下载数千页的页面。尽管如此,这绝对不是我用我的高性能Web爬虫所采取的方法。

我也应该注意,在这些代码两个块之间的资源使用一个巨大区别:

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(尽可能多地进行并发处理),而不是为每个请求分配一个。

+0

我读过一些手动解析站点的dns并将其用于DownloadStringAsync的地方,可以帮助提高性能。曾经试过那个吉姆? – paradox

+0

@paradox:是的,您提前解析DNS,以便它可能位于您的计算机的DNS解析程序缓存中。我做了一些与我的抓取工具非常相似的工具,通过这样做,我可以每秒获得超过100个连接。不过,这对于简单的下载应用程序来说是一件很痛苦的事情。但请注意,对于单个请求,执行DNS然后发出请求不会比发出请求更快地执行。提前解析DNS只会让事情变得更快,因为如果您可以在下载其他网页的同时做到这一点。 –

+0

这样做的平行foreach呢? https://stackoverflow.com/questions/46284818/parallel-request-to-scrape-multiple-pages-of-a-website – sofsntp

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) {} 
}); 
+1

这也是错误的,因为它正在写入'sourcelist'而没有同步对它的访问。这个列表很可能因此而被损坏。 – casperOne

+0

完全正确;) – David

+0

即使使用AsParallel,foreach也不会并行运行。你必须使用'Parallel.ForEach'。 – Dani

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++; 
     } 
    } 
} 

,因为你的软件正在等待你应该考虑使用相同常的,因为速度慢是对于I/O,为什么不等待I/O另一个线程开始。

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; 
} 
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创建一个新的Web客户端例如,使用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; 
}); 

现在你已经使用Task.WaitAllIEnumerable<T>,你可以转换成一个阵列并等待所有的结果:

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

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

然后,你可以只使用Result propertyTask<T>实例,以获得对网址和内容:

// 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>意思是像管道一样使用,当你完成你的工作单元时,让它继续到下一个工作单元,而不是等待所有项目完成(如果它们可以以异步方式完成)。

+0

传递(拒绝)建议的编辑:* DownloadStringAsync不要为“字符串”重载 - 仅针对“Uri”。* – user7116

+0

@sletterlettervariables:感谢您的建议;修改它在整个过程中使用'Uri'。 – casperOne

+0

这看起来像pseduocode。你在几个地方缺少'>'。例如:here =>'IEnumerable > tasks'代码不会编译,某些类型错误。 – Shiva

4

为什么不只是使用网络爬行框架。它可以为你处理所有的东西(多线程,httprequests,解析链接,日程安排,礼貌等)。

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

+2

我已经使用Abot几个月了,并且已经发现它具有高度的可扩展性并且写得很好。它的管理也很好,所以对代码库进行定期更新。您可以选择调整抓取工具作为客户端的显示方式,尊重机器人,并注入自己的处理程序,以便扩展其他类中构建的其他处理程序。 – jamesbar2

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) 
    } 
}