Data acquisition, часть 1

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

    Спасибо за внимание. Продолжение следует :)
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 27

      0
      Отличная статья. Вы в какой именно сфере применяете web data mining?
        0
        В различных сферах. Например, у меня есть быстрые интерфейсы к форумам, слепки некоторых баз. Большинство подпадает либо под NDA либо под «просто не хочу афишировать». Ведь владельцы порой подобного не любят, меняют структуры сайта и все приходится делать заново.
      • НЛО прилетело и опубликовало эту надпись здесь
          +2
          Полстатьи о том, как написать правильный сервис) Думаю, этому стоило б уделить отдельную статью — благо тема стоящая.
            0
            На самом деле я не пропагандирую написание своих сервисов. Легче использовать готовый хост.
              0
              А можно в этом месте подетальнее?
                +1
                Я планировал просто про это написать, но идея примерно в следующем: вместо того чтобы писать свой сервис, мы пишем нечто вроде «плаг-ина» для уже существующего, обобщенного сервиса. Хосты бывают для разных целей — например для WCF или для NServiceBus. Хост может сам быть сервисом. Хостом также может выступать, например, IIS.
                  0
                  Спасибо, не встречал раньше таких оберток.
            0
            занимаюсь тем же самым. почему не используете обычный WebClient/WebRequest?
              0
              Когда есть соответствие между URL и контентом, использую.
                0
                а как «боретесь» с pagination которая сделана на asp.net с помощью postback? если не секрет
                  0
                  Эта ситуация очень много где, и я с ней не борюсь. Первое что я делаю — смотрю есть ли в QueryString параметр размера страницы. Если есть — ставлю значение 100500 чтобы получить все записи на одной странице. Если нет — делаю обход с использованием WatiN. Да, это медленно, иногда жутко медленно, но скрейпинг в любом случае нельзя воспринимать как realtime processing.
                    0
                    есть ли проблемы в выявлении уникальности страницы? (чтобы не грузить по несколько раз одну станицу)
                    например ссылки на первой станице
                    (1) 2 3 4 5 6 7 8 9 следующая
                    если щелкаем на 9 ую то картина
                    такая же, только девятка вместо единицы подсвечена
                    1 2 3 4 5 6 7 8 (9) следующая
                    нужно нажать «следующая» и увидеть
                    предыдущая (10) 11 12 13 14 15 16 17 18 19 следующая
                    причем для 1 и для 10 в параметре href стоит javasript:doPostBack('blabal$page1', '')
                      0
                      На самом деле порой проблем в 10 раз больше, особенно если вы например парсите какой-нибудь мощный чат-движок. В каждом случае проблема решается в индивидуальном порядке. Например, в случае повторений, у меня часто фигурирует такая структура как HashSet<T>, тем самым дупликаты не попадают в базу данных. Это кстате актуально когда нужно делать повторный обход уже посещенных в прошлом страниц – как например в случае с использованием поллинга.
              0
              Дмитрий! Очень понравилась верстка топика, особенно модные заголовки-картинки!

              У меня есть вопрос о том, чем же вас в действительности не устраивает, например, объект WebBrowser (который можно как раз запускать в Single Thread и радоваться жизни). И кстати, могут ли эти библиотеки обрабатывать и исполнять javascript? это иногда действительно проблема!

              Я сейчас занят решением схожей задачи, и вариант с server-side WebBrowser Object меня очень и очень радует.

              P.S.: Минимальный ценник на SQL Server 2008 Standard ощутимо ниже, деталей не скажу из-за недостатка информации, но версия за 7500 — это одна из самых дорогих коробочных лицензий.
                0
                WatiN использует сам браузер, тем самым нет вообще никаких проблем ни с js ни с flash или любым другим плагином. Использование WebBrowser тоже вариант, конечно, хоть и весьма странный.

                Для типичного заказа на подобные работы, стоимость движка базы данных равна 0 (нулю). Что касается SQL Server, можно с уверенностью утверждать что стоимость SQL Server в России не может быть сильно ниже чем RRP. Посмотрите на SoftLine – 215757 рублей = $7014.
                0
                Почему не воспользоваться HtmlWeb из состава HtmlAgilityPack для загрузки страницы?
                  0
                  В большинстве случаев нужно иметь возможность управлять браузером. Насколько я понимаю, HtmlWeb это обертка вокруг WebRequest?
                    0
                    Не знаю, код не смотрел, но подозреваю, что да. Просто страница грабится в одну строку, и сразу создаётся объект HtmlDocument. Просто исходя из вашего примера, я управления браузером не увидел, вот и предложил :) Вообще, я написал несколько парсеров на .Net, и ни разу браузером рулить было не надо. Поделитесь, когда это может быть нужно? Самый сложный парсер был для яндекса, там приходилось вручную качать странички через WebRequest и отправлять данные каптчи через POST. Но в общем-то, тот же Curl :)
                      0
                      Браузером нужно рулить когда URL не соответствует контенту один-к-одному. Например, paging через postback или ajax.

                      Вот живой пример: работа с банк-клиентами. Чтобы войти в банк-клиент, нужно сделать много телодвижений. Чтобы сделать перевод с одного счета на другой тоже нужно понажимать кнопочки.
                        0
                        Можно пробовать считать ответы Ajax напрямую, но в целом я понял о чём вы.
                  0
                  отличная статья и ссылки на пакеты

                  ps — тему опасную вы подняли, это руководство можно применить не только для скрейпинга
                    0
                    руководство

                    руководство кого/чего? :)
                      +1
                      руководство = статья = мануал = tutorial

                      вы слишком долго жили _там_ :)
                        0
                        нда, что-то в голове замкнуло что де «злое руководство будет это использовать против нас» :) менталитет…
                    0
                    Подскажите, как извлечь данные из flash-приложения — www.protennislive.com/frameset.asp?year=2010&wkno=20&lang=en&tabno=1&eventid=0615&ref=www.atpworldtour.com
                      +1
                      запустите firebug.
                      отследите запросы.
                      в основном там картинки, но есть и бинарная передача данных.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое