Гайд по HtmlAgilityPack
HtmlAgilityPack (HAP) — это самая популярная и мощная библиотека для парсинга HTML в .NET. Ее главное преимущество в том, что она умеет работать с «битым» HTML (который не является валидным XML), автоматически исправляя ошибки структуры на лету.
Ниже представлен максимально подробный гайд по установке, настройке, парсингу и справочник по основным функциям и методам библиотеки.
Часть 1. Установка и базовая настройка
1. Установка
Откройте консоль NuGet в вашей IDE (Visual Studio / Rider) или терминал и выполните команду:
dotnet add package HtmlAgilityPackИли через Package Manager Console в Visual Studio:
Install-Package HtmlAgilityPack2. Подключение
В начале вашего 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.
Резюме (Шпаргалка)
- Скачиваем HTML через
HttpClient. - Грузим в
HtmlDocument.LoadHtml(). - Ищем узлы через
SelectNodes("//xpath")или.Descendants("tag"). - Достаем данные через
.InnerText,.GetAttributeValue(). - Не забываем проверять на
null(еслиSelectNodesничего не нашел). - Используем точку
.//в XPath для поиска внутри конкретного узла.