Что такое HTML Agility Pack и как его использовать
HTML Agility Pack — это инструмент для чтения, написания и обновления HTML-документов . Он обычно используется для веб-скрапинга, который представляет собой процесс программного извлечения информации с общедоступных веб-сайтов .
Чтобы начать использовать HTML Agility Pack, мы можем установить его с помощью диспетчера пакетов NuGet:
Install-Package HtmlAgilityPackПосле этого мы можем легко проанализировать строку HTML:
var html = @"<!DOCTYPE html>
<html>
<body>
<h1>Learn To Code in C#</h1>
<p>Programming is really <i>easy</i>.</p>
</body>
</html>";
var dom = new HtmlDocument();
dom.LoadHtml(html);
var documentHeader = dom.DocumentNode.SelectSingleNode("//h1");
Assert.Equal("Learn To Code in C#", documentHeader.InnerHtml);Здесь мы анализируем строку, содержащую некоторый базовый HTML, чтобы получить HtmlDocumentобъект.
Объект HtmlDocumentпредоставляет DocumentNodeсвойство, представляющее корневой тег фрагмента. Мы используем SelectSingleNode()его для запроса модели документа в поисках h1тега внутри документа. И, наконец, мы получаем доступ к текстовому содержимому h1тега через InnerHtmlсвойство.
Разбор HTML с помощью HTML Agility Pack
Хотя синтаксический анализ HTML-документов из строк прост, иногда нам нужно получить наш HTML-код из других источников.
Разбор HTML из локального файла
Мы можем легко загрузить HTML из файлов, расположенных на локальном жестком диске . Чтобы продемонстрировать это, давайте сначала создадим файл HTML и сохраним его с именем test.html:
<!DOCTYPE html>
<html>
<body>
<h1>Learn To Code in C#</h1>
<p>Programming is really <i>easy</i>.</p>
<h2>HTML Agility Pack</h2>
<p id='second'>HTML Agility Pack is a popular web scraping tool.</p>
<p>Features:</p>
<ul>
<li>Parser</li>
<li>Selectors</li>
<li>DOM management</li>
</ul>
</body>
</html>Затем мы можем создать экземпляр нового HtmlDocumentобъекта и использовать его Load()метод для анализа содержимого нашего HTML-файла:
var path = @"test.html";
var doc = new HtmlDocument();
doc.Load(path);
var htmlHeader = doc.DocumentNode.SelectSingleNode("//h2");
Assert.Equal("HTML Agility Pack", htmlHeader.InnerHtml);После загрузки мы можем запросить содержимое документа с помощью DocumentNode.SelectSingleNode()метода. В этом случае мы извлекаем текст заголовка второго уровня через InnerHtmlтег h2в документе.
Парсинг HTML из Интернета
Допустим, наша цель — получить HTML-код с общедоступного веб-сайта. Чтобы анализировать контент прямо из URL-адреса, нам нужно использовать экземпляр HtmlWebкласса вместо HtmlDocument:
var url = @"https://code-maze.com/";
HtmlWeb web = new HtmlWeb();
var htmlDoc = web.Load(url);
var node = htmlDoc.DocumentNode.SelectSingleNode("//head/title");
Assert.Equal("Code Maze - C#, .NET and Web Development Tutorials", node.InnerHtml);Как только мы проанализируем содержимое, вызвав Load()метод HtmlWebэкземпляра с URL-адресом сайта, мы сможем использовать уже известные нам методы для доступа к содержимому. В этом случае мы выбираем titleтег внутри headраздела документа.
Разбор HTML из браузера с помощью Selenium
Часто веб-сайты используют клиентский код, такой как javascript, для динамического отображения элементов HTML. Это может быть проблемой, когда мы пытаемся проанализировать HTML с удаленного веб-сайта, в результате чего содержимое становится недоступным для нашей программы, поскольку клиентский код не был выполнен.
Если нам нужно проанализировать динамически отображаемый HTML-контент, мы можем использовать инструмент автоматизации браузера, такой как Selenium WebDriver . Это работает, потому что мы будем использовать реальный браузер для получения HTML-страницы. Настоящий браузер, такой как Chrome, способен выполнять любой клиентский код, присутствующий на странице, таким образом генерируя весь динамический контент.
Мы можем легко найти ресурсы, чтобы узнать, как работать с Selenium WebDriver для загрузки удаленного веб-сайта. После этого мы можем использовать содержимое, загруженное в PageSourceсвойство драйвера:
var options = new ChromeOptions();
options.AddArguments("headless");
using (var driver = new ChromeDriver(options))
{
driver.Navigate().GoToUrl("https://code-maze.com/");
var doc = new HtmlDocument();
doc.LoadHtml(driver.PageSource);
var node = doc.DocumentNode.SelectSingleNode("//head/title");
Assert.Equal("Code Maze - C#, .NET and Web Development Tutorials", node.InnerHtml);
}Структура HtmlDocument
Внутри HtmlDocument экземпляра есть дерево HtmlNode элементов с одним корневым узлом . Доступ к корневому узлу можно получить через DocumentNodeсвойство.
У каждого узла есть Nameсвойство, которое будет соответствовать HTML-тегу, представляющему, например body, или h2. С другой стороны, элементы, не являющиеся тегами HTML, также имеют узлы, имена которых начинаются с #. Примеры этого #document, #commentили #text:
Каждый HtmlNodeпредоставляет методы SelectSingleNode()и SelectNodes()для запроса всего дерева с использованием выражений XPath.
SelectSingleNode()вернет первый HtmlNode, который соответствует выражению XPath, вместе со всеми его потомками, а если соответствующих узлов нет, он вернет null.
SelectNodes()вернет HtmlNodeCollectionобъект, содержащий все узлы, которые соответствуют выражению XPath с его потомками.
Мы часто будем использовать HtmlNodeсвойства InnerHtmlи InnerText,для OuterHtmlдоступа к содержимому узла.
Наконец, мы можем получить доступ к соседним узлам , среди прочего, с помощью свойств ChildNodes, FirstChild, и .ParentNode
Использование селекторов
Применяя все это на практике, мы можем выбрать все узлы с определенным именем независимо от их положения в дереве документа, используя //:
var doc = new HtmlDocument();
doc.Load("test.html");
var nodes = doc.DocumentNode.SelectNodes("//li");
Assert.Equal("Parser", nodes[0].InnerHtml);
Assert.Equal("Selectors", nodes[1].InnerHtml);
Assert.Equal("DOM Management", nodes[2].InnerHtml);Здесь мы выбираем все liэлементы в файле HTML, который мы использовали в предыдущем примере, без указания точного пути к элементам.
В качестве альтернативы мы можем использовать выражение для выбора узла, явно определяя его позицию в иерархии, используя /:
var node = doc.DocumentNode.SelectSingleNode("/html/body/h2");
Assert.Equal("HTML Agility Pack", node.InnerHtml);Чтобы выбрать узлы относительно текущего узла, мы можем использовать .выражение с точкой ( ):
var body = dom.DocumentNode.SelectSingleNode("/html/body");
var listItems = body.SelectNodes("./ul/li");
Assert.Equal(3, listItems.Count);Селекторы атрибутов
Мы также можем выбирать узлы на основе их атрибутов , таких как classили даже id. Это делается с помощью синтаксиса квадратных скобок:
var node = dom.DocumentNode.SelectSingleNode("//p[@id='second']");
Assert.Equal("HTML Agility Pack is a popular web scraping tool.", node.InnerHtml);Коллекции
Выражения XPath могут выбирать определенные элементы в коллекции по индексу, начинающемуся с нуля, или с помощью таких функций, как first()или last():
var secondParagraph = dom.DocumentNode.SelectSingleNode("//p[1]");
var lastParagraph = dom.DocumentNode.SelectSingleNode("//p[last()]");
Assert.Equal("Programming is really <i>easy</i>.", secondParagraph.InnerHtml);
Assert.Equal("Features:", lastParagraph.InnerHtml);HTML-манипуляция
Когда у нас есть HtmlDocumentобъект, мы можем изменить структуру базового HTML , используя набор методов, которые работают с узлами документа. Мы можем манипулировать документом, добавляя и удаляя узлы, а также изменяя их содержимое или даже их атрибуты :
var dom = new HtmlDocument();
dom.Load("test.html");
var list = dom.DocumentNode.SelectSingleNode("//ul");
list.ChildNodes.Add(HtmlNode.CreateNode("<li>Added dynamically</li>"));
Assert.Equal(@"<ul>
<li>Parser</li>
<li>Selectors</li>
<li>DOM management</li>
<li>Added dynamically</li></ul>", list.OuterHtml);Здесь мы выбираем узел в нашем HtmlDocumentсоответствующем неупорядоченном списке ul, который изначально содержит три элемента списка. Затем мы добавляем вновь созданное HtmlNodeв ChildNodesколлекцию свойство выбранного узла. После этого мы можем проверить OuterHtmlсвойство ulузла и посмотреть, как новый узел элемента списка был добавлен в документ.
Точно так же мы можем удалить узлы HTML из документа :
var list = dom.DocumentNode.SelectSingleNode("//ul");
list.RemoveChild(list.SelectNodes("li").First());
Assert.Equal(@"<ul>
<li>Selectors</li>
<li>DOM management</li>
</ul>", list.OuterHtml);В этом случае, начиная с того же ненумерованного списка, мы удаляем первый элемент списка, вызывая RemoveChild()метод в выбранном ранее HtmlNode.
Точно так же мы можем изменить существующие узлы , используя свойства, предоставляемые HtmlNodeобъектом:
var list = dom.DocumentNode.SelectSingleNode("//ul");
foreach (var node in list.ChildNodes.Where(x => x.Name == "li"))
{
node.FirstChild.InnerHtml = "List Item Text";
node.Attributes.Append("class", "list-item");
}
Assert.Equal(@"<ul>
<li class=""list-item"">List Item Text</li>
<li class=""list-item"">List Item Text</li>
<li class=""list-item"">List Item Text</li>
</ul>", list.OuterHtml);Начиная с того же неупорядоченного списка, мы заменяем внутренний текст в каждом из элементов списка и добавляем classатрибут, используя Attributes.Append().
Написание HTML
Часто нам нужно записать HTML в файл после работы с ним. Для этого мы можем использовать Save()метод HtmlDocumentкласса. Этот метод сохранит все узлы в документе в файл, включая все изменения, которые мы могли сделать с помощью API манипуляции :
var dom = new HtmlDocument();
dom.Load("test.html");
using var textWriter = File.CreateText("test_out.html");
dom.Save(textWriter);Не менее важно записывать только часть документа , обычно узлы под определенным известным узлом. Класс HtmlNodeпредоставляет WriteTo()метод, который записывает текущий узел вместе со всеми его потомками, и WriteContentTo()метод, который выводит только его дочерние элементы:
using (var textWriter = File.CreateText("list.html"))
{
list.WriteTo(textWriter);
}
using (var textWriter = File.CreateText("items_only.html"))
{
list.WriteContentTo(textWriter);
}
Assert.Equal(
@"<ul>
<li>Parser</li>
<li>Selectors</li>
<li>DOM management</li>
</ul>", File.ReadAllText("list.html"));
Assert.Equal(
@"
<li>Parser</li>
<li>Selectors</li>
<li>DOM management</li>
", File.ReadAllText("items_only.html"));Обход DOM
Есть несколько свойств и методов, которые позволяют нам удобно перемещаться по дереву узлов, из которых состоит документ.
HtmlNodeсвойства ParentNode, ChildNodes, NextSiblingи другие позволяют нам получить доступ к соседним узлам в иерархии документа. Мы можем использовать эти свойства для обхода дерева узлов по одному узлу за раз . Для оптимального обхода всего документа может быть хорошей идеей использовать рекурсию :
var toc = new List<HtmlNode>();
var headerTags = new string[] { "h1", "h2", "h3", "h4", "h5", "h6" };
void VisitNodesRecursively(HtmlNode node)
{
if (headerTags.Contains(node.Name))
toc.Add(node);
foreach(var child in node.ChildNodes)
VisitNodesRecursively(child);
}
VisitNodesRecursively(dom.DocumentNode);
// extracted nodes:
// h1 -> Learn To Code in C#
// h2 --> HTML Agility PackЗдесь мы просматриваем все узлы в порядке документа и сохраняем все заголовки, которые мы находим по пути, в tocколлекции, чтобы построить оглавление для документа. Мы используем это ChildNodesсвойство для рекурсивной обработки всех узлов.
С другой стороны, такие методы, как Descendants(), DescendantsAndSelf(), Ancestors()и AncestorsAndSelf() возвращают плоский список узлов относительно узла, для которого мы вызываем метод :
var groups = dom.DocumentNode.DescendantsAndSelf()
.Where(n => !n.Name.StartsWith("#"))
.GroupBy(n => n.Name);
foreach (var group in groups)
Console.WriteLine($"Tag '{group.Key}' found {group.Count()} times.");Здесь мы получаем всех потомков корневого узла и группируем их по имени тега. Наконец, мы подсчитываем количество вхождений каждого тега, используемого в документе. Если мы применим это к примеру HTML, который мы использовали ранее, вывод должен выглядеть так:
Tag 'html' found 1 times.
Tag 'body' found 1 times.
Tag 'h1' found 1 times.
Tag 'p' found 3 times.
Tag 'i' found 1 times.
Tag 'h2' found 1 times.
Tag 'ul' found 1 times.
Tag 'li' found 3 times.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.