Как распарсить HTML в .NET?

1,00
р.
Необходимо извлечь все URL из атрибутов href тегов a в HTML странице. Я попробовал воспользоваться регулярными выражениями:
Uri uri = new Uri("http://google.comearch?q=test") Regex reHref = new Regex(@"]+href=""([^""]+)""[^>]+>") string html = new WebClient().DownloadString(uri) foreach (Match match in reHref.Matches(html)) Console.WriteLine(match.Groups[1].ToString())
Но возникает множество потенциальных проблем:
Как отфильтровать только специфические ссылки, например, по CSS классу? Что будет, если кавычки у атрибута другие? Что будет, если вокруг знака равенства пробелы? Что будет, если кусок страницы закомментирован? Что будет, если попадётся кусок JavaScript? И так далее.
Регулярное выражение очень быстро становится монструозным и нечитаемыми, а проблемных мест обнаруживается всё больше и больше.
Что делать?

Ответ
TL DR
Для парсинга HTML используте AngleSharp.
Если вам нужно не только распарсить HTML, но и запустить полноценный браузер, выполнить все скрипты, понажимать на кнопки и посмотреть, что получилось, то используйте CefSharp или Selenium. Учтите, что это будет на порядки медленнее.
Для любознательных
Регулярные выражения предназначены для обработки относительно простых текстов, которые задаются регулярными языками. Регулярные выражения со времени своего появления сильно усложнились, особенно в Perl, реализация регулярных выражений в котором является вдохновением для остальных языков и библиотек, но регулярные выражения всё ещё плохо приспособлены (и вряд ли когда-либо будут) для обработки сложных языков типа HTML. Сложность обработки HTML заключается ещё и в очень сложных правилах обработки невалидного кода, которые достались по наследству от первых реализаций времён рождения Интернета, когда никаких стандартов не было и в помине, а каждый производитель браузеров нагромождал уникальные и неповторимые возможности.
Итак, в общем случае регулярные выражения — не лучший кандидат для обработки HTML. Обычно разумнее использовать специализированные парсеры HTML.
AngleSharp
Лицензия: BSD (3-clause)
Проверенный игрок на поле парсеров. В отличие от CsQuery, написан с нуля вручную на C#. Также включает парсеры других языков.
API построен на базе официальной спецификации по JavaScript HTML DOM. Изначально содержал в некоторых местах странности, непривычные для разработчиков на .NET (например, при обращении к неверному индексу в коллекции будет возвращён null, а не выброшено исключение), но разработчик в конце концов сдался и исправил самые жуткие костыли. Что-то ушло само, например, Microsoft BCL Portability Pack. Что-то осталось, например, пространства имён очень гранулярные, даже базовое использование библиотеки требует три using и т. п.), но в целом ничего критичного.
Обработка HTML простая:
IHtmlDocument angle = new HtmlParser().ParseDocument(html) foreach (IElement element in angle.QuerySelectorAll("a")) Console.WriteLine(element.GetAttribute("href"))
Она не усложняется, и если нужна более сложная логика:
IHtmlDocument angle = new HtmlParser().ParseDocument(html) foreach (IElement element in angle.QuerySelectorAll("h3.r a")) Console.WriteLine(element.GetAttribute("href"))
HtmlAgilityPack
Лицензия: Ms-PL
Самый старый и потому самый популярный парсер для .NET. Однако возраст не означает качество, например, уже ДЕСЯТЬ (!!!) ЛЕТ НЕ МОГУТ (!!!) ИСПРАВИТЬ КРИТИЧЕСКИЙ (!!!) БАГ с корректной обработкой самозакрывающихся тегов. Уже CodePlex успел умереть, а воз с Incorrect parsing of HTML4 optional end tags и ныне там. Вот, новой версии бага уже четвёртый год: Self closing tags modified. Там ещё рядом аналоги лежат. Какое-то время назад они этот баг "исправили". Для одного тега. С дополнительной опцией. И потом сломали опцию. Я уж молчу о том, что в API присутствуют странности, например, если ничего не найдено, возвращается null, а не пустая коллекция.
Для выбора элементов используется язык XPath, а не селекторы CSS. На простых запросах код получается более-менее удобочитаемый:
HtmlDocument hap = new HtmlDocument() hap.LoadHtml(html) HtmlNodeCollection nodes = hap.DocumentNode.SelectNodes("//a") if (nodes != null) foreach (HtmlNode node in nodes) Console.WriteLine(node.GetAttributeValue("href", null))
Однако если нужны сложные запросы, то XPath оказывается не очень приспособленным для имитации CSS селекторов:
HtmlDocument hap = new HtmlDocument() hap.LoadHtml(html) HtmlNodeCollection nodes = hap.DocumentNode.SelectNodes( "//h3[contains(concat(' ', @class, ' '), ' r ')]/a") if (nodes != null) foreach (HtmlNode node in nodes) Console.WriteLine(node.GetAttributeValue("href", null))
Fizzler
Лицензия: LGPL
Надстройка к HtmlAgilityPack, позволяющая использовать селекторы CSS.
HtmlDocument hap = new HtmlDocument() hap.LoadHtml(html) foreach (HtmlNode node in hap.DocumentNode.QuerySelectorAll("h3.r a")) Console.WriteLine(node.GetAttributeValue("href", null))
Так как это HtmlAgilityPack, то и все баги этого поделия прилагаются.
CsQuery
Лицензия: MIT
На данный момент проект заброшен, потому что есть AngleSharp.
Один из современных парсеров HTML для .NET. В качестве основы взят парсер validator.nu для Java, который в свою очередь является портом парсера из движка Gecko (Firefox). Это гарантирует, что парсер будет обрабатывать код точно так же, как современные браузеры.
API черпает вдохновение у jQuery, для выбора элементов используется язык селекторов CSS. Названия методов скопированы практически один-в-один, то есть для программистов, знакомых с jQuery, изучение будет простым.
Обладает высокой производительностью. На порядки превосходит HtmlAgilityPack+Fizzler по скорости на сложных запросах.
CQ cq = CQ.Create(html) foreach (IDomObject obj in cq.Find("a")) Console.WriteLine(obj.GetAttribute("href"))
Если требуется более сложный запрос, то код практически не усложняется:
CQ cq = CQ.Create(html) foreach (IDomObject obj in cq.Find("h3.r a")) Console.WriteLine(obj.GetAttribute("href"))
Если кто-то незнаком с концепциями jQuery, то нетривиальное использование может быть странным и непривычным.
Regex
Страшные и ужасные регулярные выражения. Применять их нежелательно, но иногда возникает необходимость, так как парсеры, которые строят DOM, заметно прожорливее, чем Regex: они потребляют больше и процессорного времени, и памяти.
Если дошло до регулярных выражений, то нужно понимать, что вы не сможете построить на них универсальное и абсолютно надёжное решение. Однако если вы хотите парсить конкретный сайт, то эта проблема может быть не так критична.
Ради всего святого, не надо превращать регулярные выражения в нечитаемое месиво. Вы не пишете код на C# в одну строчку с однобуквенными именами переменных, так и регулярные выражения не нужно портить. Движок регулярных выражений в .NET достаточно мощный, чтобы можно было писать качественный код.
Например, вот немного доработанный код для извлечения ссылок из вопроса:
Regex reHref = new Regex(@"(?inx) ]* href \s* = \s* (? ['""] ) (? [^""]+ ) \k [^>]* >") foreach (Match match in reHref.Matches(html)) Console.WriteLine(match.Groups["url"].ToString())