Массовое скачивание веб-страниц C # - PullRequest
6 голосов
/ 19 сентября 2011

Мое приложение требует, чтобы я загружал большое количество веб-страниц в память для дальнейшего анализа и обработки.Какой самый быстрый способ сделать это?Мой текущий метод (показанный ниже) кажется слишком медленным и иногда приводит к таймаутам.

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 ]

5 голосов
/ 19 сентября 2011

Способ решения этой проблемы во многом будет зависеть от того, сколько страниц вы хотите загрузить и на скольких сайтах вы ссылаетесь.

Я буду использовать хорошее круглое число, например 1000.Если вы хотите загрузить столько страниц с одного сайта, это займет намного больше времени, чем если бы вы захотели загрузить 1000 страниц, которые распределены по десяткам или сотням сайтов.Причина в том, что если вы попали на один сайт с целой кучей одновременных запросов, вы, вероятно, в конечном итоге заблокируетесь.между несколькими запросами на одном сайте.Продолжительность этой задержки зависит от ряда вещей.Если в файле 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 асинхронно).Вы можете получить больше подключений, но сделать это - большая работа.

Это подход, который я использовал в прошлом, и он очень хорошо работал для быстрой загрузки тысяч страниц.Однако это определенно не тот подход, который я использовал в своем высокопроизводительном веб-сканере.

Следует также отметить, что между этими двумя блоками кода существует огромная разница в использовании ресурсов:

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 с (столько, сколько нужно для параллельной обработки), чем выделять по одному на запрос.

4 голосов
/ 13 апреля 2013

Почему бы просто не использовать веб-рамки для сканирования.Он может обрабатывать все, что вам нравится (многопоточность, httprequests, анализ ссылок, планирование, вежливость и т. Д.).

Abot (https://code.google.com/p/abot/) обрабатывает все это за вас и написано на c #.

2 голосов
/ 19 сентября 2011

В дополнение к @ совершенно правильному ответу Дэвидса я хочу добавить немного более "чистую" версию своего подхода.

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

Еще один подход, в котором используется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;
}
1 голос
/ 19 сентября 2011

Вы должны использовать параллельное программирование для этой цели.

Есть много способов достичь того, чего вы хотите;самым простым будет что-то вроде этого:

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) {}
});
0 голосов
/ 20 сентября 2017

Я использую количество активных потоков и произвольное ограничение:

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)
    }
}
0 голосов
/ 19 сентября 2011

В то время как другие ответы совершенно верны, все они (на момент написания этой статьи) пренебрегают чем-то очень важным: обращения к Интернету IO привязаны , и поток ожидает такую ​​операцию, как это приведет к нагрузке на системные ресурсы и повлияет на системные ресурсы.

Что вы действительно хотите сделать, так это воспользоваться асинхронными методами в WebClient классе (как некоторые уже указали), а также в Task Parallel Library . способность обрабатывать Асинхронный шаблон на основе событий .

Сначала вы получите URL, которые вы хотите загрузить:

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

Затем вы должны создать новый экземпляр WebClient для каждого URL-адреса, используя TaskCompletionSource<T> класс для асинхронной обработки вызовов (это не сожжет поток):

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

Затем вы можете просто использовать Result свойство в экземплярах Task<T>, чтобы получить пару URL-адреса и содержимого:

// 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 голосов
/ 19 сентября 2011

У меня был похожий случай, и вот как я решил

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, потому что медленная скорость заключается в том, что ваше программное обеспечение ожидает ввода-вывода, и почему нет, пока поток, ожидающий ввода-вывода, еще один не запускается.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...