Гайд по HtmlAgilityPack

  • Михаил
  • 0 мин. на прочтение
  • 26
  • 25 Jun 2026
  • 25 Jun 2026

HtmlAgilityPack (HAP) — это самая популярная и мощная библиотека для парсинга HTML в .NET. Ее главное преимущество в том, что она умеет работать с «битым» HTML (который не является валидным XML), автоматически исправляя ошибки структуры на лету.

Ниже представлен максимально подробный гайд по установке, настройке, парсингу и справочник по основным функциям и методам библиотеки.


Часть 1. Установка и базовая настройка

1. Установка

Откройте консоль NuGet в вашей IDE (Visual Studio / Rider) или терминал и выполните команду:

dotnet add package HtmlAgilityPack

Или через Package Manager Console в Visual Studio:

Install-Package HtmlAgilityPack

2. Подключение

В начале вашего C# файла добавьте пространство имен:

using HtmlAgilityPack;

Часть 2. Загрузка HTML (Как «построить» DOM-дерево)

HtmlAgilityPack не умеет сам скачивать HTML из интернета (в современных версиях класс HtmlWeb считается устаревшим и не поддерживает полноценный async/await). Правильный паттерн: скачиваем через HttpClient, парсим через HtmlDocument.

using System.Net.Http;
using HtmlAgilityPack;

// 1. Создаем документ
var doc = new HtmlDocument();

// 2. Вариант А: Загрузка из строки
string html = "<html><body><h1>Hello</h1></body></html>";
doc.LoadHtml(html);

// 3. Вариант Б: Загрузка из файла
doc.Load("page.html");

// 4. Вариант В: Загрузка из сети (Рекомендуемый способ)
using var client = new HttpClient();
// Обязательно указываем User-Agent, иначе многие сайты отдадут 403
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); 
string downloadedHtml = await client.GetStringAsync("https://example.com");
doc.LoadHtml(downloadedHtml);

Часть 3. Основные способы парсинга (Поиск узлов)

В HAP есть два главных инструмента для поиска данных: XPath и LINQ.

Способ 1: XPath (Самый мощный и частый)

XPath — это язык запросов для XML/HTML. HAP поддерживает его почти полностью.

HtmlNode root = doc.DocumentNode; // Корневой узел документа

// Найти ОДИН узел (первый попавшийся)
HtmlNode title = root.SelectSingleNode("//h1"); 

// Найти КОЛЛЕКЦИЮ узлов
HtmlNodeCollection links = root.SelectNodes("//a"); 

// Примеры XPath запросов:
root.SelectNodes("//div[@class='product-card']"); // Все div с классом product-card
root.SelectNodes("//a[@href]");                  // Все ссылки, у которых есть атрибут href
root.SelectNodes("//div[@id='content']/p");      // Все <p> внутри <div id="content">
root.SelectNodes("//a[contains(@href, 'google')]"); // Ссылки, содержащие 'google' в href
root.SelectNodes("//span[text()='Цена']");       // Span, где текст в точности равен 'Цена'

Способ 2: LINQ (Ближе к C#)

Если вы не знаете XPath, можно использовать методы расширения LINQ.

// Найти все элементы <div>
var divs = doc.DocumentNode.Descendants("div");

// Найти все элементы <a> и отфильтровать
var links = doc.DocumentNode.Descendants("a")
    .Where(a => a.GetAttributeValue("href", "").StartsWith("http"))
    .ToList();

// Найти элемент по ID (аналог getElementById)
var myDiv = doc.GetElementbyId("main-content"); 

Часть 4. Извлечение данных из найденных узлов

Когда вы нашли HtmlNode, вам нужно достать из него данные.

HtmlNode node = doc.DocumentNode.SelectSingleNode("//div[@class='item']");

// 1. Текст внутри тега (без HTML-тегов)
string text = node.InnerText; // "Цена: 100 руб."

// 2. Внутренний HTML (с тегами)
string innerHtml = node.InnerHtml; // "<b>Цена:</b> 100 руб."

// 3. Внешний HTML (сам тег + его содержимое)
string outerHtml = node.OuterHtml; // "<div class='item'><b>Цена:</b> 100 руб.</div>"

// 4. Имя тега
string tagName = node.Name; // "div"

// 5. Получение атрибутов (БЕЗОПАСНЫЙ способ)
// Если атрибута нет, вернется "default_value" вместо NullReferenceException
string href = node.GetAttributeValue("href", "default_value"); 
string cssClass = node.GetAttributeValue("class", "");

// 6. Прямое обращение к атрибуту (НЕБЕЗОПАСНО, может упасть с ошибкой)
string id = node.Attributes["id"].Value; 

Часть 5. Подробный справочник API (Все важные функции)

Класс HtmlDocument

Отвечает за весь документ в целом.

  • DocumentNode (свойство) — Возвращает корневой HtmlNode (обычно это <html>).
  • LoadHtml(string html) — Загружает HTML из строки.
  • Load(string path) — Загружает HTML из файла.
  • Load(Stream stream) — Загружает HTML из потока.
  • GetElementbyId(string id) — Быстрый поиск узла по ID.
  • OptionFixNestedTags (bool) — Важно! Если true, HAP будет пытаться исправить вложенность тегов (например, переместит <p> из <pre>, если это нарушает стандарт).
  • OptionAutoCloseOnEnd (bool) — Автоматически закрывает открытые теги в конце документа.
  • ParseErrors — Коллекция ошибок парсинга (если HTML был сильно битым).

Класс HtmlNode

Отвечает за конкретный тег, текст или комментарий. Свойства:

  • Name — Имя тега (например, "div", "a").
  • NodeType — Тип узла (Element, Text, Comment).
  • InnerText — Текстовое содержимое.
  • InnerHtml — HTML содержимое.
  • OuterHtml — Полный HTML узла.
  • Attributes — Коллекция атрибутов (HtmlAttributeCollection).
  • ParentNode — Родительский узел.
  • ChildNodes — Дочерние узлы.
  • FirstChild / LastChild — Первый и последний дочерний узел.
  • NextSibling / PreviousSibling — Соседние узлы на том же уровне.

Методы поиска:

  • SelectNodes(string xpath) — Возвращает HtmlNodeCollection (или null, если ничего не найдено!).
  • SelectSingleNode(string xpath) — Возвращает один HtmlNode (или null).
  • Descendants() / Descendants(string name) — Возвращает всех потомков (рекурсивно).
  • Elements() / Elements(string name) — Возвращает только прямых детей.
  • DescendantsAndSelf() — Потомки + сам узел.

Методы работы с атрибутами:

  • GetAttributeValue(string name, string def) — Безопасное получение значения атрибута.
  • SetAttributeValue(string name, string value) — Установить или изменить атрибут.
  • RemoveAttribute(string name) — Удалить атрибут.

Методы изменения DOM (если нужно модифицировать HTML):

  • AppendChild(HtmlNode newChild) — Добавить дочерний узел.
  • Remove() — Удалить узел из дерева.
  • RemoveAll() — Удалить все дочерние узлы и атрибуты.
  • Clone() — Создать копию узла.
  • CreateElement(string name) / CreateText(string text)Статические методы для создания новых узлов.

Класс HtmlNodeCollection

Коллекция узлов, возвращаемая SelectNodes.

  • Реализует IEnumerable<HtmlNode>, поэтому можно использовать foreach и LINQ.
  • this[int index] — Доступ по индексу.
  • this[string name] — Доступ по имени тега.
  • Count — Количество элементов.

Часть 6. Практический пример: Парсинг интернет-магазина

Допустим, у нас есть такой HTML:

<html>
<body>
    <div class="catalog">
        <div class="product" data-id="101">
            <h2 class="title">Смартфон <span>Pro</span></h2>
            <p class="price">50000 руб.</p>
            <a href="/buy/101" class="btn">Купить</a>
        </div>
        <div class="product" data-id="102">
            <h2 class="title">Ноутбук</h2>
            <p class="price">80000 руб.</p>
            <a href="/buy/102" class="btn">Купить</a>
        </div>
    </div>
</body>
</html>

Код парсера на C#:

using System;
using System.Collections.Generic;
using System.Linq;
using HtmlAgilityPack;

public class Product
{
    public string Id { get; set; }
    public string Title { get; set; }
    public decimal Price { get; set; }
    public string Url { get; set; }
}

public class Parser
{
    public List<Product> ParseProducts(string html)
    {
        var doc = new HtmlDocument();
        doc.LoadHtml(html);
        
        var products = new List<Product>();

        // 1. Находим все карточки товаров
        // Используем XPath: ищем div с классом product
        var productNodes = doc.DocumentNode.SelectNodes("//div[@class='product']");

        // Проверка на null обязательна! Если товаров нет, SelectNodes вернет null
        if (productNodes == null) return products; 

        // 2. Перебираем каждый товар
        foreach (var node in productNodes)
        {
            var product = new Product();

            // Достаем ID из data-id
            product.Id = node.GetAttributeValue("data-id", "unknown");

            // Достаем Заголовок (внутри h2 есть span, InnerText возьмет весь текст: "Смартфон Pro")
            var titleNode = node.SelectSingleNode(".//h2[@class='title']"); 
            // ОБРАТИТЕ ВНИМАНИЕ: Точка в начале XPath (.//) означает поиск ТОЛЬКО внутри текущего узла (node), а не во всем документе!
            product.Title = titleNode?.InnerText.Trim() ?? "Без названия";

            // Достаем Цену и чистим от лишних символов
            var priceNode = node.SelectSingleNode(".//p[@class='price']");
            string rawPrice = priceNode?.InnerText ?? "0";
            // Убираем "руб." и пробелы, парсим в decimal
            product.Price = decimal.Parse(new string(rawPrice.Where(char.IsDigit).ToArray()));

            // Достаем Ссылку
            var linkNode = node.SelectSingleNode(".//a[@class='btn']");
            product.Url = linkNode?.GetAttributeValue("href", "#");

            products.Add(product);
        }

        return products;
    }
}

Часть 7. Продвинутые фишки и Best Practices (Важно!)

1. Относительный vs Абсолютный XPath

Если вы используете SelectSingleNode на уже найденном узле, обязательно ставьте точку в начале XPath, иначе поиск пойдет от корня документа!

var card = doc.SelectSingleNode("//div[@class='card']");
// ПЛОХО: найдет первую ссылку во ВЕСЬМ документе, а не в карточке
var badLink = card.SelectSingleNode("//a"); 

// ХОРОШО: найдет ссылку именно внутри этой карточки
var goodLink = card.SelectSingleNode(".//a"); 

2. Проблема с кодировками (Кракозябры)

Если вы грузите HTML из файла или потока, HAP может не угадать кодировку (особенно Windows-1251).

var doc = new HtmlDocument();
// Явно указываем кодировку при загрузке из файла
doc.Load("page.html", System.Text.Encoding.GetEncoding(1251)); 

// Или при загрузке из потока
doc.Load(stream, Encoding.UTF8);

3. Очистка текста (Удаление лишних пробелов и переносов)

HTML часто содержит кучу \n, \t и лишних пробелов. InnerText их сохраняет.

string dirtyText = node.InnerText;
// Чистим с помощью Regex или LINQ
string cleanText = System.Text.RegularExpressions.Regex.Replace(dirtyText, @"\s+", " ").Trim();

4. Парсинг таблиц

Таблицы парсятся очень легко через вложенные циклы или LINQ:

var rows = doc.DocumentNode.SelectNodes("//table[@id='myTable']//tr");
foreach(var row in rows)
{
    var cells = row.SelectNodes("./td | ./th"); // td ИЛИ th
    if(cells != null) {
        foreach(var cell in cells) {
            Console.Write(cell.InnerText + " | ");
        }
        Console.WriteLine();
    }
}

5. Асинхронность

HtmlAgilityPack синхронный. Методов LoadHtmlAsync не существует. Если вы парсите огромный HTML файл и боитесь заблокировать UI или ThreadPool, оберните парсинг в Task.Run:

var products = await Task.Run(() => parser.ParseProducts(hugeHtmlString));

Само скачивание через HttpClient.GetStringAsync() уже асинхронное, блокируется только момент разбора строки в DOM-дерево.

6. Работа с JavaScript (SPA сайты)

HtmlAgilityPack не выполняет JavaScript! Он парсит только тот HTML, который пришел в ответ на HTTP-запрос. Если сайт написан на React/Vue/Angular, и данные подгружаются динамически, HAP их не увидит. Решение: Либо искать скрытые API endpoints (F12 -> Network -> XHR/Fetch), либо использовать браузерные эмуляторы (Selenium, Playwright, PuppeteerSharp), а уже полученный HTML отдавать в HAP.


Резюме (Шпаргалка)

  1. Скачиваем HTML через HttpClient.
  2. Грузим в HtmlDocument.LoadHtml().
  3. Ищем узлы через SelectNodes("//xpath") или .Descendants("tag").
  4. Достаем данные через .InnerText, .GetAttributeValue().
  5. Не забываем проверять на null (если SelectNodes ничего не нашел).
  6. Используем точку .// в XPath для поиска внутри конкретного узла.