Что такое HTML Agility Pack и как его использовать

  • Михаил
  • 12 мин. на прочтение
  • 128
  • 21 Dec 2016
  • 21 Dec 2016

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.