Хотелось бы поделится опытом решения задачи web-mining'а: сбор некоторой информации с определенного списка ресурсов. Сразу хотелось бы отметить, что это не является попыткой создать свой «поисковик» — для этого используются совершенно другие подходы. Цель web-mining’а – вытащить часть информации. Например, если ресурс поддерживает микроформаты в виде «визиток» и т.п.
Теперь о реализации: почему именно node.js? Дейстительно, у меня не было ограничений по какой-то конкретной технологии – можно было использовать все от C++ с Java/.NET и до Perl/Python. Расскажу почему выбрал node.js:
Установка на моем сервере (FreeBSD, amd64) прошла более, чем плавно – «cd /usr/ports/www/node;make install» и node.js готов к использованию.
Для Windows-платформ наиболее доступен вариант установки через cygwin. Хорошей инструкции я не нашел, хотя натолкнулся на реализацию node.js чисто силами .NET.
Для Ubuntu делается тоже без особых проблем — например неплохая инструкция.
Дальше прочтение довольно приятного мануала. Хотя мануал действительно симпатично выглядит, но он покрывает только базовые элементы, и когда мне захотелось, чтоб мой веб-майнер был как большинство других классов и мог инициировать события, оказалось, что в мануале совершенно это не описано. Но об этом позже.
Взяв пример по http.Client и докрутив ожидание загрузки всего документа, разбор url и составление нужного запроса вышел вот такой “класс”:
Теперь о реализации: почему именно 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.
Интересно тут то, что каким образом происходит регистрация класса как источника ивентов:
- сначала мы регистрируемся у EventEmitter-а в конструкторе: events.EventEmitter.call(this);
- “наследуем” класс от EventEmitter
- “эмитим” событие с помощью метода 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 за инвайт. Запостить в тематический блог не хватает кармы.