Что такое 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.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.