Связаться со мной можно, черканув пару строк на mail@mindcollapse.com или же в skype: orl-light
Прежде чем начать очередное повествование, которое будет абсолютно неинтересно любителям смехуечек (делегация фишкинет встала и молча вышла из зала) - небольшое лирическое отступление. Мне на мыло написал чувак с забавным вопросом, мол где я, везучий сукин сын, нахожу столько проектов под малоизвестный и нераспространенный ноде. Отвечаю: проектов под эту платформу практически нет, языком программирования это назвать нельзя, это скорее server-side runtime environment для javascript, инструментарий для решения определенного круга задач. Меня находят совершенно по другим критериям (мой рост, соотношение размера ступни к диаметру зрачка, могу говорить, как Сталоне, ну вы поняли), я лишь выбираю NodeJS для реализации. А мог бы выбирать ерланговские Mochiweb, Misultin, Cowboy (показывающие даже более быстрые результаты) или пайтоновский Tornadoweb, но мне родной JS как-то ближе к телу, к тому же под него есть отличный репозитарий готовых велосипедов на все случаи жизни под названием NPM. Хотя, возможно, в ближайшем будущем тут появятся посты и про вышеназванные технологии. Интересно? Жми на +1.
Итак, как меня учили в гуманитарном ВУЗе, начнем с постановки проблематики. Ко мне обратился мой знакомый, подрядчик одной крупной фирмы в Британии со следующим вопросом: есть 40 GB документов в HTML формате (при ближайшем рассмотрении, размер сократился до 18 GB, что тоже немало, столько примерно весит html дамп русскоязычной википедии состоянием на 2008 год), информация крайне конфиденциальная (дневники принцесс и королей, ага) и собиралась за столетнюю историю компании, лет 5-6 тому назад фирма перешла на какую-то свою систему хранения документации, но старый дамп остался статичным и требовал индексации для поиска по нему. Интранет не позволял использовать Google, импортировать все в новую систему документооборота никто не хотел, кому нужен информационный мусор при какие-то результаты санитарных анализов воды в Нью-Гэмпшире за 1928 год? Нужно отдать должное сотрудникам этой конторы, они усердно оцифровывали все и толково составляли каталоги вручную, а значит к каждому документу был доступ по какой-то гиперссылке с другой страницы. Готовые решения я даже не искал по определенным техническим причинам, было принято решение делать свой crawler + indexer, что в наше время оказалось вполне тривиальной задачей.
Самого паука, который будет путешествовать по пыльному архиву, написать, как два пальца об асфальт, об этом мы поговорим чуточку позже, остановимся лучше на технологии поиска и хранения индекса. Всякие реляционные базы данных отпали сразу после того, как я выполнил du -sh в директории с каталогом. Хотел было попробовать сделать что-то свое на основе NoSQL, но из всех известных мне движков не позволял мне искать подобным образом (чуть позже я случайно наткнулся на Riak, увы, было уже поздно), только MongoDB мог похвастаться поддержкой регулярок при выборке, а про полноценный же full text и речь не шла, в официальном мануале советовали разбивать текст на ключевые слова, но это ужасное решение. Именно поэтому было принято решение хотя бы здесь не изобретать двухколесные механизмы и использовать готовые удобные решения. Я успел посмотреть на Apache Solr и ElasticSearch, которые построены на Java Lucene и на Sphinx. Последний сразу отпал, так как его преимущества в виде ненужных мне индексации SQL баз данных и своего Query Language для сложных выборок были сведены на нет необходимостью писать отдельную XML pipe для добавления статики. Solr, имеющий великолепный административный интерфейс, большое коммьюнити, возможность индексировать DOC и PDF (ну мы и сами можем использовать Apache Tika в любом другом движке) показался каким-то слишком сложным для быстрого старта и избыточным, плюс ему не хватало внятной документации (доки в wiki это FFFUUUU). Поэтому я и остановился на ElasticSearch, который может похвастаться великолепным REST API на основе JSON, русской морфологией из коробки, различными текстовыми анализаторами и возможностью сочетать их в любой последовательности, либо даже создавать свои собственные, встроенными языками скриптования (js или python) для выборки и фильтрации, многими вариантами storage для нашего индекса, кучей параметров сортировки и ранжирования, подсвечиванием результатов, wildcard запросами и AND, OR ключевыми словами, простым созданием кластера и репликацией между нодами, даже возможность использования поискового движка в качестве key-value хранилища данных при правильном маппинге. И да, все работает без малейшей конфигурации в стиле load`n`run, я только изменил network.host на 127.0.0.1 и после запуска получил работающее хранилище нашего индекса. А еще у еластика есть, если и не идеальная, то хотя бы внятная документация и крутое Java API, которое, впрочем, нам не понадобится.
Теперь можем переходить непосредственно к написанию нашего crawler-а, который будет переходить по ссылкам и отправлять данные в ElasticSearch. Прежде всего, я написал свою обертку вокруг RESTа еластика для облегчения процесса индексации и поиска, она очень простая и реализует только функционал двух функций - index (плюс update) и search. Код обвязки можно посмотреть. Его мы будем использовать, как в нашем поисковом боте, так и в серверном приложении, возвращающем ответ пользователю. Теперь непосредственно о преимуществах NodeJS в этой сфере. Прежде всего, это мультипоточность из коробки, разумеется, на всех современных языках можно добиться аналогичного результата (ну разве что кроме похапе, но оно там абсолютно не нужно); и, второй по значимости плюс, - jQuery. Да, я не опечатался, мы будем использовать селекторы и модификаторы jquery на серверсайде. Все это возможно благодаря великолепной библиотеки JSDOM, которая позволяет нам получить виртуальный DOM из кода HTML разметки и эмулировать все вызовы к нему. Почему jQuery? Ответ прост - банальная лень в ущерб определенной производительности, конечно же мы можем использовать регулярки для выборки всех ссылкок, но $('a').each() выглядит проще и симпатичней. Столкнулся с необычной проблемой, которая описана в том числе и в коде самого краулера: ElasticSearch почему-то наотрез игнорировал точки, двоеточия, слешы и прочие символы, поэтому сделать проверку статуса индексации ссылки через ее url не получилось, для этой цели используется md5 хэш. В целом, получилось достаточно быстрое решение, данный блог, скормленный своей главной страницей нашему боту, был полностью добавлен в кеш еластика за полторы минуты всего-то в 3 потока. Я старался, по мере возможности, комментировать исходный код, надеюсь, что у вас не возникнет проблем с его чтением. В идеале, я советую использовать 10 потоков с queueInterval около 100, если ваши сетевая подсистема и процессор выдержат подобные высокие нагрузки, с такими значениями мне удавалось индексировать около трехсот страниц википедии в минуту на средненьком железе и канале в 30 mbit\s. Паук проверяет вхождение домена индексируемой ссылки в разрешенный список и ищет дозволенный content-type в ответе, реализует своеобразный robots.txt при помощи массива регулярок indexQueryPath. К тому же возможна переиндексация при достижении определенного возраста индекса, у меня, по умолчанию, это 100 дней. Процесс индексации в вашей консоли будет выглядеть как-то так. Работающий скрипт в memory leaks замечен не был, но вот JSDOM и jQuery очень любят процессор, будьте готовы к высоким нагрузкам. Для демонстрации я прошерстил этот бложек и написал небольшое приложение поиска по всем постам, состоящее из такой вот серверной части. Его работу можно посмотреть в лаборатории им. Бена Ганна по ссылке.
В заключение, хочу выразить благодарность разработчикам и сообществу NodeJS и ElasticSearch. При всей моей нелюбви к красноглазому opensource, два этих великолепных программных продукта, создаваемых на чистом энтузиазме, помогли мне сделать работу в срок, обрадовать вычурных британцев и купить новую дозу героина. Очень сложно затягивать жгут на руке и писать сюда, до новых встреч, друзья.
Начнем с самого глубокого серверсайда. В распоряжении имелся старенький слабенький сервачок с ненавистным CentOS-ом, ну делать было нечего. Прежде всего, нужно понять, что без иксов нам абсолютно никак не обойтись. Они нужны для виртуальных дисплеев, куда мы будем высаживать наши браузеры. Варианта два: мы можем использовать xorg-x11-server-Xvfb либо же vncserver, который представляет собой perl обертку вокруг Xvnc. Я советую остановиться на втором варианте преимущественно по причине меньшего количества dependencies, возможности удобного удаленного подключения для настройки того же браузера и отсутствия проблем с битностью цвета. К тому же, на старике-центосе Xvfb почему-то постоянно отваливался, а ядро было собрано без поддержки framebuffer, поэтому устройство /dev/fb[0-9] отсутствовало, сводя на нет все преимущества утилит fbdump и fbgrab. После установки vncserver, протестируем его работоспособность, вызвав vncserver :11 -geometry 1920х1200 -depth 24. У вас спросят пароль при первом запуске и, если все прошло без ошибок, то вы счастливчик. Иксы на виртуальном дисплее localhost:11 у вас уже есть. Можете рассказать это своему environment (ненавижу слово "окружение") c помощью export DISPLAY=:11. А еще вы можете запустить xterm и сконектиться через VNC клиент (для виндовса есть отличный бесплатный от RealVNC) и в очередной раз убедиться, что все работает. Теперь нам нужно как-то воровать экран и сохранять его в изображение. В интернетах советуют использовать утилиту import, входящую в набор ImageMagic, но это как перевозить две горошины на грузовом самолете. Вместо нее мы будем использовать xwd из xorg-x11-apps для снятия слепка с виртуального дисплея, и xwdtopnm плюс pnmtopng из netpbm-progs для конвертации его в PNG формат. Тут все еще проще, для получения скриншота вам нужно просто выполнить xwd -root -display localhost:11 | xwdtopnm 2>/dev/null | pnmtopng > screenshot.png. Большая часть серверной магии окончена, осталось сделать небольшой скрипт автоматизации и защиты от дураков. Оговорюсь сразу, что процесс генерации сриншотов будет последователен на основе очереди задач, никакого распараллеливания. О причинах этого мы поговорим чуточку позже. После установки Хромиума, что для ебучего CentOS-а, который сыпется пылью из всех щелей, тоже нихуя не тривиальная задача, мы сделаем небольшой скрипт для облегчения рутины создания скриншотов. Хромиум запускается в инкогнито режиме в фоне, мы ждем открытия страницы 6 секунд (для многих страниц этого времени не хватает, но для тестов сойдет), снимаем скришот и жестко тушим все открытые процессы. Если вам не нужны элементы оформления браузера аля адресная строка или панель табов, то можете добавить ключ --kiosk при запуске хрома. Все пропадет, останется лишь окно с отрендериной страницей, но это выглядит как-то менее эстетично ;). Таким образом, мы собрали все необходимое для создания скриншотов, осталось написать обертку и обертку над оберткой. Пару слов про безопасность: создайте отдельного пользователя из-под которого будете запускать браузер и иксы, отключите все плагины на внутренней странице about:plugins. Клиентская и серверная валидация ссылок в добавок к жесткому ограничению времени исполнения (у нас это 6 секунд) защищает от умников, которые мечтают о stack overflow и arbitrary code execution или пытаются банально загрузить html файл размером с пару гигабайт. От запуска нескольких инстансов браузера для многопоточной генерации пришлось отказаться по этой же самой причине. И да, все настройки хромиума хранятся в JSON формате в файле ~/.config/chromium/Default/Preferences, изменить вам придется параметры размеров окна, потому что даже с ключом --start-maximized у браузера развернуть окно на весь виртуальный экран почему-то не получается.
Часть вторая - middleware или, другими словами, прослойка между сервером-клиентом и своеобразный примитивный менеджер задач. Писать мы ее будем на NodeJS и SocketIO, оба решения мне полюбились event-based моделью. В стандартный набор ноде входит функция spawn объекта child_process для асинхронного запуска дочерних процессов и получения их stdout потоков, которая и будет работать с нашим небольшим bash скриптом. Для создания последовательной модели исполнения, нам нужен какой-то неблокирующий алгоритм task queue и именно поэтому нам не подходит метод Array.forEach. Я, признаться, не стал ебать себе мозг академическими решениями и просто сделал рекурсивную функцию, которая банально вызывала Array.push при добавлении новой задачи и Array.shift при завершении выполнения и переходе на новый цикл итерации с проверкой блокирующей переменной. Решение не идеальное и при больших нагрузках могут возникать проблемы с выпадающими из стека заданиями, но никто больших нагрузок и не ждет - идеальная отговорка для лентяя, который поленился сделать асинхронную модель обработки с помощью setInterval. Чтобы не быть голословным - вот вам код прослойки. Судя по коллекции сохраненных скриншотов, некоторые куллхакеры, думающие, что, выполнив в консоле isURL = function() {return true;}, они обойдут все проверки и удалят мне /etc/passwd, соснули хуйцов и посмотрели веселые картинки на сайте gay.ru. Кекеке!
Последний и самый верхний уровень - clientside не представляет собой ничего особо интересного. Хочу лишь заметить, что при всех преимуществах охуительнейшей библиотеки SocketIO, у нее нету нормальной внятной документации или хотя бы описания API. Лично мне найти не удалось, на гитхабе только описание в примерах, но это не удобно и противоречит законам мироздания. Справедливости ради скажу, что все, описанное мной в этом посте, пахнет влагой и сыростью. Нету ни обработок исключений, ни проверок результата генерации, ни нормальной валидации ссылок, но я лишь даю болванку, а ее обтачивание - дело дорчитателей, которым это необходимо.
Куда ж мы без живых примеров? Никто в наше время тексту не верит. Пришлось перенести все на свой старенький сервачок в далекой Фрицландии, сделать симпатичную обертку на скорую руку и теперь вы можете проверить работу теории на практике в разрастающейся секретной экспериментальной лаборатории интернетовских опытов имени Коли Цискаридзе. Ссылка на ScreenShooter, который старательной описывался в этой статье, работает до последнего посетителя. В качестве бонуса всем, кто дочитал достаточно сложную для формата блога статью, я дарю самодельный уникальный инструмент по раскрутке вашего сайта, известный подписчикам моего твиттера и гуглоплюса. А если серьезно, спасибо всем, кто активно плюсует гуглокнопкой мои посты. Это достаточно приятно и позволяет понять направление дальнейшего развития тематик. Продолжайте в том же духе, нажимайте +1, у меня еще много разнообразных интересностей в черновиках.