В ходе работы над одним домашним проектом, столкнулся с необходимостью парсинга HTML. Поиск по гуглу выдал комментарий Athari и его микро-обзор актуальных парсеров HTML в .NET за что ему огромное спасибо.
К сожалению, никаких цифр и/или аргументов в пользу того или иного парсера найдено не было, что послужило поводом к написанию данной статьи.
Сегодня я протестирую популярные, на данный момент, библиотеки для работы с HTML, а именно: AngleSharp, CsQuery, Fizzler, HtmlAgilityPack и, конечно же, Regex-way. Сравню их по скорости работы и удобству использования.
TL;DR: Код всех бенчмарков можно найти на github. Там же лежат результаты тестирования. Самым актуальным парсером на данный момент является AngleSharp — удобный, быстрый,
Тем, кому интересен подробный обзор — добро пожаловать под кат.
Содержание
Описание библиотек
В данном разделе будут краткие описания рассматриваемых библиотек, описание лицензий и тд.
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 мне понравился больше.
Получение данных из таблицы
С данной задачей я столкнулся на практике. Причем таблица, с которой мне предстаяло поработать, не была простой.
Ребят, если будете это читать — сделайте нормальный сервис, ну или хотя бы 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. Любые дополнения и/или изменения только приветствуются.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (34)
masterL
23.12.2015 21:03Глаз зацепился:
Однако, если вам захочется поработать с css-классами, то использование XPath доставит вам много головной боли
//h3[contains(concat(' ', @class, ' '), ' r ')]/a
А что вы делаете этим запросом?Athari
23.12.2015 21:17+2Это эквивалент CSS-запроса «h3.r>a», который корректно обрабатывает атрибуты class вида «r q», «q r», «rq» и другие. Можно было бы написать просто «h3[@class='r']/a», но тогда обработка атрибута была бы не как в CSS.
Athari
23.12.2015 21:13+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»: вроде, все три работают, всеми можно пользоваться, но у всех трёх какие-то неоправданные странности. Впрочем, это взгляд перфекциониста; полагаю, большинству на такие нюансы наплевать. :)
isxaker
23.12.2015 21:52+1Много раз приходилось парсить html в .net, все время юзал HtmlAgilityPack. За исключением мелких граблей, все работает очень прилично.
BloodUnit
23.12.2015 23:05+2Каждый раз, когда тянет использовать Regex для парсинга HTML, читаю этот ответ, успокаиваюсь и использую как минимум HtmlAgilityPack, чего и всем желаю.
Alexufo
24.12.2015 01:15меня всегда интересовало, на чем основано убеждение этого ответа. Ну вот нужно выдрать там какой нить ответ из html и в чем ужас использовать для этого самый эффективный способ?
Athari
24.12.2015 01:20Ужас в цене поддержки кода. Если сайт часто меняется, то программисты удавятся ковырять регулярки каждый раз. Если код поддерживать не надо, если код сайта стабильный, если программисты дешевле железа и так далее, то регулярки рулят, конечно.
Mixim333
24.12.2015 03:05Ни могли бы объяснить, что подразумевается под: «Если сайт часто меняется, то программисты удавятся ковырять регулярки каждый раз», разве HtmlAgilityPack не сломается, если архитектура странички сайта будет переделана?
Сам разбираю непубличный HTML-портал с помощью регулярок, архитектура работы которого следующая: «устройство — Linux-серверх — Grep — NETWORK — мое приложение» — к первым 4 звеньям я никакого отношения не имею и периодически эти звенья дают сбой (необходимо все жестко проверять — дополнительное действие для регулярки — IsMatch), под «устройством» подразумеваются логи с некоторой железки. Кстати, еще по поводу скорости Regex: у него есть замечательный RegexOptions.Compiled, применение которого позволило мне сократить работу регулярки примерно на 20-40%Athari
24.12.2015 03:25+2Вопрос в устойчивости парсера к изменениям в коде HTML и в количестве необходимого кода и его читаемости.
1. Если не делать регулярки непомерно сложными, то они будут ломаться из-за добавления атрибута в каком-нибудь элементе или изменения порядка классов в атрибуте. CSS-запросы более устойчивы к подобным изменениям.
2. При написании регулярок очень легко скатиться к написанию write-only кода, когда регулярка пишется в одну строчку, а баги фиксятся костылями («воткну-ка я здесь look-behind, вроде, начинает работать»). После нескольких итераций код будет легче написать заново, чем исправить.
3. При работе с DOM и CSS-запросами кода заметно меньше: работа с атрибутами, каскадами и прочим доступна из коробки, а не требует написания с нуля (или копипасты) ради каждой новой странички.
4. Порог вхождения в CSS-запросы ниже, не нужно знать премудростей регулярок, чтобы сделать простые вещи.
По производительности регулярки рвут полноценные парсеры с построением DOM как тузик грелку, конечно, но далеко не всегда парсинг — самое узкое место. Да и если узкое, не всегда выгоднее оптимизировать код (превращением его в нечитаемое месиво) вместо закидывания железом.
И я говорю как раз про использование селекторов CSS, а не XPath. XPath хоть и лучше подходит для HTML, чем регулярные выражения, но не может сравниться с селекторами, весь смысл существования которых в выборке элементов из HTML.
BloodUnit
24.12.2015 02:11+2Если надо вытащить текст из пары нодов, то да, возможно, наверное, не знаю.
Вначале вы пишете регулярки типа...class=...[^>]*>(?<text>...)<...
.
Но вдруг, надо работать с атрибутами, порядок которых неизвестен, и начинаются пляски с опциональными группами, и вы начинаете писать регулярки типа...(?<attr1>...)?(?<attr2>...)?
либо выполняете несколько matches на каждый атрибут.
Потом вы хотите работать с коллекциями, допустим с<ul>
или чего еще хуже с таблицами, и вы начинаете понимать что что-то пошло не так, но продолжаете использовать регулярки, чтобы достать значения из td вы вначале захватываете таблицу, потом коллекцию tr, и проходите по коллекции td, регулярки вида...[^>]*>(?<text>...)<...
растут как грибы на каждый элемент/идентификатор/класс/аттрибут.
И вдруг, вам необходимо ходить по DOMу, на этом месте будет боль и холодный пот, т.к. везде уже тонны регулярок. Вы с тоской удаляете регулярки, на которые затрачено много времени и используете xpath.
Это мой печальный опыт, если у вас по-другому, расскажите.Alexufo
24.12.2015 02:20при таком подходе явно xpath проще :-) Просто парсинг обычно самодостаточен. Вот html — дай результат. И там все уже ясно.
withkittens
24.12.2015 02:20в чем ужас использовать для этого самый эффективный способ?
Как раз ровно об этом следующий ответ на странице.Alexufo
24.12.2015 02:22Да, верно.Просто эта цифра «4427» на SO имеет авторитет Бога.
А у «Don't listen to these guys.» всего 760 :-)BloodUnit
24.12.2015 02:42+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.
efremovaleksey
24.12.2015 02:27Недавно возникла задача написать утилиту, уведомляющую об обновлениях на определенном сайте, где результаты поиска подгружаются динамически.
Использовал PhantonJS + Selenium Webdriver. В плане использования связка оказалась очень удобной, чего не скажешь о производительности.
petuhov_k
24.12.2015 05:49Нужен вариант «использую свой велосипед».
Athari
24.12.2015 07:50Прямо-таки мучает вопрос, какой можно изобрести велосипед для обработки HTML…
petuhov_k
24.12.2015 08:25Четыре библиотеки из статьи изобрели же зачем-то. А мне нужен был простой парсер для построения DOM. Без блэкджека и селекторов. Пробовал HtmlAgilityPack — как показал html5test.com, он не совсем корректен. О других не знал, решил написать сам — делов то на пару часов.
Athari
24.12.2015 09:59+1И… ваш парсер, написанный за два часа, превзошёл по качеству HAP?.. По-моему, вам стоит написать об этом статью. Я б почитал.
petuhov_k
25.12.2015 05:46Я не решал задачу «превзойти по качеству X», мне нужен был html парсер для браузера. Хотелось найти более лёгкую замену паре phantomjs+selenium для автоматизированного тестирования. Как-нибудь обязательно напишу об этом статью или несколько.
l0cal
24.12.2015 09:55Ещё можно подключить jsoup через ikvm. Выходит 700 кб библиотека и 50 мб зависимостей, но работает нормально. Я использовал как замену htmlagiltypack когда нужно было быстро сделать парсер., так как CSS селекторы в jsoup работают надежней чем xpath в htmlagilitypack.
Athari
24.12.2015 09:57А как же Fizzler?
l0cal
24.12.2015 10:30На тот момент я о нем ничего не знал и паралелльно узнал о ikvm и имел опыт использования jsoup. Если надергать CSS селекторы из отладчика хрома, то можно создать парсер за 15 минут. Конечно он ломается когда меняют структуру страницы, но это легко и быстро исправить.
and_rew
24.12.2015 10:18+1Вдруг кому пригодится — используем Goose Parser, очень довольны. Работает с фантомом, позволяет декларативно описать действия пользователя и хранить их в обычном JSON. Расширяемый, если надо добавить своих фишек.
danslapman
24.12.2015 11:22+1Использую HtmlParser и HtmlTypeProvider из FSharp.Data, когда нужно сделать быстро и удобно
redmanmale
24.12.2015 14:09+1Я вижу, что автор против того, чтобы парсить HTML регулярками, но не могу не напомнить о знаменитом ответе на стеке.
luxferre
24.12.2015 14:10использую SGMLReader и XPath в таких случаях github.com/MindTouch/SGMLReader
Ch0bits
24.12.2015 15:04Большое спасибо за обзор. Раньше всегда использовал HAP, который славится своими багами. Но приходилось как-то жить с ним. Но благодаря обзору открыл для себя AngleSharp — это просто сказочная библиотека, бриллиант. Спасибо!
HellBrick
Буквально на прошлой неделе наткнулся на очень милую граблю: HtmlAgilityPack при попытке распарсить одну невалидную html-ину улетел в stack overflow, чем и положил весь процесс =\ Никто не в курсе, как с обработкой кривых данных из реального веба у конкурентов (в основном интересует AngleSharp, как наиболее живой и вкусный на вид)?
Athari
Насколько мне известно, HAP — единственная библиотека, которая не дружит с кривым HTML. Что AS, что CQ должны работать нормально. Фактами доказать не могу, но глюки HAP задокументированы, а про обе нормальные библиотеки ничего подобного не слышал. :)
isxaker
Вот еще один интересный
багфича в HtmlAgilityPack — элемент form не содержит дочерних элементов(если нужно распарсить параметры form для формирования последующего запроса, приходится использовать div вместо form в xPath подробнее)