Страницы

Поиск по вопросам

воскресенье, 24 ноября 2019 г.

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


Необходимо извлечь все 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?
И так далее.


Регулярное выражение очень быстро становится монструозным и нечитаемыми, а проблемны
мест обнаруживается всё больше и больше.

Что делать?
    


Ответы

Ответ 1



Регулярные выражения предназначены для обработки относительно простых текстов, которы задаются регулярными языками. Регулярные выражения со времени своего появления сильн усложнились, особенно в Perl, реализация регулярных выражений в котором является вдохновение для остальных языков и библиотек, но регулярные выражения всё ещё плохо приспособлен (и вряд ли когда-либо будут) для обработки сложных языков типа HTML. Сложность обработк HTML заключается ещё и в очень сложных правилах обработки невалидного кода, которы достались по наследству от первых реализаций времён рождения Интернета, когда никаки стандартов не было и в помине, а каждый производитель браузеров нагромождал уникальны и неповторимые возможности. Итак, в общем случае регулярные выражения — не лучший кандидат для обработки HTML Обычно разумнее использовать специализированные парсеры HTML. CsQuery Лицензия: MIT Один из современных парсеров HTML для .NET. В качестве основы взят парсер validator.n для Java, который в свою очередь является портом парсера из движка Gecko (Firefox) Это гарантирует, что парсер будет обрабатывать код точно так же, как современные браузеры. API черпает вдохновение у jQuery, для выбора элементов используется язык селекторо CSS. Названия методов скопированы практически один-в-один, то есть для программистов знакомых с jQuery, изучение будет простым. Обладает высокой производительностью. На порядки превосходит HtmlAgilityPack+Fizzle по скорости на сложных запросах. 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 parsin 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* (? ['""] ) (? [^""]+ ) \k [^>]* >"); foreach (Match match in reHref.Matches(html)) Console.WriteLine(match.Groups["url"].ToString());

Ответ 2



Используйте библиотеку CefSharp для решения подобных задач. Почему следует применять именно такой подход? У вас намного упрощается процесс разработки за счёт того, что вместо написания XPath, условий и/или циклов в C# вы просто в консоли браузера (желательно основанного на Chromium) просто разрабатываете всё что вам нужно, затем когда уже написан небольшой костяк из класса (покажу его ниже), вы просто вставляете JavaScript-код, который вам нужен. Надёжность. Вы не пытаетесь парсить HTML и не изобретаете велосипед, что являетс почти всегда очень плохой идеей. Проект основан на Chromium, поэтому вам не приходитс доверять какому-то новому/незнакомому продукту. Активно поддерживается для синхронизаци с новой версией. Для Javascript-обращений для простоты и демонстрации используется jQuery, предполагая что на целевом сайте он тоже есть. Но это может быть также чистый JavaScript либо друга библиотека при условии, что эта библиотека используется на сайте. Если вы проскроллите вниз, то заметите, что помимо написания небольшой прослойк кода и инициалиации, решение занимает одну-две строки: string[] urls = await wrapper.GetResultAfterPageLoad("https://yandex.ru", async () => await wrapper.EvaluateJavascript( "$('a[href]').map((index, element) => $(element).prop('href')).toArray()")); Что это такое? Это управляемая оболочка над CEF (Chromium Embedded Framework). То есть Вы получает мощь Chromium, которой управляете программно. Почему именно CEF/CefSharp? Не стоит заморачиваться парсингом страниц (а это сложная и неблагодарная задача которую крайне не рекомендую делать). Можно работать с уже загруженной страницей (после выполнения скриптов). Есть возможность выполнять произвольный JavaScript с последними возможностями. Даёт возможность вызывать AJAX с помощью JavaScript, а затем при успехе (success) дёргать события в C#-коде с результатом AJAX. Подробно и с примером рассмотрел здесь. Разновидности CefSharp CefSharp.WinForms CefSharp.Wpf CefSharp.OffScreen Первые две используются если вам надо дать пользователям элемент управления "Браузер" Концептуально похоже на WebBrowser в Windows Forms, который является оболочкой для управлени IE, а не Chromium, как в нашем случае. Поэтому мы будем использовать CefSharp.OffScreen (закадровую) разновидность. Написание кода Допустим у нас консольное приложение, но это уже зависит от Вас. Устанавливаем Nuget-пакет CefSharp.OffScreen 57-ой версии: Install-Package CefSharp.OffScreen -Version 57.0.0 Дело в том, что C# всё массивы маппает к List, результат JavaScript обёрну в object, в котором уже содержатся List, string, bool, int в зависимости о результата. Для того чтобы сделать результаты строго типизированными, создаём небольшо ConvertHelper: public static class ConvertHelper { public static T[] GetArrayFromObjectList(object obj) { return ((IEnumerable)obj) .Cast() .ToArray(); } public static List GetListFromObjectList(object obj) { return ((IEnumerable)obj) .Cast() .ToList(); } public static T ToTypedVariable(object obj) { if (obj == null) { dynamic dynamicResult = null; return dynamicResult; } Type type = typeof(T); if (type.IsArray) { dynamic dynamicResult = typeof(ConvertHelper).GetMethod(nameof(GetArrayFromObjectList)) .MakeGenericMethod(type.GetElementType()) .Invoke(null, new[] { obj }); return dynamicResult; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { dynamic dynamicResult = typeof(ConvertHelper).GetMethod(nameof(GetListFromObjectList)) .MakeGenericMethod(type.GetGenericArguments().Single()) .Invoke(null, new[] { obj }); return dynamicResult; } return (T)obj; } } Для обработки с ошибками Javascript создаём класс JavascriptException. public class JavascriptException : Exception { public JavascriptException(string message) : base(message) { } } У вас может быть свой способ обработки ошибок. Создаём класс CefSharpWrapper: public sealed class CefSharpWrapper { private ChromiumWebBrowser _browser; public void InitializeBrowser() { Cef.EnableHighDPISupport(); // Perform dependency check to make sure all relevant resources are in ou output directory. Cef.Initialize(new CefSettings(), performDependencyCheck: false, browserProcessHandler null); _browser = new ChromiumWebBrowser(); // wait till browser initialised AutoResetEvent waitHandle = new AutoResetEvent(false); EventHandler onBrowserInitialized = null; onBrowserInitialized = (sender, e) => { _browser.BrowserInitialized -= onBrowserInitialized; waitHandle.Set(); }; _browser.BrowserInitialized += onBrowserInitialized; waitHandle.WaitOne(); } public void ShutdownBrowser() { // Clean up Chromium objects. You need to call this in your applicatio otherwise // you will get a crash when closing. Cef.Shutdown(); } public Task GetResultAfterPageLoad(string pageUrl, Func> onLoadCallback) { TaskCompletionSource tcs = new TaskCompletionSource(); EventHandler onPageLoaded = null; T t = default(T); // An event that is fired when the first page is finished loading. // This returns to us from another thread. onPageLoaded = async (sender, e) => { // Check to see if loading is complete - this event is called twice one when loading starts // second time when it's finished // (rather than an iframe within the main frame). if (!e.IsLoading) { // Remove the load event handler, because we only want one snapsho of the initial page. _browser.LoadingStateChanged -= onPageLoaded; t = await onLoadCallback(); tcs.SetResult(t); } }; _browser.LoadingStateChanged += onPageLoaded; _browser.Load(pageUrl); return tcs.Task; } public async Task EvaluateJavascript(string script) { JavascriptResponse javascriptResponse = await _browser.GetMainFrame().EvaluateScriptAsync(script); if (!javascriptResponse.Success) { throw new JavascriptException(javascriptResponse.Message); } } public async Task EvaluateJavascript(string script) { JavascriptResponse javascriptResponse = await _browser.GetMainFrame().EvaluateScriptAsync(script); if (javascriptResponse.Success) { object scriptResult = javascriptResponse.Result; return ConvertHelper.ToTypedVariable(scriptResult); } throw new JavascriptException(javascriptResponse.Message); } } Далее вызываем наш класс CefSharpWrapper из метода Main. public class Program { private static void Main() { MainAsync().Wait(); } private static async Task MainAsync() { CefSharpWrapper wrapper = new CefSharpWrapper(); wrapper.InitializeBrowser(); string[] urls = await wrapper.GetResultAfterPageLoad("https://yandex.ru" async () => await wrapper.EvaluateJavascript("$('a[href]').map((index element) => $(element).prop('href')).toArray()")); wrapper.ShutdownBrowser(); } } Также: в данной библиотеке есть особенность, что пустой JavaScript-массив приводитс к null. Поэтому, возможно, есть смысл добавить в ConvertHelper соотвествующий код (эт зависит от вашего кода и потребностей), либо в вызывающем коде писать что-то вроде if (urls == null) urls = new string[0] Также установите x64 или x86 в качестве платформы. Платформа Any CPU поддерживается но требует дополнительного кода.

Ответ 3



Если требования к производительности не очень высокие, можно использовать COM-объек Internet Explorer (добавить ссылку на Microsoft HTML Object Library): public static List ParseLinks(string html) { List res = new List(); mshtml.HTMLDocument doc = null; mshtml.IHTMLDocument2 d2 = null; mshtml.IHTMLDocument3 d = null; try { doc = new mshtml.HTMLDocument();//инициализация IE d2 = (mshtml.IHTMLDocument2)doc; d2.write(html); d = (mshtml.IHTMLDocument3)doc; var coll = d.getElementsByTagName("a");//получить коллекцию элементов п имени тега object val; foreach (mshtml.IHTMLElement el in coll)//извлечь атрибут href из всех элементов { val=el.getAttribute("href"); if (val == null) continue; res.Add(val.ToString()); } } finally { //освобождение ресурсов if (doc != null) Marshal.ReleaseComObject(doc); if (d2 != null) Marshal.ReleaseComObject(d2); if (d != null) Marshal.ReleaseComObject(d); } return res; }

Ответ 4



Вставлю свои пять копеек, если нет желания возиться с COM-объектами mshtml, можн создать объект WebBrowser() из Windows.Forms, причём, если вам не нужно срабатывани всех скриптов, то, я так понимаю, страницу можно грузить не самим браузером, а чем попроще вроде WebClient.DownloadString(), а далее загружаем полученный текст страницы для парсинг в WebBrowser: var itemPageText = _webClient.DownloadString(url); using (var pageHtml = new WebBrowser()) { pageHtml.DocumentText = itemPageText; var elem = pageHtml.Document.GetElementById("imainImgHldr"); } ну и т.п., главное, что методы вроде GetElementById() тоже представляют собой нескольк более удобоваримые обёртки в отличие от mshtml.

Ответ 5



F# Поиск на странице всех ссылок на книги по F#: let fsys = "https://www.google.com/search?tbm=bks&q=F%23" let doc2 = HtmlDocument.Load(fsys) let books = doc2.CssSelect("div.g h3.r a") |> List.map(fun a -> a.InnerText().Trim(), a.AttributeValue("href")) |> List.filter(fun (title, href) -> title.Contains("F#")) F# Data F# Data HTML Parser F# Data HTML CSS selectors

Ответ 6



У меня все замечательно получается при помощи XElement Попробуйте :) var htmlDom = XElement.Parse("[Код HTML]"); Как подсказали в комментариях, это будет работать если нужная нам страница являетс валидным XHTML документом.

Комментариев нет:

Отправить комментарий