Пользователь
0,0
рейтинг
23 декабря 2015 в 19:02

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


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

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

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


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

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

Содержание




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


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

HtmlAgilityPack


Один из самых (если не самый) известный парсер HTML в мире .NET. Про него написано немало статей как на русском, так и на английском языках, к примеру на habrahabr.

Вкратце это быстрая, относительно удобная библиотека для работы с HTML (если XPath запросы будут несложными). Репозиторий давно не обновляется.
Лицензия 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 упоминал о критическом баге Incorrect parsing of HTML4 optional end tags, который приводит к некорректной обработке тегов HTML, закрывающие теги для которых опциональны.)

Fizzler


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

// Документ загружается как обычно
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


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

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

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

Сообщение от разработчика
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 как альтернативу своему проекту.

Код для получения ссылок со страницы выглядит приятно и знакомо для всех, кто использовал 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

AngleSharp


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

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

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

Код чистый, аккуратных, удобный.
К примеру, извлечение ссылок со страницы практически ничем не отличается от 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

Regex


Древний и не самый удачных подход для работы с HTML. Мне очень понравился комментарий Athari, поэтому я его, комментарий, здесь и продублирую:
Страшные и ужасные регулярные выражения. Применять их нежелательно, но иногда возникает необходимость, так как парсеры, которые строят 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;
}


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

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

Benchmark


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

Для замера производительности парсеров я использовал библиотеку BenchmarkDotNet от DreamWalker, за что ему огромное спасибо.

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

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

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

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


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

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

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


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

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

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


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

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


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

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

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

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


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

Выводы


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

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

Проголосовало 279 человек. Воздержалось 162 человека.

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

Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (34)

  • 0
    Буквально на прошлой неделе наткнулся на очень милую граблю: HtmlAgilityPack при попытке распарсить одну невалидную html-ину улетел в stack overflow, чем и положил весь процесс =\ Никто не в курсе, как с обработкой кривых данных из реального веба у конкурентов (в основном интересует AngleSharp, как наиболее живой и вкусный на вид)?
    • +1
      Насколько мне известно, HAP — единственная библиотека, которая не дружит с кривым HTML. Что AS, что CQ должны работать нормально. Фактами доказать не могу, но глюки HAP задокументированы, а про обе нормальные библиотеки ничего подобного не слышал. :)
    • +1
      Вот еще один интересный баг фича в HtmlAgilityPack — элемент form не содержит дочерних элементов(если нужно распарсить параметры form для формирования последующего запроса, приходится использовать div вместо form в xPath подробнее)
  • 0
    Глаз зацепился:
    Однако, если вам захочется поработать с css-классами, то использование XPath доставит вам много головной боли

    //h3[contains(concat(' ', @class, ' '), ' r ')]/a
    

    А что вы делаете этим запросом?
    • +2
      Это эквивалент CSS-запроса «h3.r>a», который корректно обрабатывает атрибуты class вида «r q», «q r», «rq» и другие. Можно было бы написать просто «h3[@class='r']/a», но тогда обработка атрибута была бы не как в CSS.
  • +14
    Славно накопипастили. :) Ну ладно, я не жадный. Впрочем, если подходить формально, то текст на SO лицензирован под CC BY-SA, то есть не помешает указать ссылку на источник (вопрос на SO) и лицензию (CC BY-SA 3.0). Куски кода — под Public Domain, в соответствии с указанием в моём профиле на SO (я юридически не имею права этого делать, но оставим придури законов за рамками).

    Что касается производительности, то у CsQuery и HtmlAgilityPack изначально были оптимизации в разные стороны, в результате CQ быстрее обрабатывал сложные запросы за счёт построения всяких индексов, а HAP быстрее искал по всем документы простыми запросами за счёт, собственно, отсутствия индексов. За AngleSharp не отвечаю. Я с автором пообщался, он упёртый как баран.

    Что меня напрягает во всех трёх библиотеках — это что все три паршиво следуют Framework Design Guidelines, да и просто хорошим практикам из самого .NET: HAP возвращает null вместо пустых коллекций; CQ имитирует краткие записи а-ля jQuery, нарушая все мыслимые стили именования; AS бездумно копирует интерфейсы из стандартов, наступая на грабли XmlDocument… В результате получается набор «молотков PHP»: вроде, все три работают, всеми можно пользоваться, но у всех трёх какие-то неоправданные странности. Впрочем, это взгляд перфекциониста; полагаю, большинству на такие нюансы наплевать. :)
  • +1
    Много раз приходилось парсить html в .net, все время юзал HtmlAgilityPack. За исключением мелких граблей, все работает очень прилично.
  • +2
    Каждый раз, когда тянет использовать Regex для парсинга HTML, читаю этот ответ, успокаиваюсь и использую как минимум HtmlAgilityPack, чего и всем желаю.
    • 0
      меня всегда интересовало, на чем основано убеждение этого ответа. Ну вот нужно выдрать там какой нить ответ из html и в чем ужас использовать для этого самый эффективный способ?
      • 0
        Ужас в цене поддержки кода. Если сайт часто меняется, то программисты удавятся ковырять регулярки каждый раз. Если код поддерживать не надо, если код сайта стабильный, если программисты дешевле железа и так далее, то регулярки рулят, конечно.
        • НЛО прилетело и опубликовало эту надпись здесь
          • +2
            Вопрос в устойчивости парсера к изменениям в коде HTML и в количестве необходимого кода и его читаемости.

            1. Если не делать регулярки непомерно сложными, то они будут ломаться из-за добавления атрибута в каком-нибудь элементе или изменения порядка классов в атрибуте. CSS-запросы более устойчивы к подобным изменениям.

            2. При написании регулярок очень легко скатиться к написанию write-only кода, когда регулярка пишется в одну строчку, а баги фиксятся костылями («воткну-ка я здесь look-behind, вроде, начинает работать»). После нескольких итераций код будет легче написать заново, чем исправить.

            3. При работе с DOM и CSS-запросами кода заметно меньше: работа с атрибутами, каскадами и прочим доступна из коробки, а не требует написания с нуля (или копипасты) ради каждой новой странички.

            4. Порог вхождения в CSS-запросы ниже, не нужно знать премудростей регулярок, чтобы сделать простые вещи.

            По производительности регулярки рвут полноценные парсеры с построением DOM как тузик грелку, конечно, но далеко не всегда парсинг — самое узкое место. Да и если узкое, не всегда выгоднее оптимизировать код (превращением его в нечитаемое месиво) вместо закидывания железом.

            И я говорю как раз про использование селекторов CSS, а не XPath. XPath хоть и лучше подходит для HTML, чем регулярные выражения, но не может сравниться с селекторами, весь смысл существования которых в выборке элементов из HTML.
      • +2
        Если надо вытащить текст из пары нодов, то да, возможно, наверное, не знаю.

        Вначале вы пишете регулярки типа ...class=...[^>]*>(?<text>...)<... .

        Но вдруг, надо работать с атрибутами, порядок которых неизвестен, и начинаются пляски с опциональными группами, и вы начинаете писать регулярки типа ...(?<attr1>...)?(?<attr2>...)? либо выполняете несколько matches на каждый атрибут.

        Потом вы хотите работать с коллекциями, допустим с <ul> или чего еще хуже с таблицами, и вы начинаете понимать что что-то пошло не так, но продолжаете использовать регулярки, чтобы достать значения из td вы вначале захватываете таблицу, потом коллекцию tr, и проходите по коллекции td, регулярки вида ...[^>]*>(?<text>...)<... растут как грибы на каждый элемент/идентификатор/класс/аттрибут.

        И вдруг, вам необходимо ходить по DOMу, на этом месте будет боль и холодный пот, т.к. везде уже тонны регулярок. Вы с тоской удаляете регулярки, на которые затрачено много времени и используете xpath.

        Это мой печальный опыт, если у вас по-другому, расскажите.
        • 0
          при таком подходе явно xpath проще :-) Просто парсинг обычно самодостаточен. Вот html — дай результат. И там все уже ясно.
      • 0
        в чем ужас использовать для этого самый эффективный способ?
        Как раз ровно об этом следующий ответ на странице.
        • 0
          Да, верно.Просто эта цифра «4427» на SO имеет авторитет Бога.
          А у «Don't listen to these guys.» всего 760 :-)
          • +3
            Вы только первое предложение из ответа с 760 голосами прочитали?)
            Don't listen to these guys. You actually can parse context-free grammars with regex if you break the task into smaller pieces. You can generate the correct pattern with a script that does each of these in order:

            Solve the Halting Problem.
            Square a circle (simulate the «ruler and compass» method for this).
            Work out the Traveling Salesman Problem in O(log n). It needs to be fast or the generator will hang.
            The pattern will be pretty big, so make sure you have an algorithm that losslessly compresses random data.
            Almost there — just divide the whole thing by zero. Easy-peasy.
  • 0
    Недавно возникла задача написать утилиту, уведомляющую об обновлениях на определенном сайте, где результаты поиска подгружаются динамически.

    Использовал PhantonJS + Selenium Webdriver. В плане использования связка оказалась очень удобной, чего не скажешь о производительности.
  • 0
    Я работаю с 2009 года с библиотекой Chilkat .NET — перевожу HTML в XHTML, а дальше работаю с ним как с XDocument'ом.
    • 0
      Положим, XPath (и тем более ручная навигация) — тоже не сахар.
  • 0
    Нужен вариант «использую свой велосипед».
    • 0
      Прямо-таки мучает вопрос, какой можно изобрести велосипед для обработки HTML…
      • 0
        Четыре библиотеки из статьи изобрели же зачем-то. А мне нужен был простой парсер для построения DOM. Без блэкджека и селекторов. Пробовал HtmlAgilityPack — как показал html5test.com, он не совсем корректен. О других не знал, решил написать сам — делов то на пару часов.
        • +1
          И… ваш парсер, написанный за два часа, превзошёл по качеству HAP?.. По-моему, вам стоит написать об этом статью. Я б почитал.
          • 0
            Я не решал задачу «превзойти по качеству X», мне нужен был html парсер для браузера. Хотелось найти более лёгкую замену паре phantomjs+selenium для автоматизированного тестирования. Как-нибудь обязательно напишу об этом статью или несколько.
  • 0
    Спасибо за хороший обзор :)
  • 0
    Ещё можно подключить jsoup через ikvm. Выходит 700 кб библиотека и 50 мб зависимостей, но работает нормально. Я использовал как замену htmlagiltypack когда нужно было быстро сделать парсер., так как CSS селекторы в jsoup работают надежней чем xpath в htmlagilitypack.
    • 0
      А как же Fizzler?
      • 0
        На тот момент я о нем ничего не знал и паралелльно узнал о ikvm и имел опыт использования jsoup. Если надергать CSS селекторы из отладчика хрома, то можно создать парсер за 15 минут. Конечно он ломается когда меняют структуру страницы, но это легко и быстро исправить.
  • +1
    Вдруг кому пригодится — используем Goose Parser, очень довольны. Работает с фантомом, позволяет декларативно описать действия пользователя и хранить их в обычном JSON. Расширяемый, если надо добавить своих фишек.
  • +1
    Использую HtmlParser и HtmlTypeProvider из FSharp.Data, когда нужно сделать быстро и удобно
  • +1
    Я вижу, что автор против того, чтобы парсить HTML регулярками, но не могу не напомнить о знаменитом ответе на стеке.
  • 0
    использую SGMLReader и XPath в таких случаях github.com/MindTouch/SGMLReader
  • 0
    Большое спасибо за обзор. Раньше всегда использовал HAP, который славится своими багами. Но приходилось как-то жить с ним. Но благодаря обзору открыл для себя AngleSharp — это просто сказочная библиотека, бриллиант. Спасибо!

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