Новый спайдер на основе htmlparser + soupselect
На досуге набросал новый спайдер для очередного микропроекта. В этот раз я решил не использовать jsdom (как в нескольких предыдущих), а остановиться на связке htmlparser + soupselect. Страница сначала отдаётся парсеру, а поиск по полученному DOM делается с помощью SoupSelect.
Архитектура спайдера
Архитектура самого спайдера тоже отличается от предыдущего. На этот раз в основе лежит EventEmitter, а состояния спайдера реализованы в виде событий. События всего два — ready, означающее что спайдер готов запросить и обработать новую страницу, и new_friend, означающее что получен отдельный результат. Сейчас спайдер парсит страницы пользователей Хабра, и событие new_friend означает что в профиле пользователя найдена ссылка на друга.
Код выглядит так:
var spider = new process.EventEmitter(); var users_to_go = []; spider.on('ready', function() { if (users_to_go.length == 0) { console.log('We\'re done'); process.exit(1); } // Получаем следующего юзера var user = users_to_go.shift(); // Получаем друзей из его профиля get_friends(user.username, function(err, data) { if (!err) { data.forEach(function(friend) { // Обнаружена ссылка на другой профиль spider.emit('new_friend', friend, user.level + 1, user.path + ' ' + user.username); }); } // Переводим спайдер в состояние ready через 4 секунды setTimeout(function() { spider.emit('ready'); }, 4000); }); }); spider.on('new_friend', function(username, level, path) { // Событие обнаружения новой ссылки. Здесь мы можем делать с ней что угодно. }); // Начальный пользователь users_to_go.push({username: 'shoohurt', level: 0, path:''}); // Запускаем спайдер, переводя его в состояние ready spider.emit('ready');
В принципе для моих целей этого уже достаточно, но для большей гибкости получение и обработку страницы можно было бы выделить в отдельные события, чтобы можно было вклиниваться в процесс на разных этапах.
HTMLparser и SoupSelect
Внутри функции get_friends происходит создание HTTP-клиента, получение страницы и её обработка. Вот на обработке я хочу остановиться подробнее.
Сначала полный текст страницы передаётся htmlparser’у, на выходе получается DOM. Сделать это можно двумя способами, в зависимости от того где Вы взяли модуль htmlparser’а (npm или github). Версия с Github работает так:
var html = require('./node-htmlparser'); var dom = html.ParseHtml(text);
Версия из npm отличается способом подключения (из всех названий модулей npm выбрасывают слово node) и способом создания dom:
var html = require('htmlparser'); var handler = new htmlparser.DefaultHandler(function (error, dom) {}); var parser = new htmlparser.Parser(handler); parser.parseComplete(text); var dom = handler.dom;
Я сначала пользовался версией из Github, потом починил таки npm и перешёл на версию оттуда. После получения DOM мы можем выполнять по нему поиск с помощью soupselect:
var select = require('soupselect'); var friends = []; select(dom,'dl.friends_list a').forEach(function(friend){ friends.push(friend.attribs.href.match(/\/\/([^.]*).habrahabr.ru/)[1])});
Модулю передаётся dom и селектор, на выходе получается массив найденных элементов. DOM в htmlparser немного не такой как в браузере, но структура довольно понятна (больше информации можно найти в документации htmlparser). Связка htmlparser+soupselect работает быстрее чем jsdom+jquery.
Кроме того, в спайдере используется коннектор для Redis (npm install redis), с очень удобным API и стабильной работой. Помещение награбленного контента в Redis происходит в обработчике события new_friend. Вообще, для более открытой архитектуры стоит ещё разделить функцию get_friends на отдельные события обработки страницы — получение кода статуса и заголовков, получение текста ответа, построение DOM. Тогда можно будет подключать готовый спайдер целиком и просто вешать обработчики на готовые события. Также в коде спайдера нужен пул соединений и поддержка вытягивания страниц в несколько потоков, но это всё будет потом.
Надо будет, наконец, написать что-то серьёзное на Ноде 🙂
Имхо, не стоит использовать master с гитхаба как раз из-за постоянного изменения API. Я ещё понимаю, когда пишут багрепорты на эту тему контрибьюторы проекта, но ведь часто пишут пользователи, которым не всегда понятно, что master в Git обычно содержит нестабильную версию и нужно использовать последнюю тегированную версию.
P.S. Нужно при экспорте на nodejs.ru относительные ссылки править, http://nodejs.ru/2010/04/15/node-js-pseudo-threads/ ведёт на 404.
Опс 🙂 Ссылки должны становиться абсолютными сами (через XSLT), похоже что то сломалось в преобразователе ) Спасибо что указал.
Посмотри на Scrapy http://scrapy.org/
Оно на питоне, если с питоном не знаком, хотя бы архитектуру можно подглядеть, она там очень удачная. Тоже асинхронное, работает на ура.