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

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

Posts Tagged ‘watin

Data acquisition, часть 1

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

Одно из приемуществ всеобщего удешевления аппаратуры и интернета в том, что сбор информации из разных источников в интернете почти ничего не стоит и может производиться без особых проблем. Задача получения и обработки больших объемов данных является коммерчески превлекательной ввиду спроса на считывание (“скрейпинг”) веб-сайтов со стороны заказчиков (обычно это описывается термином ‘social media analysis’, т.е. анализ социальных медиа). Ну и в принципе это достаточно интересно – по крайней мере по сравнению с рутинной разработкой сайтов, отчетов, и т.д.

В этой статье я начну рассказ про то, как можно реализовать сбор и обработку данных с использованием платформы .Net. Было бы интересно послушать про то как делать то же самое в стеке Java, поэтому если кто-то хочет присоединиться к данной статье в качестве соавтора – милости прошу.

Все исходники находятся тут: http://bitbucket.org/nesteruk/datagatheringdemos

Обзор задачи

Итак, у нас пожалуй самая “размытая” из возможных задач – получение, обработка и хранение данных. Для чтого чтобы получить работующую систему, нам нужно знать

  • Где находятся данные и как к ним правильно обращаться
  • Как обработать данные чтобы получить только то, что нужно
  • Где и как хранить данные

Источники данных

Давайте рассмотрим те источники данных, с которых нужно получать информацию:

  • Форумы
  • Twitter
  • Блоги
  • Новостные сайты
  • Каталоги, листинги
  • Публичные веб-сервисы
  • Прикладное ПО

Сразу хочу подчеркнуть, что веб-браузер не является единственным источником данных. Тем не менее, если работа с веб-сервисами или, скажем, использование API какой-то социальной платформы, является достаточно понятной задачей и не требует много телодвижений, разбор HTML является намного более сложной задачей. И HTML это не предел – порой приходится разбирать JavaScript или даже визуальную информацию с картинок (к пр. для обхода “капчи”).

Другой проблемой является то, что порой контент подгружается динамически через AJAX, что делает нужным разного сорта ‘учет состояний’ для того чтобы получать контент именно тогда, когда он доступен.

Обработка данных

Обработка данных – это самая трудоемкая и дорогостоящая (с точки зрения потенциального заказчика) операция. С одной стороны, может показаться что тот же HTML должен очень просто разбираться существующими средствами, но на самом деле это не так. Во-первых, HTML в большинстве случаев не является XHTML, иначе говоря сделав XElement.Parse() вы попросту получите исключение. Поэтому нужно как минимум иметь возможность “корректировать” плохо написаный HTML.

Даже имея хорошо сформированные данные, у вас все равно будет много проблем – ведь любая более-менее сложная веб-страничк является проекцией многомерной структуры базы данных владельца на одномерное пространство. Восстановление связей и зависимостей является тем самым необходимой задачей для хранения полученной информации в реляционных БД.

Не следует забывать и про более “приземленный” процессинг данных, то есть некие трансформации или произвольные действия над полученными данными. Например, получив IP-адрес вам захочется узнать местоположение или наличие веб-сервера по этому адресу, что потребует дополнительных запросов. Или, скажем, при получении новых данных вам нужно постоянно пересчитывать движимое среднее (streaming OLAP).

Хранение данных

Получив данные, их нужно где-то хранить. Вариантов храниния много – использование сериализации, текстовый файлов, а также объектно- и документно-ориентированных а также конечно реляционных баз данных. Выбор хранища в коммерческом заказе зависит скорее всего либо от заказчика (“мы хотим MySQL”) либо от финансовых предпочтений заказчика. В .Net-разработке базой “по умолчанию” является SQL Server Express. Если же вы делаете хранилище для себя, позволительно использовать все что угодно – будь то MongoDB, db4o или например SQL Server 2008R2 Datacenter Edition.

В большинстве случаев, хранилища данных не требуют особой сложности, т.к. пользователи просто проецируют базу в Excel (ну или SPSS, SAS, и т.п.) а дальше используют привычные методы для анализа. Варианты вроде SSAS (SQL Server Analysis Services) используются намного реже (ввиду минимального ценника в $7500 – см. тут), но знать о них тоже стоит.

Небольшой пример

Давайте посмотрим на минимальный кусочек кода, который поможет нам скачать и “распарсить” страницу. Для этих задач, мы воспользуемся двумя пакетами:

  • WatiN – это библиотека для тестирования веб-интерфейсов. Ее хорошо использовать для автоматизированного нажатия кнопочек, выбора элементов из списка, и подобных вещей. WatiN также предоставляет объектную модель заполученной страницы, но я бы ей не пользовался. Причина в целом одна – WatiN нестабильная и весьма капризная библиотека, которую нужно с опаской использовать (только в 32-битном режиме!) для управления браузером.
  • HTML Agility Pack – библиотека для разбора HTML. Сам HTML можно взять из WatiN, загрузить, и даже если он плохо сформирован, Agility Pack позволит делать в нем поиски и выборки с помощью XPath.

Вот минимальный пример того, как можно использовать два этих фреймворка вместе для того чтобы получить страничку с сайта:

// Загрузка сайта с использованием WatiN + HTML Agility Pack
[STAThread]
static void Main()
{
  using (var browser = new IE("http://www.pokemon.com"))
  {
    var doc = new HtmlDocument();
    doc.LoadHtml(browser.Body.OuterHtml);
    var h1 = doc.DocumentNode.SelectNodes("//h3").First();
    Console.WriteLine(h1.InnerText);
  }
  Console.ReadKey();
}

В примере выше, мы получили страницу через WatiN, загрузили тело страницы в HTML Agility Pack, нашли первый элемент типа H3 и выписали в консоль его содержание.

Поллинг

Наверное для вас очевидно, что запись данных в какое-то хранилище не делается из консольного приложения. В большинстве случаев, для этого используется сервис (windows service). А то чем занимается сервис – это в большинстве случаев поллинг, то есть регулирное скачивание ресурса и обновление нашего представления о нем. Скачивание обычно происходит с интервалом раз в N минут/часов/дней.

// Простой поллинг-сервис
public partial class PollingService : ServiceBase
{
  private readonly Thread workerThread;
  public PollingService()
  {
    InitializeComponent();
    workerThread = new Thread(DoWork);
    workerThread.SetApartmentState(ApartmentState.STA);
  }
  protected override void OnStart(string[] args)
  {
    workerThread.Start();
  }
  protected override void OnStop()
  {
    workerThread.Abort();
  }
  private static void DoWork()
  {
    while (true)
    {
      log.Info("Doing work...");
      // do some work, then
      Thread.Sleep(1000);
    }
  }
}

Для хорошего поведения сервиса нужно еще несколько полезных фишек. Во-первых, полезно добавлять в сервисы возможность запуска из консоли. Это помогает при отладке.

// Использование сервиса в консоли
var service = new PollingService();
ServiceBase[] servicesToRun = new ServiceBase[] { service };
 
if (Environment.UserInteractive)
{
  Console.CancelKeyPress += (x, y) => service.Stop();
  service.Start();
  Console.WriteLine("Running service, press a key to stop");
  Console.ReadKey();
  service.Stop();
  Console.WriteLine("Service stopped. Goodbye.");
}
else
{
  ServiceBase.Run(servicesToRun);
}

Другая полезная фича – это саморегистрация, чтобы вместо использования installutil можно было установить сервис через myservice /i. Для этого существует отдельный класс…

// Утилита саморегистрации
class ServiceInstallerUtility
{
  private static readonly ILog log = 
    LogManager.GetLogger(typeof(Program));
  private static readonly string exePath = 
    Assembly.GetExecutingAssembly().Location;
  public static bool Install()
  {
    try { ManagedInstallerClass.InstallHelper(new[] { exePath }); }
    catch { return false; }
    return true;
  }
  public static bool Uninstall()
  {
    try { ManagedInstallerClass.InstallHelper(new[] { "/u", exePath }); }
    catch { return false; }
    return true;
  }
}

Класс установки использует мало знакомую сборку System.Configuration.Install. Используется она прямо из Main():

// Разбор пути для саморегистрации
if (args != null && args.Length == 1 && args[0].Length > 1
    && (args[0][0] == '-' || args[0][0] == '/'))
{
  switch (args[0].Substring(1).ToLower())
  {
    case "install":
    case "i":
      if (!ServiceInstallerUtility.InstallMe())
        Console.WriteLine("Failed to install service");
      break;
    case "uninstall":
    case "u":
      if (!ServiceInstallerUtility.UninstallMe())
        Console.WriteLine("Failed to uninstall service");
      break;
    default:
      Console.WriteLine("Unrecognized parameters.");
      break;
  }
}

Ну и последняя фича это конечно же использование логирования. Я использую библиотеку log4net, а для записывания логов в консоль можно использвать очень вкусную фичу под названием ColoredConsoleAppender. Сам процесс логирования примитивен.

Несколько важных правил

На первый раз достаточно информации. К концу хочу напомнить несколько простых правил:

  • Запуск IE требует single-thread apartment; я правда использую FireFox т.к. мне нравится FireBug
  • WatiN следует исполнять в 32-битной программе (x86)
  • Поллинг, приведенный выше неидеален, т.к. не учитывает тот факт, что сам по себе WatiN протормаживает и парсинг HTML – тоже операция небыстрая

Кстати о птичках… вместо сервиса можно в принципе сделать EXE и запускать его через sheduler. Но это как-то неопрятно.

Спасибо за внимание. Продолжение следует :)

Реклама

Written by Dmitri

20 мая 2010 at 9:18

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

Tagged with ,

Скрейпинг сайтов с .Net и WatiN

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

Рано или поздно у каждого разработчика появится соблазн «скачать» какой-нибудь сайт, либо для того чтобы получить или проанализировать определенный контент, либо просто доказать себе что это в его (её) силах. На самом деле, получить доступ к HTML определенного сайта просто, но проблемы начинаются тогда, когда сайт требует авторизации или содержит контент в виде картинки (например капчу или текст). В этом посте я расскажу какими методами я получаю контент с сайтов и что я с ним делаю.

Как получить текст?

Первый инструмент который я использую называется WatiN. Этот фреймворк используется для интерактивного тестирования веб-приложений. С помощью него, ваша .Net программа может открыть браузер, перейти на определенную страницу, нажать на кнопку или проверить что открылось ожидаемое окошко. WatiN предоставляет более-менее объектную модель, с помощью которой можно разбирать страницу на ее DOM составляющие и вытаскивать из них информацию.

Начать работать с WatiN просто – нужно всего лишь создать объект типа браузера (например IE или Firefox).

using (var browser = new FireFox(@"http://apps.facebook.com/desktopdefender/"))
{
  ...
}

Дальше, WatiN сам дождется пока страница загрузится, и потом объектная модель станет доступна. Для идентификации элемента очень удобно использовать Firebug. Конечно, если страница использует AJAX, скорее всего придется использовать Thread.Sleep() – то же самое относится например к ситуациям где вам нужно дождаться загрузки какой-нибудь flash вставки.

Получив объект browser, мы можем беспрепятственно найти определенный элемент. Например вот так:

var infoTable = browser.Tables.First(t => t.ClassName == "info");

Код выше находит первую таблицу, у которой определен класс info. Дальше действия могут быть разными, но мое предпочтение – по возможности загружать начинку элементов в System.Xml.Linq.XElement, чтобы в посделствии использовать LINQ:

var clean = Regex.Replace(infoTable.InnerHtml, @"<img [^>]+>", string.Empty);
var xe = XElement.Parse(clean);

В примере выше я корректирую тот факт что в HTML страница иногда использует незакрытый тэг <img>. В обычном сценарии бывает несколько подобных незакрытых элементов которые нужно либо удалить либо трансформировать.

После того, как вы загрузили XElement (и не произошло ошибки), его можно анализировать и вытаскивать начинку с помощью чисто .Net-ного API:

var rows = xe.Elements("tr");
string[] values = rows.Where(r => r.Elements("td").Any()).Select(r => r.Element("td").Value).ToArray();
foreach (string value in values)
  ...

Мое обычное действие в данном случае – создание сущности для вставления в базу.

Как получить картинку?

Иногда нужно автоматизировать не взаимодействие с HTML (что достаточно просто, т.к. элементы в WatiN обладают такими полезными методами как Click()), а например с Flash. В этом случае наша первая цель – это сделать «снимок» страницы дабы понять что где находится.

Мой подход к «фотографированию» страницы прост – я делаю скриншот всего монитора! Обычно графические элементы находятся на одном и том же месте, следовательно последующий поиск отрезка (например капчи) который нужно проанализировать особого труда не составляет.

Скриншот берется вот так:

private static Bitmap GetSnapshot()
{
  var bounds = Screen.GetBounds(Point.Empty);
  var bmp = new Bitmap(bounds.Width, bounds.Height);
  using (var g = Graphics.FromImage(bmp))
    g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);
  return bmp;
}

Что делать с картинкой – решать вам, но учтите, что использовать managed структуру System.Drawing.Bitmap я не рекоммендую – это один из самых медленных аспектов в .Net. Работать с картинкой лучше в C++, о чем я уже писал.

Как двигать и кликать мышкой?

Казалось бы – простая вещь – двигать и кликать мышью. Это еще одна фича которая требуется если вы хотите взаимодействовать с графическим или RIA-контентом. Мне для этого понадобилась следующая инфраструктура:

enum Messages { WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202 };
 
[DllImport("User32.dll")]
static extern void mouse_event(MouseFlags dwFlags, int dx, int dy, int dwData, UIntPtr dwExtraInfo);
 
[Flags]
enum MouseFlags
{
  Move = 0x0001, LeftDown = 0x0002, LeftUp = 0x0004, RightDown = 0x0008,
  RightUp = 0x0010, Absolute = 0x8000
};

Ну а что касается самого метода «переместиться и кликнуть», то он выглядит вот так:

private static void ClickAt(Point p, int waitMsec)
{
  Console.WriteLine("Clicking at {0}, {1}", p.X, p.Y);
  Cursor.Position = p;
  mouse_event(MouseFlags.LeftDown, p.X, p.Y, 0, UIntPtr.Zero);
  mouse_event(MouseFlags.LeftUp, p.X, p.Y, 0, UIntPtr.Zero);
  Thread.Sleep(waitMsec);
}

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

Written by Dmitri

10 марта 2010 at 18:44

Опубликовано в Technology

Tagged with