Давайте напишем парсер веб-страницы! Он будет раз в час загружать набор страниц, искать в них таблицу с определённым 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 * * *» вы их все сохраните в файл. Ещё и промизы не понадобятся.