Оптимизация парсинга больших страниц c# httpwebrequest

Как обычно скажу что возможно подобные темы есть. Но я и не нашел, либо гуглил плохо. Все что нашел испробовал, но мне нужно конкретные подсказки или помощь именно для меня.

Мне нужно оптимизировать вывод страницы, её обработку (например найти слова и записать количество или взять данные со страницы). Но страница очень большая скрин: введите сюда описание изображения

Я не знаю, возможно для кого то она маленькая, я её обрабатываю за 2 секунды, бывает за 5, или за 10. Но можно ли быстрее? И вообще оптимизировать вывод, и обработку. В дальнейшим я хочу сделать многопоточный режим, т.е парсить несколько сайтов. Но они так-же большие. И не могу понять как мне быстро обрабатывать страницу, или как то получать более малый ответ но данных будет меньше и не смогу все спарсить.

Сейчас у меня есть List<string> конкретных слов, я проверяю количество этих слов на данной странице. И как я понимаю такая страница у меня хранится в памяти программы если я записываю её в string.

Вот код: Консольное приложение. Главный класс Program вызывает конструктор ServiesCheck который принимает настройки (В виде коллекции классов 1 класс - настройка).

new ServiesCheck(settingslist);

Конструктор ServiesCheck перебирает настройки (ссылки) и вызывает метод В конструкторе вызываю метод: ControlSettingsServies()

    private bool ControllSettingsServies(ServiesSettings setting, CookieContainer containerCookies)
        {

            if (setting.oneChecked)
            {
                var onecheckerservies = OneCheckServies(setting.settingsCheck, containerCookies, setting.linkServis);
                if (onecheckerservies.isValid)
                {
                    Console.WriteLine("Нашел");
                    return true;
                }
                else
                {
                    Console.WriteLine("Не нашел");
                    return false;
                }
            }
            return false;
        }

В методе вызываю OneCheckServies(принимает мои заготовленные куки, настройки по которым я буду обрабатывать вывод, и ссылку по которой нужно перейти)

     private (bool isValid, int[] countSearch, string ResHtml) OneCheckServies(ServiesSettingCheck settingsCheck, CookieContainer containercookies, string link)s
        {
            var startTime = System.Diagnostics.Stopwatch.StartNew();

            HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(link);
            request.CookieContainer = containercookies;
            request.AutomaticDecompression = DecompressionMethods.GZip;
            //request.Proxy = null;

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            String setCookieHeader = response.Headers[HttpResponseHeader.SetCookie];

            bool isSerach = false;
            string responceTextHtml = "";
            int[] countSerchList = new int[settingsCheck.worldSerach.Count];

            using (Stream stream = response.GetResponseStream())
            {
                using (StreamReader reader = new StreamReader(stream))
                {
                    string line = "";
                    while ((line = reader.ReadLine()) != null) {
                        responceTextHtml += line + "\n";
                        foreach (string worldS in settingsCheck.worldSerach) {
                            if (countSerchList[settingsCheck.worldSerach.IndexOf(worldS)] <= settingsCheck.closeFoundNum) {
                                if (line.Contains(worldS)) {
                                    countSerchList[settingsCheck.worldSerach.IndexOf(worldS)] += 1;
                                }
                            }
                        }
                    }
                }
            }
            for (int i = 0; i < countSerchList.Length; i++) {
                if (countSerchList[i] > settingsCheck.closeFoundNum) {
                    isSerach = true;
                }else {
                    isSerach = false;
                    break;
                }
            }

            startTime.Stop();
            var resultTime = startTime.Elapsed;
            Console.WriteLine(String.Format("{0:00}:{1:00}:{2:00}.{3:000}",
                resultTime.Hours,
                resultTime.Minutes,
                resultTime.Seconds,
                resultTime.Milliseconds));

            var result = (isSerach, countSerchList, responceTextHtml);
            response.Close();
            return result;
        }

Код возможно для вас ужасный но по другому не могу, и не знаю.

  1. Как оптимизировать вывод?
  2. Как оптимизировать поиск?
  3. И оптимизация памяти если нужно. Или это придельная скорость (от 2 до 10 секунд)?

В дальнейшем мне нужно использовать эту найденную страницу для дальнейших манипуляций.


Ответы (1 шт):

Автор решения: aepot

Чтобы быстро всё работало, надо использовать хорошо оптимизированный .NET 6 и HttpClient вместо давно устаревшего HttpWebRequest.

Напишу класс парсера на основе HtmlAgilityPack. Смысл парсера будет в том, чтобы посчитать количество слов на странице по ссылке и вывести результаты подсчета в консоль.

class HttpParser : IDisposable
{
    private readonly HttpClient client;
    private readonly char[] splitChars = new char[] { ' ', '\t', '\r', '\n' };

    public HttpParser(CookieContainer cookies = null, WebProxy proxy = null)
    {
        var handler = new HttpClientHandler
        {
            CookieContainer = cookies ?? new CookieContainer(),
            AutomaticDecompression = DecompressionMethods.All,
            Proxy = proxy
        };
        client = new HttpClient(handler)
        {
            DefaultRequestVersion = HttpVersion.Version20
        };
        client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:100.0) Gecko/20100101 Firefox/100.0"); // пример добавления HTTP заголовка по умолчанию
    }
    public async Task<Dictionary<string, int>> ParseLink(string url, int minLength = 1, int maxLength = 30)
    {
        string html = await client.GetStringAsync(url);
        HtmlDocument doc = new HtmlDocument();
        doc.LoadHtml(html);
        Dictionary<string, int> words = new Dictionary<string, int>();
        // Descendants - полный список нод документа в виде единого списка
        // из них выбираю только текстовые ноды
        // и зачищаю каждую от эскейп-кодов и HTML-Entity, меня вполне устраивает чистый юникод
        foreach (string nodeText in doc.DocumentNode
            .Descendants()
            .Where(n => n.NodeType == HtmlNodeType.Text)
            .Select(n => HtmlEntity.DeEntitize(Regex.Unescape(n.InnerText))))
        {
            // бью текст на слова по пустым местам, можно еще Regex.Split использовать для этого
            // далее фильтрую все что не попадает под мои условия выбора
            foreach (string word in nodeText
                .Split(splitChars, StringSplitOptions.RemoveEmptyEntries)
                .Select(text => text.Trim().ToLower())
                .Where(t => t.Length >= minLength && t.Length <= maxLength))
            {
                // заношу в словарь
                words[word] = words.TryGetValue(word, out int count) ? count + 1 : 1;
            }
        }
        return words;
    }

    public void Dispose()
    {
        client.Dispose();
    }
}

И вот так использую

static async Task Main(string[] args)
{
    using HttpParser parser = new HttpParser();
    var sw = Stopwatch.StartNew();
    var words = await parser.ParseLink("https://ru.wikipedia.org/wiki/HTTP", 5);
    Console.WriteLine(sw.Elapsed.ToString(@"hh\:mm\:ss\.fff"));
    foreach (var pair in words.OrderByDescending(x => x.Value).Take(20)) // возьму топ-20 слов, чтобы вывод не был огромным
    {
        Console.WriteLine($"[{pair.Key}] = {pair.Value}");
    }
}

Вес страницы 278 килобайт, по данным браузера она загружается за 0.335 секунды.

И вот что у меня получилось

00:00:00.387
[править] = 70
[может] = 31
[сервер] = 31
[сообщения] = 22
[только] = 21
[заголовки] = 19
[заголовок] = 19
[ответа] = 19
[также] = 18
[сервера] = 18
[ответ] = 18
[англ.] = 17
[содержимое] = 17
[клиент] = 17
[запрос] = 17
[http/1.1] = 16
[могут] = 15
[запроса] = 15
[используется] = 14
[версия] = 13

То есть вся работа по парсингу заняла, грубо говоря, ~50мс. Ну ваша страница намного больше, но я полагаю, что должно все равно меньше, чем за полсекунды отработать.

Из получившегося словаря уже можете выбрать те слова, которые вам надо. Можете конечно модифицировать сам метод и выфильтровывать ненужное еще на этапе выборки. Я просто методику хотел показать.

Можно конечно для получения только текста из документа doc.DocumentNode.InnerText использовать, но тогда в выборку не попадут всякие динамические объекты, текст которых может прятаться где-то внутри JavaScript блоков или CSS стилей. еще обработка документа по частям позволяет не создавать больших объектов в оперативе, вследствие чего ее экономить. Но вы можете попробовать убрать внешний foreach и заменить nodeText на doc.DocumentNode.InnerText

Кстати, давайте попробуем

public async Task<Dictionary<string, int>> ParseLink(string url, int minLength = 1, int maxLength = 30)
{
    string html = await client.GetStringAsync(url);
    HtmlDocument doc = new HtmlDocument();
    doc.LoadHtml(html);
    Dictionary<string, int> words = new Dictionary<string, int>();

    foreach (string word in HtmlEntity.DeEntitize(Regex.Unescape(doc.DocumentNode.InnerText))
        .Split(splitChars, StringSplitOptions.RemoveEmptyEntries)
        .Select(text => text.Trim().ToLower())
        .Where(t => t.Length >= minLength && t.Length <= maxLength))
    {
        words[word] = words.TryGetValue(word, out int count) ? count + 1 : 1;
    }
    return words;
}

Вывод в консоль

00:00:00.394
[править] = 35
[сервер] = 31
[может] = 30
[сообщения] = 21
[только] = 21
[заголовок] = 19
[ответ] = 18
[заголовки] = 17
[также] = 17
[клиент] = 17
[сервера] = 17
[ответа] = 17
[запрос] = 17
[содержимое] = 16
[могут] = 15
[запроса] = 15
[используется] = 14
[http/1.1] = 14
[основные] = 13
[метод] = 13

Немного другие числа, но вы сами решайте, что для вас больше подходит. Мне логически последний вариант больше нравится, да и попроще он.

По поводу создания экземпляров HttpParser - на каждый набор кук и на каждую прокси придется создавать свой парсер. Не забывайте созданные экземпляры диспозить, когда они больше не нужны.

Для работы без прокси и без кук на все запросы хватит одного парсера, даже если вы хотите выполнять их многопоточно. Кстати, про многопоточку с асинхронностью можете почитать здесь.

→ Ссылка