Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Data acquisition, часть 4

7 комментариев

В предыдущих частях я описал в общих чертах процесс сбора данных из веб-источников. В этом посте я покажу как сделать общий сервис (generic host) для процессирования различных сайтов с использованием WatiN. Также, я затрону проблему многопоточности в использовании WatiN. Исходники, как всегда, тут.

Generic WatiN host с использованием MEF

Поскольку запускать несколько WatiN-управляемых сервисов опасно, нам нужно проконтролировать этот процесс с помощью сервиса (хоста), которые реализует плаг-ин архитектуру. Для начала, давайте определим некий интерфейс по которому будут работать WatiN-управляемые сервисы:

public abstract class WatinDataAcquisitionService : DataAcquisitionService
{
  /// <summary>
  /// This method must be implemented by any scraping service that needs to
  /// use WatiN.
  /// </summary>
  /// <param name="browser">A preinitialized <c>Browser</c> object
  /// that one can use for scraping.</param>
  /// <remarks>Do not pass the <c>browser</c> object into other
  /// threads or asynchronous operations.</remarks>
  public abstract void AcquireData(Browser browser, ILog log);
}

У нашего интерфейса всего один метод для исполнения скрейпинга. В этот сервис мы передаем уже инициализированный объект типа Browser (это может быть IE или FireFox) а также ссылку на логгер из главного сервиса – это позволяет нам логировать процесс из основного хоста.

Дабы получить все доступные WatiN-сервисы, наш хост использует MEF, декларируя тот факт что он хочет загрузить все объекты типа WatinDataAcquisitionService:

[ImportMany(typeof(WatinDataAcquisitionService))]
public WatinDataAcquisitionService[] WatinServices { get; set; }

Подгрузка доступных сервисов происходит в инициализации самого сервиса. В нашем случае, мы просто находим все DLLки в субдиректории plugins:

cat = new DirectoryCatalog("plugins");
cc = new CompositionContainer(cat);
cc.ComposeParts(this);

Наш стереотипичный метод DoWork() выглядит весьма витьевато. Давайте сначала я его покажу:

private void DoWork()
{
  while (true)
  {
    log.InfoFormat("Found {0} WatiN services", WatinServices.Length);
    if (WatinServices.Length > 0)
      using (var browser = new IE())
      {
        browser.Visible = false;
        foreach (var s in WatinServices)
        {
          using (var timer = new MyTimer(s.GetType().FullName, log))
          {
            // prevent errors from bleeding through
            try
            {
              s.AcquireData(browser, log);
            }
            catch (Exception ex)
            {
              log.Error(
                string.Format("WatiN service {0} threw an exception", s.GetType().FullName),
                ex);
            }
          }
        }
      }
    // do some work, then
    Thread.Sleep(pollingFrequency);
  }
}

Тут происходит несколько вещей – замер времени, запуск сервисов и логирование ошибок на тот случай если их авторы позволяют исключениям пробиваться через менингеальный барьер (надо смотреть Хауса). Поскольку сервисы вызываются последовательно, все они пользуются браузером не мешая друг другу.

Что касается нашего плагина, то все очень просто – это DLLка в которой есть класс(ы) помеченный аттрибутом Export. Примерно вот так:

[Export(typeof(WatinDataAcquisitionService))]
public class PokemonService : WatinDataAcquisitionService
{
  public override void AcquireData(Browser browser, ILog log)
  {
    log.Info("Pokemon service running");
    browser.GoTo("http://www.pokemon.com");
    var doc = new HtmlDocument();
    doc.LoadHtml(browser.Body.OuterHtml);
    var h3 = doc.DocumentNode.SelectNodes("//h3").First();
    log.Info(h3.InnerText);
  }
}

Прелесть MEF в том, что получившуюся DLL можно просто скопировать в папочку plugins и все будет работать. Danger, Will Robinson: зависимости тоже нужно копировать в эту папку или делать ILmerge (второе предпочтительнее).

Серьезно, а что с многопоточностью?

На самом деле, многопоточное использование WatiN конечно возможно – ведь мы можем открыть несколько копий IE одновременно, не так ли? Но не все так просто.

Во-первых, нельзя открыть сразу, скажем, 100 копий IE – что конкретно ломается не понятно (COM исключения такие информативные…), но проблемы гарантированы. С другой стороны, можно открыть например 2*Environment.ProcessorCount копий и все более-менее работает.

Вторая проблема в том, что если использовать, скажем, TPL, то нужно писать свой StaTaskScheduler который будет создавать STA-потоки вместо MTA. К счастью, такое решение уже было в сети (на MSDN), и я вставил его в примеры. Вот пример того, как можно запустить по 4 копии IE каждый раз:

var po = new ParallelOptions();
po.TaskScheduler = new StaTaskScheduler(4);
Parallel.For(0, 100, po, x =>
{
  using (var browser = new IE("http://news.bbc.co.uk"))
  {
    browser.Visible = false;
    var doc = new HtmlDocument();
    doc.LoadHtml(browser.Body.OuterHtml);
    var h3 = doc.DocumentNode.SelectNodes("//h3").First();
    Console.WriteLine(h3.InnerText);
  }
});

По аналогии с этим подходом, наш хост-сервер может открывать не один браузер, а иметь целый пул из, скажем, 10 браузеров которые могут выборочно передаваться в подконтрольные сервисы.

Реклама

Written by Dmitri

29 мая 2010 в 12:55

Опубликовано в .NET

комментариев 7

Subscribe to comments with RSS.

  1. Пробовал использовать WatiN для сбора данных с сайтов, однако в конечном счете отказался от этого решения.

    Нашел более удобное для меня решение в виде связки HtmlUnit + IKVM:
    http://blog.stevensanderson.com/2010/03/30/using-htmlunit-on-net-for-headless-browser-automation/

    Алексей Диян

    30 мая 2010 at 23:22

    • Класс, да, на самом деле у WatiN уйма минусов и работать с ним сложно. Но что-то мне подсказывает что использование движка Java в C# тоже дело непростое…

      Dmitri

      1 июня 2010 at 17:40

      • По своему опыту скажу что такая казалось бы непростая связка работает намного стабильнее чем WatiN.

        На самом деле из реалистичных недостатков я бы выделил следующие:

        1. Достаточно немалый размер библиотек IKVM + HtmlUnit. Все зависимости занимают около 21 МБ. Возможно в каких-то случаях это будет недопустимо много.

        2. Сложность отладки скриптов несколько сложнее чем у WatiN, так как ты реального браузера не существует и разработчику не видно что сейчас находится на экране.
        В качестве отладочной информации можно получить содержимое страницы в формате Html.
        Если этой информации недостаточно, то можно запросить страницу целиком, вместе с картинками, но тогда кол-во зависимостей библиотек IKVM увеличивается еще где-то мегабайт на 6.
        Лично я обошелся минимальными средствами — сохранял текущую страницу в текстовый файл во время ошибки выполнения сценария.

        3. API один в один как это выглядит в Java с соответствующими naming conventions. Естественно что там отсутствует синтаксический сахар C# 2.0/3.0.

        В целом я бы назвал эту связку не самым изящным, но достаточно надежным решением которое решает поставленные задачи.

        Alexey Diyan

        3 июня 2010 at 14:03

    • Тут на самом деле есть такая проблема что некоторые сайты отказываются работать на всем кроме IE. А HtmlUnit это ведь не «родной» браузер, следовательно вообще работать не будет?

      Dmitri

      5 июня 2010 at 22:26

  2. А в многопоточном режиме эта связка будет работать? И через разные прокси?)

    yiith

    7 июня 2010 at 3:20

    • @yiith

      Откровенно говоря, в многопоточных сценариях я не пробовал использовать HtmlUnit/IKVM, но с большой долей вероятности можно сказать что эта связка работать таки будет.

      На официальном сайте, в разделе Getting Started видно, что API поддерживает прокси сервера:
      http://htmlunit.sourceforge.net/gettingStarted.html

      @Dmitri
      В Getting Started говорится, что:
      Specifying this BrowserVersion will change the user agent header that is sent up to the server and will change the behavior of some of the JavaScript.

      …т.е. определенные возможности в плане эмуляции работы браузера IE есть, но я бы сильно на это не рассчитывал.

      В любом случае есть масса вещей, которые работают в IE и не работают в других браузерах. Эта библиотека к сожалению не подходит в подобных сценариях.

      Alexey Diyan

      7 июня 2010 at 17:42

      • Существует масса сайтов где требуется ждать пока отработает javascript/ajax. Поэтому полновесный браузер — часто единственный вариант, хоть и уж очень неудобный. Зато любая возможность просто использовать WebRequest — праздник на нашей улице :)

        Dmitri

        7 июня 2010 at 19:48


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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: