Как распарсить HTML в .NET?
Необходимо извлечь все URL из атрибутов
href
теговa
в HTML странице. Я попробовал воспользоваться регулярными выражениями:Uri uri = new Uri("http://google.com/search?q=test"); Regex reHref = new Regex(@"<a[^>]+href=""([^""]+)""[^>]+>"); string html = new WebClient().DownloadString(uri); foreach (Match match in reHref.Matches(html)) Console.WriteLine(match.Groups[1].ToString());
Но возникает множество потенциальных проблем:
- Как отфильтровать только специфические ссылки, например, по CSS классу?
- Что будет, если кавычки у атрибута другие?
- Что будет, если вокруг знака равенства пробелы?
- Что будет, если кусок страницы закомментирован?
- Что будет, если попадётся кусок JavaScript?
- И так далее.
Регулярное выражение очень быстро становится монструозным и нечитаемыми, а проблемных мест обнаруживается всё больше и больше.
Что делать?
Answer 420355
Регулярные выражения предназначены для обработки относительно простых текстов, которые задаются регулярными языками. Регулярные выражения со времени своего появления сильно усложнились, особенно в 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) <a \s [^>]* href \s* = \s* (?<q> ['""] ) (?<url> [^""]+ ) \k<q> [^>]* >"); foreach (Match match in reHref.Matches(html)) Console.WriteLine(match.Groups["url"].ToString());
Answer 450586
У меня все замечательно получается при помощи
XElement
Попробуйте :)var htmlDom = XElement.Parse("[Код HTML]");
Как подсказали в комментариях, это будет работать если нужная нам страница является валидным XHTML документом.
Answer 596363
Source - by Vadim Ovchinnikov
Используйте библиотеку CefSharp для решения подобных задач.
Почему следует применять именно такой подход?
- У вас намного упрощается процесс разработки за счёт того, что вместо написания XPath, условий и/или циклов в C# вы просто в консоли браузера (желательно основанного на Chromium) просто разрабатываете всё что вам нужно, затем когда уже написан небольшой костяк из класса (покажу его ниже), вы просто вставляете JavaScript-код, который вам нужен.
- Надёжность. Вы не пытаетесь парсить HTML и не изобретаете велосипед, что является почти всегда очень плохой идеей. Проект основан на Chromium, поэтому вам не приходится доверять какому-то новому/незнакомому продукту. Активно поддерживается для синхронизации с новой версией.
Для Javascript-обращений для простоты и демонстрации используется jQuery, предполагая, что на целевом сайте он тоже есть. Но это может быть также чистый JavaScript либо другая библиотека при условии, что эта библиотека используется на сайте.
Если вы проскроллите вниз, то заметите, что помимо написания небольшой прослойки кода и инициалиации, решение занимает одну-две строки:
string[] urls = await wrapper.GetResultAfterPageLoad("https://yandex.ru", async () => await wrapper.EvaluateJavascript<string[]>( "$('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<object>
, результат JavaScript обёрнут вobject
, в котором уже содержатсяList<object>
,string
,bool
,int
в зависимости от результата. Для того чтобы сделать результаты строго типизированными, создаём небольшойConvertHelper
:public static class ConvertHelper { public static T[] GetArrayFromObjectList<T>(object obj) { return ((IEnumerable<object>)obj) .Cast<T>() .ToArray(); } public static List<T> GetListFromObjectList<T>(object obj) { return ((IEnumerable<object>)obj) .Cast<T>() .ToList(); } public static T ToTypedVariable<T>(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 our 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 application otherwise // you will get a crash when closing. Cef.Shutdown(); } public Task<T> GetResultAfterPageLoad<T>(string pageUrl, Func<Task<T>> onLoadCallback) { TaskCompletionSource<T> tcs = new TaskCompletionSource<T>(); EventHandler<LoadingStateChangedEventArgs> 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 snapshot 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<T> EvaluateJavascript<T>(string script) { JavascriptResponse javascriptResponse = await _browser.GetMainFrame().EvaluateScriptAsync(script); if (javascriptResponse.Success) { object scriptResult = javascriptResponse.Result; return ConvertHelper.ToTypedVariable<T>(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<string[]>("$('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
поддерживается, но требует дополнительного кода.
Answer 738668
Source - by MSDN.WhiteKnight
Если требования к производительности не очень высокие, можно использовать COM-объект Internet Explorer (добавить ссылку на Microsoft HTML Object Library):
public static List<string> ParseLinks(string html) { List<string> res = new List<string>(); 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; }
Answer 900973
Вставлю свои пять копеек, если нет желания возиться с 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.
Answer 943055
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#"))
Content is retrieved from StackExchange API.
Auto-generated by ruso-archive tools.