Парсер веб-страницы на node.js

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

Мне это понадобилось чтобы сохранять архив цен на одном сайте. Благодаря модулям request и cheerio сделать это очень легко.

Нам понадобится несколько модулей Node.js: http и request для загрузки страницы, iconv-lite для перевода кодировок, cheerio для парсинга DOM страницы, fs для сохранения файла на диск, vow для правильной синхронности и, наконец, cron чтобы делать всё это по расписанию.

var http = require('http'),
express = require('express'),
request = require('request'),
cheerio = require('cheerio'),
iconv = require('iconv-lite'),
fs = require('fs'),
vow = require('vow');

var get_table = [], full_table = [];

var cronJob = require('cron').CronJob;

//Назначим работу крону - выполнять переданные функцию раз в час - в 10-ю минуту.
//То есть, в 0:10:00, 1:10:00, 2:10:00 и так далее.
new cronJob('* 23 * * * *', function(){

var getUrlTables=function (i, url) {

//Добавляем в массив новый promise. После прохождения всего цикла получим
//массив всех promise, который очень удобно отслеживать.
get_table_promise[i]=vow.promise();

request({uri:url,method:'GET',encoding:'binary'},
function (err, res, body) {
//Получили текст страницы, теперь исправляем кодировку и
//разбираем DOM с помощью Cheerio.
var $=cheerio.load(
iconv.encode(
iconv.decode(
new Buffer(body,'binary'),
'win1251'),
'utf8')
);
table='';
//Cheerio даёт возможность навигации по DOM
//с помощью стандартных CSS-селекторов.
$('table#info_table > tr').each(function(){
table+='<tr>'+this.html()+'</tr>';
});

//Работа с таблицей, удаление ненужных строк и прочего
table=table.split('</td></tr><tr><td>');
table.splice(0,1);
table=table.join('</td></tr><tr><td>');
//Складываем результат в массив результатов и завершаем promise
full_table[i]='<tr><td>'+table;
get_table_promise[i].fulfill();
});
}

var crawlData=(function () {
var urls={1:'1',2:'2',3:'3',4:'4',5:'5',6:'6',7:'7',8:'8',9:'9',10:'10'};

//Обрабатываем каждый адрес из списка
for(i in urls)
getUrlTables(i,urls[i]);

//Передаём в all массив Promise - он дождётся завершения их всех.
vow.all(get_table_promise).spread(function (building) {

//Склеим все полученные таблицы в одну
full_info='<table>';
for(i in urls) full_info+=full_table[i];
full_info+='</table>';

//Имя файла будет формироваться из текущей даты и времени
date=new Date;
day=date.getDate(); mon=date.getMonth()+1;
yr=date.getFullYear(); hr=date.getHours();
date_str=((hr<10?'0':'')+hr)+'_'+((day<10?'0':'')+day)
+((mon<10?'-0':'-')+mon)+'-'+yr;

//Сохраняем результат
fs.open("vrosts_"+date_str+".dat","w",0644, function(err,file_handle){
if(!err){ fs.write(file_handle,full_info,null,'utf8'); }
});
});
})();

}, null, true);

Теперь живое объяснение. В цикле вызываем 10 (по количеству url) копий функции getUrlTables. Она принимает «номер» и url. Номер будет использоваться и для заполнения промизов (promise), и для сохранения результатов в массиве.

Переходим в функцию getUrlTables. Она запрашивает url, получает страницу в текстовом виде. Тут же переводим её в нужную (читай, правильную) кодировку utf8, и натравливаем на неё cheerio. Он формирует DOM-дерево страницы, и предоставляет интерфейс, похожий на jQuery.

С помощью этого интерфейса находим таблицу с именем info_table, а в ней — все строки, т.е. теги tr. Тут всё просто, используем стандартные CSS-селекторы.

Для каждого из найденных элементов получаем их html-код командой this.html() и облачаем их обратно в теги <tr>..</tr>.

Чистим полученную таблицу от мусора (в моём случае нужно было избавиться от первой строки таблицы, в которой лежал заголовок) и записываем её в массив результатов. Завершаем Promise командой fulfill.

Вернёмся к основной процедуре. Функция all модуля Vow даёт очень удобный метод отслеживать все необходимые промизы — она принимает на вход их массив. А у нас уже есть отличный массив get_table_promise — прямо его и пустим ей на вход.

После завершения всех промизов, т.е. после запроса и обработки всех страниц, склеиваем полученные таблицы в одну, генерируем имя файла вида costs_час_день-месяц-год.dat и сохраняем таблицу в этот файл. Таким образом, в файле теперь хранится html-код таблицы, который можно вставлять в какую-либо страницу сайта.

Теперь об оптимизациях. Конечно, правильным является переводить страницу в нужную кодировку сразу после загрузки; однако, в нашем случае можно переводить кодировку итоговой таблицы. Благодаря этому можно сэкономить много времени на переводах ненужных частей исходных файлов.

Также, можно было парсить страницы без применения модуля Cheerio — просто строковыми функциями. Наверняка это было бы быстрее, но выглядит как велосипедное решение.

Предостережение: не нужно создавать большую нагрузку на сайты, чем реже вы их запрашиваете — тем лучше. Также не стоит загружать сразу все нужные страницы, как в моём простом примере — лучше распределить это во времени. Поскольку у нас уже используется модуль cron, им это сделать очень легко: допустим, первый файл загружайте по заданию «* 10 * * *», второй — «* 11 * * *»,  и так далее. 10 файлов загрузятся за 10 минут, а по заданию «* 21 * * *» вы их все сохраните в файл. Ещё и промизы не понадобятся.