Создание веб-сервера, или веб-приложения, как мы делали в первом примере, может быть интересным, также и с созданием веб-сканера. Вы знаете, это такая штука, которая скачивает страницы и делает с ними что-нибудь интересное.

Давайте начнем с чего-нибудь простого.

Класс http, который мы рассматривали ранее, предоставляет для этого несколько методов. Мы рассмотрим метод http.get, который предоставляет простой, хотя и ограниченный интерфейс.

examples/node/crawl_01.js

var http = require('http');

if (process.argv.length <= 2) {
    console.log("Usage: " + __filename + " URL");
    process.exit(-1);
}

var url = process.argv[2]

http.get(url, function(res) {
    console.log("Got response: " + res.statusCode);
}).on('error', function(e) {
    console.log("Got error: " + e.message);
});

Первая часть кода это просто проверка, передал ли пользователь URL в командной строке.

Как только мы получили url, мы вызываем http.get, передавая url и коллбек (функцию обратного вызова). Коллбек будет вызван с объектом ответа.

Давайте посмотрим, как ведет себя наш скрипт с различными параметрами.

Сначала запустим его вообще без параметров:

$ node crawl_01.js 
Usage: /Users/gabor/work/articles/code-maven/examples/node/crawl_01.js URL

Ок, он ответил, что нам нужно передать ему URL.

$ node crawl_01.js http://code-maven.com/
Got response: 200

Выглядит нормально.

Давайте запросим страницу, которой не существует:

$ node crawl_01.js http://code-maven.com/x
Got response: 404

Выглядит правильно, мы получили код http 404 как и ожидалось, но: он завис. Не знаю, сколько я ждал, пока он закончил работу. Может быть, минуту или больше. Это не совсем то, чего мы хотим, не так ли?

Сначала я подумал, что у меня какая-то проблема с сервером, поэтому я попробовал другой запрос:

$ time node crawl_01.js http://google.com/
Got response: 302

Код http 302 выглядит правильно, но после этого скрипт опять завис как и раньше.

В общем, проблема не в моем сервере. Я не был уверен, как решить эту проблему, поэтому я решил двигаться дальше. В конце концов, мы получили верный код ответа, и все еще хотим получить содержимое ответа. Так как тут все асинхронное, то содержимое совершенно не обязательно прийдет к нам в момент, когда будет вызван коллбек. Мы должны использовать объект ответа и добавить немного кода, чтобы забрать пришедшие данные.

Получение данных

examples/node/crawl_02.js

var http = require('http');

if (process.argv.length <= 2) {
    console.log("Usage: " + __filename + " URL");
    process.exit(-1);
}

var url = process.argv[2]

http.get(url, function(res) {
    console.log("Got response: " + res.statusCode);
    var content = '';
    res.on('data', function(chunk) {
        console.log('chunk ' + chunk.length);
        content += chunk;
    });
    res.on('end', function() {
        console.log('end');
        console.log(content.length);
        console.log(content);
    });
}).on('error', function(e) {
    console.log("Got error: " + e.message);
});

Как только соединение установлено и сервер отдал какой-то ответ, Node.js вызывает коллбек, передавая объект ответа. Это все можно найти в переменной res.

Мы добавили два коллбека в это объект: res.on('data', function(chunk) { ... - будет вызываться каждый раз, когда придет очередная порция данных, отправленных сервером. Если страница маленькая, то может быть он будет вызван всего один раз, но если страница большая, то может потребоваться некоторое время (даже несколько секунд?), чтобы получить все данные. Тем временем, мы можем сделать что-нибудь другое. Таким образом, эта функция будет вызываться каждый раз, когда к нам приходят какие-то данные. Параметр, передаваемый в функцию, будет содержать текущую порцию данных.

Мы создали переменную content, к которой будем добавлять текущую порцию данных.

Конечно, мы предполагаем, что размер страницы не превышает объем памяти. Это вполне разумное допущение в случае html страницы.

Второй коллбек, добавленный к событию, сработает тогда, когда мы завершим получение данных: res.on('end', function() { ...

Без этого мы не сможем быть уверены, что получили все данные, которые сервер собирался отправить. Мы просто выведем в консоль 'end' и выведем размер содержимого (content), которое мы получили от сервера.

Давайте запустим теперь наш скрипт:

$ node crawl_02.js http://code-maven.com/
Got response: 200
end
20558

Итак, главная страница Code Maven имеет размер 20,558 байт.

$ node crawl_02.js http://perlmaven.com/
Got response: 200
end
18487

По-видимому, главная страница Perl Maven меньше.

А что на счет страниц, которые зависли?

$ node crawl_02.js http://code-maven.com/x
Got response: 404
end
7641

Они больше не зависают!

$ node crawl_02.js http://google.com/
Got response: 302
end
261

И эта тоже не зависает.

По-видимому, ранее скрипт ожидал, что мы будем и дальше забирать данные. И вот почему он ждал, пока не отключился по таймауту. Хотя мне все равно не понятно, почему он не зависал в случае ответа 200.

На самом деле порция-за-порцией?

Тут есть две закомментированных строки. Если мы уберем // с первой, где код console.log('chunk ' + chunk.length);, и снова запустим скрипт:

$ node crawl_02.js http://code-maven.com/
Got response: 200
chunk 1235
chunk 12672
chunk 1408
chunk 1408
chunk 1408
chunk 1408
chunk 1026
end
20558

мы сможем увидеть, что получаем данные несколькими частями.

Содержимое в случае кода 302

Если мы уберем второй комментарий //, где код console.log(content);, тогда мы увидим содержимое страницы.

К примеру:

$ node crawl_02.js http://google.com/
Got response: 302
chunk 261
end
261
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="http://www.google.co.il/?gfe_rd=cr&ei=abcdVK1234GG8Qftxx1234">here</A>.
</BODY></HTML>

Goole.com переадресовывает на локальную версию Google.

Выводы

Это хороший старт для сканера, но многое нужно сделать. На самом деле, вот несколько сканеров на Node.js, которые предоставляют более высокий уровень абстракции.

Продвинутые сканеры

Для получения более продвинутого сканера нам нужно взглянуть на один их этих проектов: node-simplecrawler, node-crawler, and spider.