2011-09-19 20 views
5

Moja aplikacja wymaga pobierania dużej ilości stron internetowych z pamięci w celu dalszego przetwarzania i przetwarzania. Jaki jest najszybszy sposób to zrobić? Moja obecna metoda (pokazana poniżej) wydaje się zbyt powolna i czasami skutkuje przekroczeniem limitu czasu.Masowe pobieranie stron internetowych 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

Trzeba połączenie T1 –

+2

Ponieważ wiele odpowiedzi sugeruje równoległy sprowadzanie, chcę cię ostrzec przed wysłaniem zbyt wielu jednoczesnych żądań; możesz zostać zbanowany, jeśli strona nie jest przyjazna. Również będzie limit, na ile pomaga każdy dodatkowy wątek i poza punkt, który spowoduje degradację. –

+0

@Hemal Pandya: Ważna troska, to nie * to * wiele niepokoju; klasa 'WebClient' ostatecznie użyje klas' HttpWebRequest'/'HttpWebResponse', które używają klasy' ServicePointManager'. Domyślnie "ServicePointManager" ograniczy liczbę pobrań do dwóch na raz dla konkretnej domeny (zgodnie z zaleceniem w specyfikacji HTTP 1.1). – casperOne

Odpowiedz

3

Sposób podejścia do tego problemu zależy w dużej mierze od liczby stron, które chcesz pobrać oraz od liczby stron, do których się odwołujesz.

Użyję dobrej liczby okrągłej jak 1000. Jeśli chcesz pobrać tyle stron z jednej witryny, zajmie to znacznie więcej czasu niż pobranie 1000 stron rozłożonych na dziesiątkach lub setkach witryn. Powodem jest to, że jeśli trafisz na jedną stronę z całą masą współbieżnych żądań, prawdopodobnie zostaniesz zablokowany.

Musisz więc wprowadzić rodzaj "polityki grzecznościowej", która powoduje opóźnienie między wieloma żądaniami w jednej witrynie. Długość tego opóźnienia zależy od wielu rzeczy. Jeśli plik robots.txt witryny ma wpis crawl-delay, powinieneś to uszanować. Jeśli nie chcą, abyś uzyskiwał dostęp do więcej niż jednej strony na minutę, jest to tak szybkie, jak powinno się czołgać. Jeśli nie ma numeru crawl-delay, należy oprzeć opóźnienie na czasie reakcji witryny. Na przykład, jeśli możesz pobrać stronę z witryny w ciągu 500 milisekund, ustawiasz opóźnienie na X. Jeśli zajmuje to pełną sekundę, ustaw opóźnienie na 2X. Prawdopodobnie możesz ograniczyć opóźnienie do 60 sekund (chyba że crawl-delay jest dłuższy), a ja zaleciłbym ustawienie minimalnego opóźnienia od 5 do 10 sekund.

Nie polecam do tego celu użycia . Moje testy wykazały, że nie sprawdza się to dobrze. Czasami nadmiernie obciąża połączenie i często nie pozwala na wystarczającą liczbę jednoczesnych połączeń. Chciałbym zamiast tworzyć kolejkę WebClient instancji, a następnie napisać coś takiego:

// 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, ...); 
} 

czasie inicjowania WebClient instancje, które go do kolejki, ustawić swoje OnDownloadStringCompleted obsługi zdarzeń, aby wskazywał wypełnionego obsługi zdarzeń. Ten program obsługi powinien zapisać ciąg do pliku (lub może po prostu użyć DownloadFileAsync), a następnie klient, , dodaje się ponownie do ClientQueue.

Podczas testów mogłem obsłużyć od 10 do 15 równoczesnych połączeń za pomocą tej metody. Co więcej, mam problemy z rozpoznawaniem DNS (`DownloadStringAsync 'nie asynchronicznie zmienia rozdzielczość DNS). Możesz uzyskać więcej połączeń, ale jest to dużo pracy.

Takie podejście podjąłem w przeszłości i bardzo dobrze działało, ponieważ szybko pobierało tysiące stron. Z pewnością nie jest to podejście, które podjąłem z moim zaawansowanym robotem sieciowym.

I należy również pamiętać, że istnieje różnica w ogromny wykorzystania zasobów między tymi dwoma blokami kodu:

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

Pierwszy przydziela jeden WebClient instancji, który jest używany dla wszystkich żądań. Drugi przydziela jeden WebClient dla każdego żądania. Różnica jest ogromna. WebClient wykorzystuje wiele zasobów systemowych, a przydzielanie tysięcy w stosunkowo krótkim czasie wpłynie na wydajność. Uwierz mi ... Wpadłem na to. Lepiej przydzielać tylko 10 lub 20 WebClient s (tyle ile potrzeba do przetwarzania współbieżnego), zamiast przydzielać jedną na żądanie.

+0

Czytałem gdzieś, że ręczne rozwiązywanie dns dla witryny i używanie go do DownloadStringAsync pomaga wydajności. Próbowałeś kiedyś tego Jima? – paradox

+0

@paradox: Tak, serwer DNS jest rozwiązywany z wyprzedzeniem, aby prawdopodobnie znajdował się w pamięci podręcznej rozpoznawania nazw DNS komputera. Robię coś bardzo podobnego do tego w moim robocie, a dzięki temu mogę uzyskać 100 połączeń na sekundę. To trochę trudny do zrobienia dla prostej aplikacji do pobierania. Należy jednak pamiętać, że w przypadku pojedynczego żądania wykonanie DNS, a następnie zgłoszenie nie będzie wykonywane szybciej niż samo wysłanie żądania. Rozwiązanie DNS z wyprzedzeniem przyspiesza działanie, jeśli możesz to zrobić podczas pobierania innych stron. –

+0

co z równoległym foreach zrobionym w ten sposób? https://stackoverflow.com/questions/46284818/parallel-request-to-scrape-multiple-pages-of-site.com – sofsntp

1

Do tego celu należy użyć programowania równoległego.

Istnieje wiele sposobów osiągnięcia tego, czego potrzebujesz; najłatwiej byłoby coś takiego:

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

Błędne jest również pisanie do 'sourcelist' bez synchronizowania dostępu do niego. Istnieje duża szansa, że ​​lista zostanie uszkodzona w wyniku. – casperOne

+0

całkowicie prawy jesteś;) – David

+0

'foreach' nie działa równolegle, nawet jeśli używasz' AsParallel'. musisz użyć 'Parallel.ForEach'. – Dani

0

miałem podobny przypadek, i to w jaki sposób mogę rozwiązać

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

Trzeba myśleć używając Paralel dlatego, że powolna prędkość bo jesteś oprogramowanie czeka dla I/O i dlaczego nie podczas gdy wątek czeka na I/O, rozpoczyna się inny.

2

Oprócz dodania @Davids perfectly valid answer, chcę dodać nieco czystszą "wersję" jego podejścia.

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

Jeszcze inne podejście, które wykorzystuje asynchroniczny:

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

Podczas gdy inne odpowiedzi są całkowicie poprawny, wszystkie z nich (w momencie pisania tego tekstu) zaniedbujemy coś bardzo ważnego: wywołania do sieci to IO bound, mając wątek oczekiwania na operację, która spowoduje obciążenie zasobów systemowych i wpłynie na zasoby systemowe.

Co naprawdę chcesz zrobić, to skorzystać z metod asynchronicznych na WebClient class (jak niektórzy wskazał), jak również zdolność Task Parallel Library „s do obsługi Event-Based Asynchronous Pattern.

pierwsze, co można uzyskać adresy URL, które chcesz pobrać:

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

Następnie należy utworzyć nową instancję WebClient dla każdego adresu URL, używając TaskCompletionSource<T> class obsługiwać połączenia asynchronicznie (nie będzie spalić nitki):

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

teraz mają IEnumerable<T> które można przekonwertować do tablicy i czekać na wszystkich wyników za pomocą Task.WaitAll:

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

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

Następnie można po prostu użyć Result property na Task<T> przypadkach, aby uzyskać parę URL i treść:

// 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. 
} 

Zauważ, że powyższy kod ma zastrzeżenie o nie posiadające obsługę błędów.

Jeśli chcesz uzyskać jeszcze większą przepustowość, zamiast czekać na zakończenie całej listy, możesz przetworzyć zawartość pojedynczej strony po zakończeniu pobierania; Task<T> ma być używany jak potok, po zakończeniu pracy jednostki, należy przejść do następnego, zamiast czekać na wszystkie elementy do zrobienia (jeśli można to zrobić w sposób asynchroniczny).

+0

Przekazywanie wzdłuż (odrzuconej) sugerowanej edycji: * PobierzStringAsync nie przejmuj się "ciągiem znaków" - tylko dla "Uri". * – user7116

+0

@sletterletykresy: Dziękujemy za sugestię; zmodyfikował go tak, aby używał 'Uri' przez całą drogę. – casperOne

+0

To wygląda jak pseduocode. Brakuje '>' w kilku miejscach. Przykład: tutaj => 'IEnumerable > Zadania' Kod nie zostanie skompilowany, a niektóre typy są błędne. – Shiva

4

Dlaczego po prostu nie używać ram indeksowania stron internetowych. Może obsłużyć wszystkie rzeczy dla Ciebie (wielowątkowość, zapytania HTTP, linki do analizowania, planowanie, uprzejmość itp.).

Abot (https://code.google.com/p/abot/) obsługuje wszystkie te rzeczy dla Ciebie i jest napisany w języku C#.

+2

Używam Abot już od kilku miesięcy, i znalazłem go bardzo rozszerzalny i bardzo dobrze napisany. Jest również dobrze zarządzany, więc istnieją całkiem regularne aktualizacje bazy kodu. Możesz zmienić sposób, w jaki robota pojawia się jako klient, szanować roboty i wstrzykiwać własne programy obsługi z możliwością rozszerzenia innych wbudowanych w inne klasy. – jamesbar2

0

Używam aktywnych wątków liczyć i arbitralne ograniczenie:

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) 
    } 
} 
Powiązane problemy