2011-09-19 21 views
5

La mia applicazione richiede di scaricare una grande quantità di pagine Web in memoria per un'ulteriore analisi ed elaborazione. Qual è il modo più veloce per farlo? Il mio metodo attuale (mostrato sotto) sembra essere troppo lento e occasionalmente si verifica un timeout.Download in massa di pagine Web 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

È necessaria una connessione T1 –

+2

Dal momento che molte risposte sono suggerendo il recupero parallelo, voglio guardia contro l'invio di troppe richieste simultanee; potresti essere bannato se il sito web non è amichevole. Inoltre, ci sarà un limite a quanto ogni thread aggiuntivo aiuti e oltre un punto che causerà il degrado. –

+0

@Hemal Pandya: una preoccupazione valida, che non è * quella * molto di una preoccupazione; la classe 'WebClient' userà infine le classi' HttpWebRequest'/'HttpWebResponse' che usano la classe' ServicePointManager'. Il 'ServicePointManager' per impostazione predefinita limiterà la maggior parte dei download a due alla volta per un dominio particolare (come da raccomandazione nella specifica HTTP 1.1). – casperOne

risposta

3

Il modo in cui affronta questo problema dipenderà molto dal numero di pagine che desideri scaricare e da quanti siti stai facendo riferimento.

Userò un numero di round buono come 1,000. Se si desidera scaricare più pagine da un singolo sito, ci vorrà molto più tempo rispetto a quando si desidera scaricare 1.000 pagine distribuite su dozzine o centinaia di siti. Il motivo è che se colpisci un singolo sito con un sacco di richieste simultanee, probabilmente finirai per essere bloccato.

Quindi è necessario implementare un tipo di "politica di cortesia" che emette un ritardo tra più richieste su un singolo sito. La durata di questo ritardo dipende da un numero di cose. Se il file robots.txt del sito ha una voce crawl-delay, dovresti rispettarlo. Se non vogliono che tu acceda a più di una pagina al minuto, allora è veloce come dovresti eseguire la scansione. Se non c'è crawl-delay, dovresti basare il tuo ritardo su quanto tempo impiega un sito a rispondere. Ad esempio, se puoi scaricare una pagina dal sito in 500 millisecondi, imposta il ritardo su X. Se impiega un secondo intero, imposta il ritardo su 2X. Probabilmente è possibile limitare il ritardo a 60 secondi (a meno che lo crawl-delay non sia più lungo) e si consiglia di impostare un ritardo minimo compreso tra 5 e 10 secondi.

Non mi consiglia di utilizzare Parallel.ForEach per questo. I miei test hanno dimostrato che non fa un buon lavoro. A volte esagerare la connessione e spesso non consente un numero sufficiente di connessioni simultanee. Vorrei invece creare una coda di WebClient istanze e scrivere qualcosa come:

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

Quando si inizializza le WebClient istanze che vanno in coda, impostare le loro OnDownloadStringCompleted gestori di eventi per puntare a un gestore di eventi completata. Quel gestore deve salvare la stringa in un file (o forse dovresti semplicemente usare DownloadFileAsync), quindi il client, si aggiunge nuovamente allo ClientQueue.

Nel mio test, sono stato in grado di supportare da 10 a 15 connessioni simultanee con questo metodo. Più di questo e mi imbatto in problemi con la risoluzione DNS (`DownloadStringAsync 'non esegue la risoluzione DNS in modo asincrono). Puoi ottenere più connessioni, ma farlo è molto lavoro.

Questo è l'approccio che ho adottato in passato e ha funzionato molto bene per scaricare rapidamente migliaia di pagine. Non è sicuramente l'approccio che ho seguito con il mio crawler Web ad alte prestazioni.

Vorrei anche notare che c'è una differenza enorme nell'uso delle risorse tra questi due blocchi di codice:

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

La prima assegna un unico WebClient istanza che viene utilizzato per tutte le richieste. Il secondo assegna uno WebClient per ogni richiesta. La differenza è enorme. WebClient utilizza molte risorse di sistema e l'allocazione di migliaia di esse in un tempo relativamente breve inciderà sulle prestazioni. Credimi ... mi sono imbattuto in questo. È meglio allocare solo 10 o 20 WebClient s (tanti quanti ne occorrono per l'elaborazione simultanea), piuttosto che assegnarne uno per richiesta.

+0

Ho letto da qualche parte che la risoluzione manuale del DNS per il sito e l'utilizzo di DownloadStringAsync aiuta le prestazioni. Hai mai provato quel Jim? – paradox

+0

@paradox: Sì, il DNS viene risolto in anticipo in modo che sia probabilmente nella cache del resolver DNS della macchina. Faccio qualcosa di abbastanza simile a quello nel mio crawler, e posso farlo con 100 connessioni al secondo. È un po 'un dolore da fare per una semplice applicazione di download, però. Si noti, tuttavia, che per una singola richiesta, eseguire il DNS e quindi effettuare la richiesta non verrà eseguita più rapidamente rispetto all'emissione della richiesta. Risolvere il DNS in anticipo rende le cose solo più veloci se puoi farlo mentre vengono scaricate altre pagine. –

+0

e per quanto riguarda il parallel foreach fatto in questo modo? https://stackoverflow.com/questions/46284818/parallel-request-to-scrape-multiple-pages-of-a-website – sofsntp

1

È necessario utilizzare la programmazione parallela per questo scopo.

Ci sono molti modi per ottenere quello che vuoi; il modo più semplice sarebbe qualcosa di simile:

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

È anche sbagliato in quanto sta scrivendo su 'sourcelist' senza sincronizzarne l'accesso. C'è una buona probabilità che la lista sia danneggiata di conseguenza. – casperOne

+0

totalmente a posto;) – David

+0

'foreach' non funziona in parallelo anche se si usa' AsParallel'. devi usare 'Parallel.ForEach'. – Dani

0

ho avuto un caso simile, ed è così che ho risolto

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

Si dovrebbe pensare utilizzando Parallel di quanto la velocità lenta è perché sei il software è in attesa per I/O e perché non mentre un thread in attesa di I/O ne viene avviato un altro.

2

Oltre a @Davids perfectly valid answer, voglio aggiungere una "versione" leggermente più pulita del suo approccio.

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

Ancora un altro approccio, che utilizza async:

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

Mentre le altre risposte sono perfettamente valide, tutti loro (al momento della stesura di questo) sono trascurare qualcosa di molto importante: le chiamate al web sono IO bound, con un thread wait su un'operazione come questa sta andando a filtrare le risorse di sistema e ha un impatto sulle risorse del sistema.

cosa si vuole veramente fare è sfruttare i metodi asincroni sul WebClient class (come alcuni hanno fatto notare), così come la capacità s' il Task Parallel Library per gestire il Event-Based Asynchronous Pattern.

In primo luogo, si dovrebbe ottenere gli URL che si desidera scaricare:

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

Poi, si potrebbe creare una nuova istanza WebClient per ogni URL, utilizzando la TaskCompletionSource<T> class per gestire le chiamate in modo asincrono (questo non lo farà masterizzare un filo):

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

Ora avete un IEnumerable<T> cui è possibile convertire in un array e attendere su tutti i risultati utilizzando Task.WaitAll:

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

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

Poi, si può semplicemente utilizzare Result property sulle Task<T> istanze per ottenere la coppia di URL e il contenuto:

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

Nota che il codice di cui sopra ha l'avvertenza di non avere una gestione degli errori.

Se si desidera ottenere un throughput ancora maggiore, invece di attendere che l'intera lista sia terminata, è possibile elaborare il contenuto di una singola pagina dopo averlo scaricato; Task<T> è pensato per essere usato come una pipeline, quando hai completato la tua unità di lavoro, continua con quella successiva invece di aspettare che tutti gli elementi siano eseguiti (se possono essere fatti in modo asincrono).

+0

Passare lungo una modifica suggerita (rifiutata): * DownloadStringAsync non richiede un sovraccarico per "string" - solo per "Uri". * – user7116

+0

@sixlettervariables: Grazie per il suggerimento; modificato per usare 'Uri' per tutto il percorso. – casperOne

+0

Sembra simile a pseduocode. '>' In diversi punti. Es: here => 'IEnumerable > tasks' Il codice non verrà compilato e alcuni tipi sono errati. – Shiva

4

Perché non utilizzare solo una struttura di scansione del Web. Può gestire tutte le cose per te (multithreading, httprequests, link di analisi, programmazione, cortesia, ecc.).

Abot (https://code.google.com/p/abot/) gestisce tutte queste cose per te ed è scritto in C#.

+2

Ho usato Abot per alcuni mesi e l'ho trovato altamente estensibile e molto ben scritto. È anche ben gestito, quindi ci sono aggiornamenti abbastanza regolari per la base di codice. Hai la possibilità di modificare il modo in cui il tuo crawler appare come un client, rispettare i robot e iniettare i tuoi gestori con la possibilità di estendere l'altro costruito in altre classi. – jamesbar2

0

Sto usando contano un thread attivi e un limite arbitrario:

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