2011-09-19 51 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) 
    } 
}