Необходимо извлечь все URL из атрибутов href тегов a в HTML странице. Я попробовал воспользоваться регулярными выражениями:
Uri uri = new Uri("http://google.com/search?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?
И так далее.
Регулярное выражение очень быстро становится монструозным и нечитаемыми, а проблемных мест обнаруживается всё больше и больше.
Что делать?
Ответ
Регулярные выражения предназначены для обработки относительно простых текстов, которые задаются регулярными языками. Регулярные выражения со времени своего появления сильно усложнились, особенно в Perl, реализация регулярных выражений в котором является вдохновением для остальных языков и библиотек, но регулярные выражения всё ещё плохо приспособлены (и вряд ли когда-либо будут) для обработки сложных языков типа HTML. Сложность обработки HTML заключается ещё и в очень сложных правилах обработки невалидного кода, которые достались по наследству от первых реализаций времён рождения Интернета, когда никаких стандартов не было и в помине, а каждый производитель браузеров нагромождал уникальные и неповторимые возможности.
Итак, в общем случае регулярные выражения — не лучший кандидат для обработки HTML. Обычно разумнее использовать специализированные парсеры HTML.
CsQuery
Лицензия: MIT
Один из современных парсеров 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"));
HtmlAgilityPack
Лицензия: Ms-PL
Самый старый, и потому самый популярный парсер для .NET. Однако возраст не означает качество, например, уже пять лет (!!!) висит незакрытым критический баг Incorrect parsing of HTML4 optional end tags, который приводит к некорректной обработке тегов HTML, закрывающие теги для которых опциональны. В 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));
AngleSharp
Лицензия: BSD (3-clause)
Новый игрок на поле парсеров. В отличие от CsQuery, написан с нуля вручную на C#. Также включает парсеры других языков.
API построен на базе официальной спецификации по JavaScript HTML DOM. В некоторых местах есть странности, непривычные для разработчиков на .NET (например, при обращении к неверному индексу в коллекции будет возвращён null, а не выброшено исключение; есть свой отдельный класс Url; пространства имён очень гранулярные, даже базовое использование библиотеки требует три using и т. п.), но в целом ничего критичного.
Из других странностей — библиотека тащит за собой Microsoft BCL Portability Pack. Поэтому, когда подключите AngleSharp через NuGet, не удивляйтесь, если обнаружите подключенными три дополнительных пакета: Microsoft.Bcl, Microsoft.Bcl.Build, Microsoft.Bcl.Async.
Обработка HTML простая:
IHtmlDocument angle = new HtmlParser(html).Parse();
foreach (IElement element in angle.QuerySelectorAll("a"))
Console.WriteLine(element.GetAttribute("href"));
Она не усложняется, и если нужна более сложная логика:
IHtmlDocument angle = new HtmlParser(html).Parse();
foreach (IElement element in angle.QuerySelectorAll("h3.r a"))
Console.WriteLine(element.GetAttribute("href"));
Regex
Страшные и ужасные регулярные выражения. Применять их нежелательно, но иногда возникает необходимость, так как парсеры, которые строят DOM, заметно прожорливее, чем Regex: они потребляют больше и процессорного времени, и памяти.
Если дошло до регулярных выражений, то нужно понимать, что вы не сможете построить на них универсальное и абсолютно надёжное решение. Однако если вы хотите парсить конкретный сайт, то эта проблема может быть не так критична.
Ради всего святого, не надо превращать регулярные выражения в нечитаемое месиво. Вы не пишете код на C# в одну строчку с однобуквенными именами переменных, так и регулярные выражения не нужно портить. Движок регулярных выражений в .NET достаточно мощный, чтобы можно было писать качественный код.
Например, вот немного доработанный код для извлечения ссылок из вопроса:
Regex reHref = new Regex(@"(?inx)
]*
href \s* = \s*
(? ['""] )
(?
[^>]* >");
foreach (Match match in reHref.Matches(html))
Console.WriteLine(match.Groups["url"].ToString());
Комментариев нет:
Отправить комментарий