- PVSM.RU - https://www.pvsm.ru -

Распарсить HTML в .NET и выжить: анализ и сравнение библиотек

Распарсить HTML в .NET и выжить: анализ и сравнение библиотек - 1
В ходе работы над одним домашним проектом, столкнулся с необходимостью парсинга HTML. Поиск по гуглу выдал комменарий [1] Athari [2] и его микро-обзор актуальных парсеров HTML в .NET за что ему огромное спасибо.

К сожалению, никаких цифр и/или аргументов в пользу того или иного парсера найдено не было, что послужило поводом к написанию данной статьи.

Сегодня я протестирую популярные, на данный момент, библиотеки для работы с HTML, а именно: AngleSharp [3], CsQuery [4], Fizzler [5], HtmlAgilityPack [6] и, конечно же, Regex-way [7]. Сравню их по скорости работы и удобству использования.

TL;DR: Код всех бенчмарков можно найти на github [8]. Там же лежат результаты тестирования. Самым актуальным парсером на данный момент является AngleSharp [3] — удобный, быстрый, молодежный парсер с удобным API.

Тем, кому интересен подробный обзор — добро пожаловать под кат.

Содержание

Описание библиотек

В данном разделе будут краткие описания рассматриваемых библиотек, описание лицензий и тд.

HtmlAgilityPack [19]

Один из самых (если не самый) известный парсер HTML в мире .NET. Про него написано немало статей как на русском, так и на английском языках, к примеру на habrahabr [20].
Вкратце это быстрая, относительно удобная библиотека для работы с HTML (если XPath запросы будут несложными). Репозиторий [19] давно не обновляется.
Лицензия MS-PL.

Парсер будет удобным если задача типична и хорошо описывается XPath выражением, к примеру, чтобы получить все ссылки со страницы, нам понадобится совсем немного кода:

/// <summary>
/// Extract all anchor tags using HtmlAgilityPack
/// </summary>
public IEnumerable<string> HtmlAgilityPack()
{
    HtmlDocument htmlSnippet = new HtmlDocument();
    htmlSnippet.LoadHtml(Html);

    List<string> hrefTags = new List<string>();

    foreach (HtmlNode link in htmlSnippet.DocumentNode.SelectNodes("//a[@href]"))
    {
        HtmlAttribute att = link.Attributes["href"];
        hrefTags.Add(att.Value);
    }

    return hrefTags;
}

Однако, если вам захочется поработать с css-классами, то использование XPath доставит вам много головной боли:

/// <summary>
/// Extract all anchor tags using HtmlAgilityPack
/// </summary>
public IEnumerable<string> HtmlAgilityPack()
{
    HtmlDocument hap = new HtmlDocument();
    hap.LoadHtml(html);
    HtmlNodeCollection nodes = hap
        .DocumentNode
        .SelectNodes("//h3[contains(concat(' ', @class, ' '), ' r ')]/a");
    
    List<string> hrefTags = new List<string>();

    if (nodes != null)
    {
        foreach (HtmlNode node in nodes)
        {
            hrefTags.Add(node.GetAttributeValue("href", null));
        }
    }

    return hrefTags;
}

Из замеченных странностей — специфическое API, порой непонятное и запутывающее. Если ничего не найдено, возвращается null, а не пустая коллекция. Ну и обновление библиотеки как-то затянулось — новый код давно никто не коммитал. Баги не фиксаются ( Athari [2] упоминал о критическом баге Incorrect parsing of HTML4 optional end tags [21], который приводит к некорректной обработке тегов HTML, закрывающие теги для которых опциональны.)

Fizzler [5]

Надстройка к HtmlAgilityPack, позволяющая использовать селекторы CSS.
Код, в данном случае, будет наглядным описанием того, какую проблему решает Fizzler [5]:

// Документ загружается как обычно
var html = new HtmlDocument();
html.LoadHtml(@"
  <html>
      <head></head>
      <body>
        <div>
          <p class='content'>Fizzler</p>
          <p>CSS Selector Engine</p></div>
      </body>
  </html>");

// Fizzler это набор методов-расширений для HtmlAgilityPack
// к примеру QuerySelectorAll у HtmlNode

var document = html.DocumentNode;

// вернется: [<p class="content">Fizzler</p>]
document.QuerySelectorAll(".content"); 

// вернется: [<p class="content">Fizzler</p>,<p>CSS Selector Engine</p>]
document.QuerySelectorAll("p");

// вернется пустая последовательность
document.QuerySelectorAll("body>p");

// вернется [<p class="content">Fizzler</p>,<p>CSS Selector Engine</p>]
document.QuerySelectorAll("body p");

// вернется [<p class="content">Fizzler</p>]
document.QuerySelectorAll("p:first-child");

По скорости работы практически не отличается от HtmlAgilityPack, но удобнее за счет работы с селекторами CSS.

С коммитами такая же проблема как и у HtmlAgilityPack — обновлений давно нет и, по-видимому, не предвидится.

Лицензия: LGPL.

CsQuery [4]

Был одним из современных парсеров HTML для .NET. В качестве основы был взят парсер validator.nu для Java, который в свою очередь является портом парсера из движка Gecko (Firefox).

API черпал вдохновение у jQuery, для выбора элементов используется язык селекторов CSS. Названия методов скопированы практически один-в-один, то есть для программистов, знакомых с jQuery, изучение будет простым.

На данный момент разработка CsQuery [4] находится в пассивной стадии.

Сообщение от разработчика

CsQuery is not being actively maintained. I no longer use it in my day-to-day work, and indeed don't even work in .NET much these day! Therefore it is difficult for me to spend any time addressing problems or questions. If you post issues, I may not be able to respond to them, and it's very unlikely I will be able to make bug fixes.

While the current release on NuGet (1.3.4) is stable, there are a couple known bugs (see issues) and there are many changes since the last release in the repository. However, I am not going to publish any more official releases, since I don't have time to validate the current code base and address the known issues, or support any unforseen problems that may arise from a new release.

I would welcome any community involvement in making this project active again. If you use CsQuery and are interested in being a collaborator on the project please contact me directly.

Сам автор советует использовать AngleSharp [3] как альтернативу своему проекту.

Код для получения ссылок со страницы выглядит приятно и знакомо для всех, кто использовал jQuery:

/// <summary>
/// Extract all anchor tags using CsQuery
/// </summary>
public IEnumerable<string> CsQuery()
{
    List<string> hrefTags = new List<string>();

    CQ cq = CQ.Create(Html);
    foreach (IDomObject obj in cq.Find("a"))
    {
        hrefTags.Add(obj.GetAttribute("href"));
    }

    return hrefTags;
}

Лицензия: MIT [22]

AngleSharp [3]

В отличие от CsQuery, написан с нуля вручную на C#. Также включает парсеры других языков.

API построен на базе официальной спецификации по JavaScript HTML DOM. В некоторых местах есть странности, непривычные для разработчиков на .NET (например, при обращении к неверному индексу в коллекции будет возвращён null, а не выброшено исключение; есть свой отдельный класс Url; пространства имён очень гранулярные), но в целом ничего критичного.

Развивается библиотека очень быстро. Количество различных плюшек, облегчающих работу просто поражает воображение, к примеру IHtmlTableElement [23], IHtmlProgressElement [24] и тд.

Код чистый, аккуратных, удобный.
К примеру, извлечение ссылок со страницы практически ничем не отличается от Fizzler:

/// <summary>
/// Extract all anchor tags using AngleSharp
/// </summary>
public IEnumerable<string> AngleSharp()
{
    List<string> hrefTags = new List<string>();

    var parser = new HtmlParser();
    var document = parser.Parse(Html);
    foreach (IElement element in document.QuerySelectorAll("a"))
    {
    hrefTags.Add(element.GetAttribute("href"));
    }

    return hrefTags;
}

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

Лицензия: MIT [25]

Regex [26]

Древний и не самый удачных подход для работы с HTML. Мне очень понравился комментарий Athari [2], поэтому я его, комментарий, здесь и продублирую:

Страшные и ужасные регулярные выражения. Применять их нежелательно, но иногда возникает необходимость, так как парсеры, которые строят DOM, заметно прожорливее, чем Regex: они потребляют больше и процессорного времени, и памяти.

Если дошло до регулярных выражений, то нужно понимать, что вы не сможете построить на них универсальное и абсолютно надёжное решение. Однако если вы хотите парсить конкретный сайт, то эта проблема может быть не так критична.

Ради всего святого, не надо превращать регулярные выражения в нечитаемое месиво. Вы не пишете код на C# в одну строчку с однобуквенными именами переменных, так и регулярные выражения не нужно портить. Движок регулярных выражений в .NET достаточно мощный, чтобы можно было писать качественный код.

Код для получения ссылок со страницы выглядит ещё более-менее понятно:

/// <summary>
/// Extract all anchor tags using Regex
/// </summary>
public IEnumerable<string> Regex()
{
    List<string> hrefTags = new List<string>();

    Regex reHref = new Regex(@"(?inx)
    <a s [^>]*
        href s* = s*
            (?<q> ['""] )
                (?<url> [^""]+ )
            k<q>
    [^>]* >");
    
    foreach (Match match in reHref.Matches(Html))
    {
        hrefTags.Add(match.Groups["url"].ToString());
    }

    return hrefTags;
}

Но если вам вдруг захочется поработать с таблицами, да ещё и в вычурном формате, то пожалуйста, сначала посмотрите сюда [27].

Лицензия указана на этом сайте [28].

Benchmark

Скорость работы парсера, как ни крути, один из важнейших атрибутов. От скорости обработки HTML зависит то, сколько у вас времени займет та или иная задача.

Для замера производительности парсеров я использовал библиотеку BenchmarkDotNet [29] от DreamWalker [30], за что ему огромное спасибо.
Замеры производились на Intel Core(TM) i7-4770 CPU @ 3.40GHz, но опыт подсказывает, что относительное время будет одинаковое на любых других конфигурациях.

Пару слов о Regex — не повторяйте этого дома. Regex очень хороший инструмент в умелых руках, но работа с HTML это точно не то, где стоит его использовать. Но в качестве эксперимента я попробовал реализовать минимально рабочую версию кода. Свою задачу он выполнил успешно, но количество времени, потраченное на написание этого кода, подсказывает, что повторять это я точно не стану.

Что ж, давай-те посмотрим на бенчмарки.

Получение адресов из ссылок на странице

Данная задача, как мне кажется, является базовой для всех парсеров — чаще именно с такой постановки задачи начинается увлекательное знакомство с миром парсеров (иногда и Regex).

Код бенчмарка можно найти на github [31], а ниже представлена таблица с результатами:

Библиотека Среднее время Среднеквадратическое отклонение операций/сек
AngleSharp [3] 8.7233 ms 0.4735 ms 114.94
CsQuery [4] 12.7652 ms 0.2296 ms 78.36
Fizzler [5] 5.9388 ms 0.1080 ms 168.44
HtmlAgilityPack [19] 5.4742 ms 0.1205 ms 182.76
Regex [7] 3.2897 ms 0.1240 ms 304.37

В целом, ожидаемо Regex оказался самым быстрым, но далеко не самым удобным. HtmlAgilityPack и Fizzler показали примерно одинаковое время обработки, немного опередив AngleSharp. CsQuery, к сожалению, безнадежно отстал. Вполне вероятно, что я не умею его готовить. Буду рад услышать комментарии от людей, которые работали с данной библиотекой.

Оценить удобство не представляется возможным, так как код практически идентичен. Но при прочих равных условиях, код CsQuery и AngleSharp мне понравился больше.

Получение данных из таблицы

С данной задачей я столкнулся на практике. Причем таблица, с которой мне предстаяло поработать, не была простой.

Заметка о жизни в Беларуси

Захотелось мне получать актуальную информацию о обменном курсе валют в славном городе Минске. Каких-либо сервисов, для получения информации о курсах в банках, найдено не было, но случайно наткнулся на http://select.by/kurs/ [32]. Там информация обновляется частно и есть то, что мне нужно. Но в очень неудобном формате.
Ребят, если будете это читать — сделайте нормальный сервис, ну или хотя бы HTML поправьте.

Я предпринял попытку максимально запрятать всё то, что не относится именно к обработке HTML, но ввиду специфики задачи, не всё получилось.

Код у всех библиотек примерно одинаков, отличие только в API и том, какие возвращаются результаты. Однако стоит упомянуть о двух вещах: во-первых, у AngleSharp есть специализированные интерфейсы, что облечило решение задачи. Во-вторых, Regex для данной задачи не подходит вообще никак [27].

Давай-те посмотрим на результаты:

Библиотека Среднее время Среднеквадратическое отклонение операций/сек
AngleSharp [3] 27.4181 ms 1.1380 ms 36.53
CsQuery [4] 42.2388 ms 0.7857 ms 23.68
Fizzler [5] 21.7716 ms 0.6842 ms 45.97
HtmlAgilityPack [19] 20.6314 ms 0.3786 ms 48.49
Regex [7] 42.2942 ms 0.1382 ms 23.64

Как и в предыдущем примере HtmlAgilityPack и Fizzler показали примерно одинаковое и очень хорошее время. AngleSharp отстаёт от них, но, возможно, я сделал всё не самым оптимальным образом. К моему удивлению, CsQuery и Regex показали одинаково плохое время обработки. Если с CsQuery всё понятно — он просто медленный, то с Regex не всё так однозначно — скорее всего задачу можно решить более оптимальным способом.

Выводы

Выводы, наверное, каждый сделал для себя сам. От себя добавлю, что оптимальным выбором сейчас будет AngleSharp, так как он активно разрабатывается, облатает интуитивным API и показывает хорошо время обработки. Имеет ли смысл перебегать на AngleSharp с HtmlAgilityPack? Скорее всего нет — ставим Fizzler и радуемся очень быстрой и удобной библиотеке.

Всем спасибо за внимание.
Весь код можно найти в репозитории на github [8]. Любые дополнения и/или изменения только приветствуются.

Автор: forcewake

Источник [33]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-2/107157

Ссылки в тексте:

[1] комменарий: http://habrahabr.ru/post/112325/#comment_8578117

[2] Athari: http://habrahabr.ru/users/athari/

[3] AngleSharp: https://github.com/AngleSharp/AngleSharp

[4] CsQuery: https://github.com/jamietre/CsQuery

[5] Fizzler: https://code.google.com/p/fizzler/

[6] HtmlAgilityPack: https://htmlagilitypack.codeplex.com/

[7] Regex-way: http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454

[8] github: https://github.com/forcewake/Benchmarks

[9] Описание библиотек: #description

[10] HtmlAgilityPack: #HtmlAgilityPack

[11] Fizzler: #Fizzler

[12] CsQuery: #CsQuery

[13] AngleSharp: #AngleSharp

[14] Regex: #Regex

[15] Benchmark: #Benchmark

[16] Получение адресов из ссылок на странице: #ahref-benchmark

[17] Получение данных из таблицы: #table-benchmark

[18] Выводы: #results

[19] HtmlAgilityPack: https://htmlagilitypack.codeplex.com

[20] habrahabr: http://habrahabr.ru/post/112325/

[21] Incorrect parsing of HTML4 optional end tags: https://htmlagilitypack.codeplex.com/workitem/29218

[22] MIT: https://github.com/jamietre/CsQuery/blob/master/LICENSE.txt

[23] IHtmlTableElement: https://github.com/AngleSharp/AngleSharp/blob/master/AngleSharp/Interfaces/Html/IHtmlTableElement.cs

[24] IHtmlProgressElement: https://github.com/AngleSharp/AngleSharp/blob/master/AngleSharp/Interfaces/Html/IHtmlProgressElement.cs

[25] MIT: https://github.com/AngleSharp/AngleSharp/blob/master/LICENSE

[26] Regex: https://msdn.microsoft.com/en-us/library/system.text.regularexpressions.regex(v=vs.110).aspx

[27] сюда: https://github.com/forcewake/Benchmarks/blob/master/src/Benchmarks.HtmlParsers/Benchmarks/TableBenchmark.cs#L207-L261

[28] этом сайте: http://www.microsoft.com/net/dotnet_library_license.htm

[29] BenchmarkDotNet: https://github.com/PerfDotNet/BenchmarkDotNet

[30] DreamWalker: http://habrahabr.ru/users/dreamwalker/

[31] github: https://github.com/forcewake/Benchmarks/blob/master/src/Benchmarks.HtmlParsers/Benchmarks/AHrefBenchmark.cs

[32] http://select.by/kurs/: http://select.by/kurs/

[33] Источник: http://habrahabr.ru/post/273807/