Как стать автором
Обновить

Асинхронный web-mining c помощью node.js

Время на прочтение 6 мин
Количество просмотров 2.6K
Хотелось бы поделится опытом решения задачи web-mining'а: сбор некоторой информации с определенного списка ресурсов. Сразу хотелось бы отметить, что это не является попыткой создать свой «поисковик» — для этого используются совершенно другие подходы. Цель web-mining’а – вытащить часть информации. Например, если ресурс поддерживает микроформаты в виде «визиток» и т.п.


Теперь о реализации: почему именно node.js? Дейстительно, у меня не было ограничений по какой-то конкретной технологии – можно было использовать все от C++ с Java/.NET и до Perl/Python. Расскажу почему выбрал node.js:
  • Асинхронные IO-операции. Хотя в других языках тоже можно организовать асинхронность, и иногда очень просто – в F# есть блок async, но у node.js асинхронность идет «из коробки» и является предпочтительным способ выполнения операций.
  • Наиболее привычный синтаксис с наименьшим количеством излишних конструкций. Конечно пункт «холиварный», но по-факту javascript более близок тем, кто использовал C/C++, java, C#, чем F# или Python.
  • Поддержка http-клиента и регулярных выражений «из коробки» без необходимости ставить дополнительные модули.
  • Скорость выполнения. Хотя у V8 «слабое» место – переключение контекста, но у данной задачи это не должно являться «узким горлышком» и более важна «линейная» скорость. А V8 как раз этим может похвастаться (N.B. сделать benchmark чтоб доказать в цифрах этот пункт).

Установка node.js


Установка на моем сервере (FreeBSD, amd64) прошла более, чем плавно – «cd /usr/ports/www/node;make install» и node.js готов к использованию.

Для Windows-платформ наиболее доступен вариант установки через cygwin. Хорошей инструкции я не нашел, хотя натолкнулся на реализацию node.js чисто силами .NET.

Для Ubuntu делается тоже без особых проблем — например неплохая инструкция.

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

Разгрузчик страниц


Взяв пример по http.Client и докрутив ожидание загрузки всего документа, разбор url и составление нужного запроса вышел вот такой “класс”:
var webDownloader = function(sourceUrl) {<br>
    events.EventEmitter.call(this);<br>
    this.load = function(sourceUrl) {<br>
      var src = url.parse(sourceUrl);<br>
      var webClient = http.createClient(src.port==undefined?80:src.port,src.hostname);<br>
      var get = src.pathname+(src.search==undefined?'':src.search);<br>
      sys.log('loading '+src.href);<br>
      var request = webClient.request('GET', get ,<br>
       {'host': src.hostname});<br>
      request.end();<br>
      var miner = this;<br>
      request.on('response', function (response) {<br>
  //     console.log('STATUS: ' + response.statusCode);<br>
  //     console.log('HEADERS: ' + JSON.stringify(response.headers));<br>
       response.setEncoding('utf8');<br>
       var body = '';<br>
       response.on('data', function (chunk) {<br>
        body += chunk;<br>
       });<br>
       response.on('end', function() {<br>
          miner.emit('page',body, src);<br>
       });<br>
      });<br>
    };<br>
  }<br>
  sys.inherits(webDownloader, events.EventEmitter);
<br>
<br>
* This source code was highlighted with Source Code Highlighter.


Интересно тут то, что каким образом происходит регистрация класса как источника ивентов:
  1. сначала мы регистрируемся у EventEmitter-а в конструкторе: events.EventEmitter.call(this);
  2. “наследуем” класс от EventEmitter
  3. “эмитим” событие с помощью метода emit


Именно работа с EventEmitter-ом пока слабо документирована, по этому пришлось немного погуглить.

Теперь мы можем подписаться на ивент полной загрузки страницы:
var loader = new webDownloader();<br>
loader.on('page',vcardSearch);


Поиск vCard-данных


Теперь менее интересная функция которая именно вытаскивает vCard-данные из странички. Я не хотел тратить много времени на правильную реализацию, по этому сделал «в лоб» — поиск элементов с нужными классами.

Тут уже ничего особо интересного, кроме разве что использования модуля Apricot для парсинга странички (хотя реально достаточно было бы использовать htmlparser, но Apricot у меня гораздо быстрее поставился). Сначала я попробовал построить CSS-селектор для поиска нужных элементов и использовать функцию find у Apricot’а (которая, в свою очередь, использует Sizzle для поиска), но, как оказалось рекуррентный обход всех элементов быстрее.

В итоге получилась вот такая функция:
var vcardSearch = function(body,src) {<br>
    sys.log('scaning '+src.href);;<br>
    Apricot.parse(body,function(doc) {<br>
      var vcardClasses = [<br>
        // required<br>
        'fn',<br>
        'family-name', 'given-name', 'additional-name', 'honorific-prefix', 'honorific-suffix',<br>
        'nickname',<br>
        // optional<br>
        'adr','contact',<br>
        'email',<br>
        'post-office-box', 'extended-address', 'street-address', 'locality', 'region', 'postal-code', 'country-name',<br>
        'bday','email','logo','org','photo','tel'<br>
      ];<br>
      var vcard = new vCard();<br>
      var scanElement = function(el) {<br>
        if (el==undefined) return;<br>
<br>
        if (el.className != undefined && el.className!='') {<br>
          var classes = el.className.split(' ');<br>
          for(var n in classes) {<br>
            if (vcardClasses.indexOf(classes[n])>=0) {<br>
              var value = el.text.trim().replace(/<\/?[^>]+(>|$)/g, '');<br>
              if (value != '') vcard.Values[classes[n]] = value;<br>
            }<br>
          }<br>
        }<br>
        for(var i in el.childNodes) scanElement(el.childNodes[i]);<br>
      }<br>
      scanElement(doc.document.body);<br>
      if (!vcard.isEmpty())<br>
        sys.log('vCard = '+vcard.toString());<br>
      else<br>
        sys.log('no vCard found on '+src.href);<br>
    });<br>
  }
<br>
<br>
* This source code was highlighted with Source Code Highlighter.

Итог


Использовать результат просто:

loader.load('http://www.google.com/profiles/olostan');<br>
loader.load('http://www.flickr.com/people/olostan/');<br>


Сразу хочу сказать, что задумывалось не как конечный хоть немного серьезный продукт, а скорее как proof-of-concept и для того, чтоб пощупать node.js

Код полностью (загруженно на Google Docs, может потребовать google аккаунт)

P.S. Это перепост с моего поста в песочнице. Извиняюсь, если так не принято, но интересно было бы услышать комментарии. Спасибо Romachev за инвайт. Запостить в тематический блог не хватает кармы.
Теги:
Хабы:
+10
Комментарии 4
Комментарии Комментарии 4

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн