Одно из приемуществ всеобщего удешевления аппаратуры и интернета в том, что сбор информации из разных источников в интернете почти ничего не стоит и может производиться без особых проблем. Задача получения и обработки больших объемов данных является коммерчески превлекательной ввиду спроса на считывание («скрейпинг») веб-сайтов со стороны заказчиков (обычно это описывается термином ‘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.

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

[STAThread]<br/>
static void Main()<br/>
{<br/>
  using (var browser = new IE("http://www.pokemon.com"))<br/>
  {<br/>
    var doc = new HtmlDocument();<br/>
    doc.LoadHtml(browser.Body.OuterHtml);<br/>
    var h1 = doc.DocumentNode.SelectNodes("//h3").First();<br/>
    Console.WriteLine(h1.InnerText);<br/>
  }<br/>
  Console.ReadKey();<br/>
}<br/>

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

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

public partial class PollingService : ServiceBase<br/>
{<br/>
  private readonly Thread workerThread;<br/>
  public PollingService()<br/>
  {<br/>
    InitializeComponent();<br/>
    workerThread = new Thread(DoWork);<br/>
    workerThread.SetApartmentState(ApartmentState.STA);<br/>
  }<br/>
  protected override void OnStart(string[] args)<br/>
  {<br/>
    workerThread.Start();<br/>
  }<br/>
  protected override void OnStop()<br/>
  {<br/>
    workerThread.Abort();<br/>
  }<br/>
  private static void DoWork()<br/>
  {<br/>
    while (true)<br/>
    {<br/>
      log.Info("Doing work⋮");<br/>
      // do some work, then
      Thread.Sleep(1000);<br/>
    }<br/>
  }<br/>
}<br/>

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

var service = new PollingService();<br/>
ServiceBase[] servicesToRun = new ServiceBase[] { service };<br/>
 <br/>
if (Environment.UserInteractive)<br/>
{<br/>
  Console.CancelKeyPress += (x, y) => service.Stop();<br/>
  service.Start();<br/>
  Console.WriteLine("Running service, press a key to stop");<br/>
  Console.ReadKey();<br/>
  service.Stop();<br/>
  Console.WriteLine("Service stopped. Goodbye.");<br/>
}<br/>
else<br/>
{<br/>
  ServiceBase.Run(servicesToRun);<br/>
}<br/>

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

class ServiceInstallerUtility<br/>
{<br/>
  private static readonly ILog log = <br/>
    LogManager.GetLogger(typeof(Program));<br/>
  private static readonly string exePath = <br/>
    Assembly.GetExecutingAssembly().Location;<br/>
  public static bool Install()<br/>
  {<br/>
    try { ManagedInstallerClass.InstallHelper(new[] { exePath }); }<br/>
    catch { return false; }<br/>
    return true;<br/>
  }<br/>
  public static bool Uninstall()<br/>
  {<br/>
    try { ManagedInstallerClass.InstallHelper(new[] { "/u", exePath }); }<br/>
    catch { return false; }<br/>
    return true;<br/>
  }<br/>
}<br/>

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

if (args != null && args.Length == 1 && args[0].Length > 1<br/>
    && (args[0][0] == '-' || args[0][0] == '/'))<br/>
{<br/>
  switch (args[0].Substring(1).ToLower())<br/>
  {<br/>
    case "install":<br/>
    case "i":<br/>
      if (!ServiceInstallerUtility.Install())<br/>
        Console.WriteLine("Failed to install service");<br/>
      break;<br/>
    case "uninstall":<br/>
    case "u":<br/>
      if (!ServiceInstallerUtility.Uninstall())<br/>
        Console.WriteLine("Failed to uninstall service");<br/>
      break;<br/>
    default:<br/>
      Console.WriteLine("Unrecognized parameters.");<br/>
      break;<br/>
  }<br/>
}<br/>

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

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

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

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

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