113

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

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

Что делать?

mymedia
  • 8,602
Kyubey
  • 32,103
  • Я бы заменил метку [tag:.net] на [tag:faq]. – VladD May 01 '15 at 10:51
  • 2
    @VladD Если ответ подробный, он не становится FAQ. В моём понимании FAQ — это когда вопрос на грани нарушения правил за всеобщность, а ответ — компиляция десятков ответов разных юзеров (а.к.а. вики). Тут же простой вопрос и простой ответ. Если добавится ещё какая-то библиотека, то лучше, если она будет отдельным ответом, скажем. Ну и тег .NET мне жалко было бы терять — всё-таки .NET не ограничивается шарпом и вбасиком. – Kyubey May 01 '15 at 10:58
  • На мой вкус, [tag:faq] как раз и есть ответ на часто задаваемый вопрос. Вопросы типа «как найти X в HTML сайта Y при помощи регулярки Z» всплывают регулярно. – VladD May 01 '15 at 11:02
  • @VladD Если формулировать критерий таким образом, то тег [tag:faq] становится мета-тегом и должен быть удалён, потому что для определения популярности есть другие инструменты. :) – Kyubey May 01 '15 at 11:07
  • Возможно. Мне, например, этот тег удобен как список вопросов, которые нужно указывать в качестве дубликата. – VladD May 01 '15 at 11:08
  • Ну да, это неявно и не гарантирует успех. Мне больше нравится явное обозначение интенции. Впрочем, вопрос ваш, вы хозяин. – VladD May 01 '15 at 11:26
  • @VladD У меня способности к предсказанию будущего отсутствуют, поэтому предпочитаю, когда не мне нужно предсказывать, что что-то будет целью для закрытия для дубликата, а система сама показывает, что реально используется таким образом. :) – Kyubey May 01 '15 at 11:29
  • 10
    Я просто обязан положить это здесь http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454 – Alex May 06 '15 at 20:01
  • 1
  • ассоциация: https://stackoverflow.com/questions/56107/ – MSDN.WhiteKnight Jun 10 '22 at 05:50

6 Answers6

137

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)
    <a \s [^>]*
        href \s* = \s*
            (?<q> ['""] )
                (?<url> [^""]+ )
            \k<q>
    [^>]* >");
foreach (Match match in reHref.Matches(html))
    Console.WriteLine(match.Groups["url"].ToString());
Kyubey
  • 32,103
  • 3
    Я бы еще добавил Silenium прежде всего как построитель DOM в заскриптованных таблицах http://www.seleniumhq.org/docs/05_selenium_rc.jsp#c http://scraping.pro/example-of-scraping-with-selenium-webdriver-in-csharp/

    Для того что бы добраться до сформированного DOM можно использовать вместо PageSource вычисляемый скрипт

    http://stackoverflow.com/questions/26584215/selenium-page-source-does-not-return-modified-dom-tree

    var pageSource = (string)driver.ExecuteScript("return document.body.outerHTML");

    – Serginio Apr 26 '16 at 07:13
  • @Serginio Если использовать outerHTML, то Селениум будет сторонним инструментом для получения HTML, а парсинг всё равно делать парсерами, про которые здесь и идёт речь. Вот если использовать напрямую — да, получается DOM как после парсера, два в одном. Селениум умеет только XPath или CSS-запросы тоже? – Kyubey Apr 26 '16 at 10:56
  • https://kreisfahrer.gitbooks.io/selenium-webdriver/content/webdriver_api_slozhnie_vzaimodeistviya/lokatori_css,_xpath,_jquery.html можно через тот же ExecuteScript. Я к тому, что столкнулся с заскриптованными сайтами. AngleSharp поддержка JS недоделана. Другие не пробовл. – Serginio Apr 26 '16 at 11:54
  • 1
    Вот еще ссылка http://www.vcskicks.com/selenium-jquery.php – Serginio Apr 26 '16 at 12:03
  • Еще ссылка http://selenium2.ru/docs/webdriver.html Chrome Driver

    Chrome Driver разрабатывается и поддерживается участниками проекта Chromium

    Плюсы ◾Запуск тестов в реальном браузере и поддержка JavaScript ◾Так как Chrome базируется на движке Webkit, это позволит убедиться, что веб-сайт с известной долей вероятности работает и в браузере Safari. Однако учтите, что, поскольку Chrome использует интерпретатор JavaScript V8, а Safari - Nitro, исполнение JavaScript может отличаться.

    – Serginio Apr 26 '16 at 12:25
  • 1
    @Serginio Говорить про то, что в "AngleSharp поддержка JS недоделана", пожалуй, не совсем честно, потому что он вряд ли собирается мутировать в полноценный браузер. :) Я вообще не понимаю, зачем автору сдалось прикручивать к парсеру жабоскрипт, потому что ничего полезного с ним не сделаешь. Если возникнет необходимость эмулировать браузер, то возьмут явно не его урезанный proof-of-concept. // И повторюсь: парсер HTML — это штука, которая разирает вполне конкретный язык. Эмулятор браузера — гораздо более мощная и высокоуровневая вещь, разбор текста без DOM — более слабая и низкоуровневая. – Kyubey Apr 26 '16 at 14:42
  • @Serginio Собственно, если вы столкнулись с динамическим сайтом, то решить задачу можно на любом уровне, вопрос только в сложности и затрате вычислительных ресурсов: если парсить регулярками, то придётся влезать в потроха сайта, разбираться в выполняемых жабоскриптом запросах и преобразованиях, выполнять нужные вам; если взять селениум, то браузер съест тонны ресурсов на абсолютно ненужные вам задачи, но в конце концов выдаст DOM ровно в том виде, как его видит посетитель сайта, и позволит взаимодействовать с элементами на сайте. Разные уровни, разные подходы, разная эффективность. – Kyubey Apr 26 '16 at 14:42
  • @Serginio ОК, селекторы CSS имеются, отлично. Ещё и jQuery можно прикрутить. Будет время — добавляю в свой ответ вариант. Впрочем, вы сами можете добавить ещё один ответ. С селениумом много нюансов и вариантов использования, неплохо бы вкратце описать, что к чему. – Kyubey Apr 26 '16 at 14:43
  • Я сам сегодня в первый раз им занялся. Самому хотелось бы разобраться на будущее. Пока AngleSharp вполне хватает. Но есть заскрипованные сайты, где лень искать запросы фиддлером или поиском в JS. – Serginio Apr 26 '16 at 15:06
  • @Serginio Фиддлер — это чересчур сурово, по-моему. :) Жабоскрипт и запросы гораздо приятнее отлаживать и анализироать прямо в браузере, там всё сразу вместе: и дебаггер, и запросы, и дом, и консоль, и всё остальное. – Kyubey Apr 26 '16 at 15:19
  • 1
    Ну пока я еще не во всем разобрался. Я же 1С ник http://infostart.ru/profile/82159/public/ – Serginio Apr 27 '16 at 07:16
  • Спасибо за наводку на счет браузера. Сейчас изучаю все возможности. – Serginio Apr 27 '16 at 07:31
  • Я не согласен с отзывом об HtmlAgilyPack, он мне кажется слишком эмоциональным и субъективным. Для подавляющего числа задач этот пакет вполне подходит и исправно работает. Много где использую, проблем не испытывал. Достаточно быстрый и легковесный. – aepot Feb 15 '22 at 21:45
  • @aepot Если вам нравится кривой инструмент, то пользуйтесь на здоровье. Только признайтесь себе, что пользуетесь им не за какие-либо достоинства на фоне конкуренции, а исключительно в силу привычки. – Kyubey Feb 16 '22 at 22:42
  • @Kyubey оно просто работает. А то что у вас что-то пошло не так, это не значит что каждый обречен. Мне даже кажется, что возможно вы что-то не так делаете или бьете гвозди микроскопом, непонятно. Наверное стоит быть сдержаннее. Сюда приходят новички, которым чтобы добраться до ваших проблем, надо многое пройти, а вы им сразу в лицо такое выбрасываете. Смысл в этом вообще есть? Объясните объективно, что работает, а что нет от этого будет больше пользы чем от 100% фокуса на багах и недоработках. Пока кажется, что вообще ничего не работает, но это же не так. – aepot Feb 17 '22 at 01:30
  • @aepot Я предельно ясно объяснил проблему с HAP: она некорректно обрабатыввает корректные входные данные, которые встречаются довольно часто. Это ломает раунд-трип десериализации-сериализации, это выдаёт неверные данные, это ломает форматирование и последующий рендеринг. Если бы у библиотеки было хоть одно преимущество над конкуренцией (скорость, память, активная разработка, удобный API), ещё можно было бы обсуждать варианты, но пока я от вас не услышал ни одного весомого аргумента в пользу HAP кроме "а мне сойдёт". Ну а что баг не могут 10 лет исправить — банально признак заброшенности. – Kyubey Feb 17 '22 at 11:35
  • @aepot Просто наконец задайтесь вопросом: ПОЧЕМУ. Почему вы используете HAP? Есть ли хоть одна причина использовать библиотеку с багами в парсинге и с кривым API, если есть альтернатива, в которой более адекватный API, нет багов в парсинге, которая активно развивается и поддерживает больше функционала? Новичков, кстати, в первую очередь надо отучать от следования по стопам предков, которые изучили HAP 15 лет назад и с тех пор больше ничего не пробовали. HAP заброшен. Это как с либой CommandLine — это одна из худших библиотек с абс-но неподдерживаемым кодом, но ей пользуются по старой привычке. – Kyubey Feb 17 '22 at 11:41
  • Ничего не понимаю, обновлялась совсем недавно, задачи саои выполняет исправно. ПОЧЕМУ я должен что-то менять? https://github.com/zzzprojects/html-agility-pack 13 дней назад коммит был, мы точно про одно и то же говорим? Еще у вас ссылка битая а ответе про баги. Видимо вы действительно не в курсе актуальности пакета. – aepot Feb 17 '22 at 11:48
  • @aepot За последний год я вижу 7 коммитов, которые НЕ "update readme", "update source", "update issue template". Всего 400 коммитов, 2/3 из которых с мусорными комментариями. Теперь идём в https://github.com/AngleSharp/AngleSharp. 6400 коммитов, 2021-й год кончается где-то на 6-й странице коммитов, считать лень, там сотни, и все с осмысленными комментариями. HAP — легаси, и код поддерживается как легаси. AngleSharp — активная разработка. – Kyubey Feb 17 '22 at 11:59
  • @aepot Если хотите добавить веса вашим словам, пойдите и сравните библиотеки, как они себя проявляют в корректности (раунд-трип, тесты), скорости (скорость на входных данных разной сложности с запросами разной сложности), памяти (сколько расходуется всего, какой траффик сборки мусора), соответствия API официальным гайдлайнам дизайна API (есть такой документ), активности разработки (коммиты, пулл-реквесты, баг-репорты), разнообразию функционала и т.п. Если найдёте хоть одну причину использовать HAP, буду рад вас выслушать. А выслушивать бесконечные "а мне и так сойдёт" мне откровенно надоело. – Kyubey Feb 17 '22 at 11:59
  • Так я же не пытаюсь их сравнивать AngleSharp - отличная библиотека, я ее использую вплоть до запуска JS. Но и сказать что HAP не может нормально парсить обычный HTML не могу. XHTML может и не может, но мне это не надо. – aepot Feb 17 '22 at 12:18
  • В версии AngleSharp 0.10 изменили API, код из этого ответа не работал. Я решил обновить его, если есть возражения, откатите правку и напишите мне. – MSDN.WhiteKnight Aug 06 '23 at 13:19
24

Используйте библиотеку 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 поддерживается, но требует дополнительного кода.

Nick Volynkin
  • 34,094
  • 9
    Вы бы о недостатках подхода тоже написали: запускать полноценный браузерный движок в 100 раз медленнее, чем парсить DOM, и в 1000 раз медленнее, чем парсить регулярками. :) Ну и с бинарниками веселья заметно добавляется. Имеет смысл использовать разве что на полностью динамических сайтах, в потрохах которых лень разбираться, и в прочих хардкорных случаях. – Kyubey Nov 28 '16 at 11:29
  • Абсолютно не согласен по поводу производительности. Не замечал никаких ощутимой разницы между парсингом HtmlAgilityPack, регулярками и моим способом. – Vadim Ovchinnikov Nov 28 '16 at 11:38
  • По поводу использования, я бы даже для текстового анализа очень простых сайтов применял, потому что после написания пары маленьких классов C# вы просто программируете на JavaScript в консоли и затем ваш код переносите в программу. Это намного удобней и наглядней XPath, а также циклов и фильтров в C#. – Vadim Ovchinnikov Nov 28 '16 at 11:40
  • Если вы не замечали разницы, значит, вы не меряли ни скорость, ни расход памяти, ни расход траффика. Распарсить 10 страничек — разницы нет, конечно, но вот попробуйте 10000 страничек какого-нибудь тумблера распарсить — посмотрим, что получится. :) – Kyubey Nov 28 '16 at 11:48
  • Как раз-таки я его и применял для парсинга десятков, если не сотен тысяц страниц. Скорость программы полностью определялась скоростью интернета. – Vadim Ovchinnikov Nov 28 '16 at 11:49
  • 2
    Почему на произвольной странице должен оказаться jQuery? – Qwertiy Nov 28 '16 at 12:24
  • Из ответа: "Для Javascript-обращения для простоты используется jQuery, предполагая, что на целевом сайте он тоже есть. Но это может быть также чистый JavaScript либо другая библиотека при условии, что эта библиотека используется на сайте." – Vadim Ovchinnikov Nov 28 '16 at 12:25
  • Но видимо следует вынести этот текст выше. – Vadim Ovchinnikov Nov 28 '16 at 12:27
  • А можете привести пример того, как код на js может быть удобнее и нагляднее кода на C#? – VladD Dec 29 '16 at 22:50
  • @VladD Конечно. Код в примере $('a[href]').map((index, element) => $(element).prop('href')).toArray() и так почти любой JavaScript удобней и наглядней кода на C# просто потому, что он не зависит от библиотеки C#, каждая из которой предлагает свой специфический способ сделать то же самое. Причём для чего-то не такого тривиального приходится изобретать велосипеды. – Vadim Ovchinnikov Dec 29 '16 at 23:00
  • @VladD Более наглядный пример. Надо просто извлечь текст всего блока или документа. Решается, к примеру для текста всего документа document.documentElement.innerText. Для конкретного блока решается сложней. – Vadim Ovchinnikov Dec 29 '16 at 23:04
  • 1
    @VadimOvchinnikov: Ну, вы вместо библиотек на C#, каждая из которых предлагает свой синтаксис, используете библиотеки на JS, каждая из которых тоже предлагает собственный синтаксис. У вас, например, это JQuery, а не чистый JS. – VladD Dec 30 '16 at 15:30
  • @VladD Тут большая разница. Вы можете использовать библиотеки есть они есть. А в библиотеках C# вы ограничены только тем API, которое есть. Плюс в большинстве случаев разработчики знают JavaScript, а вникать в нюансы конкретной библиотеки сложней, чем просто написать JavaScript для большинства. Я имею ввиду часть, которая связана с тем, чтобы просто вытащить всё что вам нужно со страницы. – Vadim Ovchinnikov Dec 30 '16 at 15:49
  • 1
    @VadimOvchinnikov: Честно говоря, не вижу большой разницы. Вы можете точно так же использовать и библиотеки на C#, если они есть. Плюс вот я, например, не знаю JS, а знаю C# — зачем знать и использовать два языка, если и один справляется? – VladD Dec 30 '16 at 15:58
  • Vadim Ovchinnikov дайте полный код ваших советов – EgorVB.net Feb 28 '17 at 18:57
  • 1
    @EgorVB.net А в чём данный код не полон? Я и так дал всё, что мог. – Vadim Ovchinnikov Mar 01 '17 at 06:18
8

Если требования к производительности не очень высокие, можно использовать 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.designMode = &quot;On&quot;;
    d2.write(html);

    d = (mshtml.IHTMLDocument3)doc;
    var coll = d.getElementsByTagName(&quot;a&quot;);//получить коллекцию элементов по имени тега
    object val;

    foreach (mshtml.IHTMLElement el in coll)//извлечь атрибут href из всех элементов
    {
        val=el.getAttribute(&quot;href&quot;);
        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;

}

3

Вставлю свои пять копеек, если нет желания возиться с 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.

Mic
  • 31
2

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

Anatol
  • 3,746
-4

У меня все замечательно получается при помощи XElement Попробуйте :)

var htmlDom = XElement.Parse("[Код HTML]");

Как подсказали в комментариях, это будет работать если нужная нам страница является валидным XHTML документом.

iRumba
  • 5,946
  • 4
    Нет, это вы попробуйте: XElement.Parse("<html><body><ul class=foo><li><input type=checkbox checked>Hello, world!<li>Second line"); – Pavel Mayorov Sep 14 '15 at 05:37
  • @PavelMayorov, что это такое? Где закрывающие теги? – iRumba Sep 14 '15 at 05:39
  • 4
    в HTML разрешается не закрывать тэги, не ставить кавычки вокруг значения атрибута - и даже не указывать это самое значение если оно булево. – Pavel Mayorov Sep 14 '15 at 05:43
  • 1
    С другой стороны, в XML любой тэг можно "самозакрыть", в то время как в HTML некоторые тэги обязаны быть закрыты явно (к примеру, script). Поэтому использовать XML парсер для разбора HTML - очень плохая идея. – Pavel Mayorov Sep 14 '15 at 05:44
  • @PavelMayorov, да ладно! Если браузер отображает - это не значит, что разрешается. Вам с вашей строкой вот сюда https://validator.w3.org/#validate_by_input – iRumba Sep 14 '15 at 05:45
  • @PavelMayorov если уж мы парсим HTML онлайн, то можно код пропустить через тот же ресурс для Clean-up. Он превратит вашу строку в валидный HTML вида ` `, который уже легко распарсится при помощи `XElement` – iRumba Sep 14 '15 at 05:48
  • 6
    где вы в интернете видели полностью валидные страницы? А ведь автору надо парсить реальные страницы, а не сферические в вакууме... – Pavel Mayorov Sep 14 '15 at 05:55
  • @PavelMayorov да полно. Смысл парсить страницы, написанные первоклассником на коленке? Что с них можно взять кроме расписания уроков? К тому же в сети куча сервисов для Cleanup грязного кода. Пропускаете вашу строчку через них и получаете красивый валидный HTML – iRumba Sep 14 '15 at 05:58
  • 8
    А какой смысл пользоваться сторонним сервисом, если можно использлвать полноценный HTML парсер? – Pavel Mayorov Sep 14 '15 at 06:05
  • @PavelMayorov, мы все равно тянем HTML из сети. Логичнее тянуть правильный. К тому же есть пространство имен Microsoft.mshtml. Если не хотите валидный html, то это то, что нужно. Качать готовые решения, или изобретать велосипед, когда все уже придумано и доступно - глупо. – iRumba Sep 14 '15 at 07:31
  • 8
    @iRumba Вы не поняли юмора. Закрытие многие тегов в HTML является опциональным, незакрытые теги не приведут к ошибке валидации. Вот если страница XHTML — это да, парсер XML с ней справится, вот только таких страниц мало. – Kyubey Sep 14 '15 at 13:03
  • 1
    @iRumba А можете отредактировать ваш ответ, написав, что это будет работать если нужная нам страница валидный XHTML? – Vadim Ovchinnikov Dec 30 '16 at 08:35