Pull to refresh

Data acquisition, часть 4

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

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


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

public abstract class WatinDataAcquisitionService : DataAcquisitionService<br/>
{<br/>
  /// <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);<br/>
}<br/>

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

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

[ImportMany(typeof(WatinDataAcquisitionService))]<br/>
public WatinDataAcquisitionService[] WatinServices { get; set; }<br/>

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

cat = new DirectoryCatalog("plugins");<br/>
cc = new CompositionContainer(cat);<br/>
cc.ComposeParts(this);<br/>

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

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

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

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

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

Прелесть 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();<br/>
po.TaskScheduler = new StaTaskScheduler(4);<br/>
Parallel.For(0, 100, po, x =><br/>
{<br/>
  using (var browser = new IE("http://news.bbc.co.uk"))<br/>
  {<br/>
    browser.Visible = false;<br/>
    var doc = new HtmlDocument();<br/>
    doc.LoadHtml(browser.Body.OuterHtml);<br/>
    var h3 = doc.DocumentNode.SelectNodes("//h3").First();<br/>
    Console.WriteLine(h3.InnerText);<br/>
  }<br/>
});<br/>

По аналогии с этим подходом, наш хост-сервер может открывать не один браузер, а иметь целый пул из, скажем, 10 браузеров которые могут выборочно передаваться в подконтрольные сервисы.
Tags:
Hubs:
Total votes 15: ↑10 and ↓5 +5
Views 1.5K
Comments 0
Comments Leave a comment