「JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック」個人的学習ノート(第2章)

個人的学習ノートの目次

01:Webページのダウンロード

特に気になるところは無し。見直すとhttp モジュールと fs モジュールとかの理解が出来ていなかったので追記しています。
まあここ↓

var http = require('http'); // HTTPのモジュール
var fs = require('fs');     // ファイル関連モジュール

を見て、 Ruby と違ってモジュールのオブジェクトを何か変数に入れておかなきゃいけないんだな、とか、括弧が多いよな、というのが気になるところか。
あと、コールバックという概念が慣れない。順繰り進んでいく Ruby と違って、 Node.js は非同期なのでぐんぐん進んでいく。だから sleep することさえ一苦労、ってことを昨日知ったのは余談。

立ち返って http モジュールと fs モジュールのマニュアル
fs.createWriteStream(savepath)
// 出力先を指定
var outfile = fs.createWriteStream(savepath);

とあります。

fs.createWriteStream(path[, options])

Returns a new WriteStream object. (See Writable Stream).

「書き込みストリーム」ってのを作るわけですかね。ポートみたいなもんでしょう。
"See Writable Stream"って書いてますし、んじゃリンク先を見てみましょう。

Class: stream.Writable

The Writable stream interface is an abstraction for a destination that you are writing data to.
Examples of writable streams include:

「書き込み可能ストリームはデータ書き込み先を抽象化したインターフェース」…まあなんかイメージしときましょう。
結局は

  • outfile は stream.Writable クラスのオブジェクトである

ということになるかと(多分)。
まあ具体的には

  • outfile に送られるデータはパス savepath で指定されるファイル("test.html")へ書き込む

ことになります。

http.get(url, function(res) {…

つぎのところで先ほど作った outfile が使われています。

// 非同期でURLからファイルをダウンロード
http.get(url, function(res) {
  res.pipe(outfile);
  res.on('end', function () {
    outfile.close();
    console.log("ok");
  });
});

では http.get() を見ていきましょう。

http.get(options[, callback])

Since most requests are GET requests without bodies, Node.js provides this convenience method. The only difference between this method and http.request() is that it sets the method to GET and calls req.end() automatically.
Example:

http.get('http://www.google.com/index.html', (res) => {
  console.log(`Got response: ${res.statusCode}`);
  // consume response body
  res.resume();
}).on('error', (e) => {
  console.log(`Got error: ${e.message}`);
});

いまごろ気付きましたが、例示表記が ES6 のアロー関数になってますね。
脱線はともかく。
一般的な HTTP としては http.request() なのだけど、(一番頻繁に使われる)GET リクエストで済む場合には http.get() を使うとラクだよ、ってことですね。
で。JavaScript ではしょっちゅう出てくる callback 関数がとうとう現れました*1
んで callback には何が来るのか?

The optional callback parameter will be added as a one time listener for the 'response' event.

つまり GET リクエストのレスポンスが 'response' イベントのイベントリスナ(この表現だとDOM的だけど)として追加される、ということになりますかね。
では callback を見ていきます。

  res.pipe(outfile);
  res.on('end', function () {
    outfile.close();
    console.log("ok");
  });

res は GET リクエストのレスポンスであり「読み取り可能」なオブジェクトなので、おそらく「読み取り可能ストリーム」に含まれるのだと思います。確認してみましょう。

Class: stream.Readable

The Readable stream interface is the abstraction for a source of data that you are reading from. In other words, data comes out of a Readable stream.
 :
You can switch to flowing mode by doing any of the following:

You can switch back to paused mode by doing either of the following:

  • If there are no pipe destinations, by calling the stream.pause() method.
  • If there are pipe destinations, by removing any 'data' event handlers, and removing all pipe destinations by calling the stream.unpipe() method.

Note that, for backwards compatibility reasons, removing 'data' event handlers will not automatically pause the stream. Also, if there are piped destinations, then calling stream.pause() will not guarantee that the stream will remain paused once those destinations drain and ask for more data.
Examples of readable streams include:

長々と引用しましたが、HTTP responses, on the client というのが挙げられてますね。
まとめますと

  • http.get() の callback に渡される引数 res は stream.Readable クラスのオブジェクトである

ってことですね(多分)。
なので res は .isPaused(), .pause(), .pipe(), .read(), .resume(), .setEncoding(), .unpipe(), .unshift(), .wrap() の各メソッドを持ちます。

res.pipe(outfile)

で。ようやく res.pipe(outfile) まで辿り着きました。

readable.pipe(destination)

This method pulls all the data out of a readable stream, and writes it to the supplied destination, automatically managing the flow so that the destination is not overwhelmed by a fast readable stream.

具体的に書き下すと次のようになるでしょう。

  • GET コマンドのレスポンス res は読み取り可能ストリーム。この res からのデータ(レスポンス)を目的先 outfile(これは書き込み可能ストリーム)に書き込む。データ流量は自動的に制御されるので、res からのデータ流量が早い場合にも outfile 側が溢れることはない。
res.on('end', function () {…

次は res.on() が出てきました。
res は上記したように「'response' イベントのイベントリスナとして追加」されていますので、EventEmitter クラスにも属しています(と考えて良いのかな…)
…詳細はよく分からないけど、多分 jQuery.on と同じようなことになるんじゃないかと思います*2

emitter.on(eventName, listener)

Adds the listener function to the end of the listeners array for the event named eventName.
 :

server.on('connection', (stream) => {
  console.log('someone connected!');
});

Returns a reference to the EventEmitter so calls can be chained.

今回の場合、イベント名 'end' が生じたとき(つまり GET コマンドのレスポンスが終わったとき)に

    outfile.close();
    console.log("ok");

を実行します。
close() は読んだまま「ファイルクローズ」なのでしょうけど、いちおう引用。

fs.close(fd, callback)
Asynchronous close(2). No arguments other than a possible exception are given to the completion callback.

イベント 'end' についてのマニュアル記載も引用しておきます。

Event: 'end'

This event fires when there will be no more data to read.
 :

var readable = getReadableStreamSomehow();
readable.on('data', (chunk) => {
  console.log('got %d bytes of data', chunk.length);
});
readable.on('end', () => {
  console.log('there will be no more data.');
});

先ほど書いた「GET コマンドのレスポンスが終わったとき」に相当する「読めるデータが無くなったとき」に発火する、と書いてあるので、これでいいのでしょう。

emitter.on()に関する参考リンク

ブラウザ上のJavaScriptで addEventListener を使ってイベントドリブンの開発を行うが、Node上でそれを行うのための機能を提供するのが EventEmitter。

2012年記事だけど、Node.js の根幹部分なので変わってないはず。

02:HTMLの解析(リンクと画像の抽出)

cheerio-httpcliモジュールのインストール
$ npm install cheerio-httpcli
cheerio-httpcli@0.3.3 node_modules/cheerio-httpcli
├── ent@2.2.0
├── rsvp@3.1.0
├── iconv-lite@0.4.11
├── jschardet@1.3.0
├── prettyjson@1.1.3 (colors@1.1.2, minimist@1.2.0)
├── request@2.61.0 (aws-sign2@0.5.0, stringstream@0.0.4, forever-agent@0.6.1, caseless@0.11.0, tunnel-agent@0.4.1, oauth-sign@0.8.0, isstream@0.1.2, json-stringify-safe@5.0.1, extend@3.0.0, node-uuid@1.4.3, qs@4.0.0, combined-stream@1.0.5, form-data@1.0.0-rc3, mime-types@2.1.6, bl@1.0.0, tough-cookie@2.0.0, http-signature@0.11.0, har-validator@1.8.0, hawk@3.1.0)
└── cheerio@0.19.0 (entities@1.1.1, dom-serializer@0.1.0, css-select@1.0.0, htmlparser2@3.8.3, lodash@3.10.1)

この状況でのモジュールインストール状況を確認します。

$ npm ls
/Users/riocampos/Documents/ch02/02-analize
└─┬ cheerio-httpcli@0.3.3
  ├─┬ cheerio@0.19.0
  │ ├─┬ css-select@1.0.0
  │ │ ├── boolbase@1.0.0
  │ │ ├── css-what@1.0.0
  │ │ ├─┬ domutils@1.4.3
  │ │ │ └── domelementtype@1.3.0
  │ │ └── nth-check@1.0.1
  │ ├─┬ dom-serializer@0.1.0
  │ │ └── domelementtype@1.1.3
  │ ├── entities@1.1.1
  │ ├─┬ htmlparser2@3.8.3
  │ │ ├── domelementtype@1.3.0
  │ │ ├── domhandler@2.3.0
  │ │ ├── domutils@1.5.1
  │ │ ├── entities@1.0.0
  │ │ └─┬ readable-stream@1.1.13
  │ │   ├── core-util-is@1.0.1
  │ │   ├── inherits@2.0.1
  │ │   ├── isarray@0.0.1
  │ │   └── string_decoder@0.10.31
  │ └── lodash@3.10.1
  ├── ent@2.2.0
  ├── iconv-lite@0.4.11
  ├── jschardet@1.3.0
  ├─┬ prettyjson@1.1.3
  │ ├── colors@1.1.2
  │ └── minimist@1.2.0
  ├─┬ request@2.61.0
  │ ├── aws-sign2@0.5.0
  │ ├─┬ bl@1.0.0
  │ │ └─┬ readable-stream@2.0.2
  │ │   ├── core-util-is@1.0.1
  │ │   ├── inherits@2.0.1
  │ │   ├── isarray@0.0.1
  │ │   ├── process-nextick-args@1.0.2
  │ │   ├── string_decoder@0.10.31
  │ │   └── util-deprecate@1.0.1
  │ ├── caseless@0.11.0
  │ ├─┬ combined-stream@1.0.5
  │ │ └── delayed-stream@1.0.0
  │ ├── extend@3.0.0
  │ ├── forever-agent@0.6.1
  │ ├─┬ form-data@1.0.0-rc3
  │ │ └── async@1.4.2
  │ ├─┬ har-validator@1.8.0
  │ │ ├── bluebird@2.10.0
  │ │ ├─┬ chalk@1.1.1
  │ │ │ ├── ansi-styles@2.1.0
  │ │ │ ├── escape-string-regexp@1.0.3
  │ │ │ ├─┬ has-ansi@2.0.0
  │ │ │ │ └── ansi-regex@2.0.0
  │ │ │ ├─┬ strip-ansi@3.0.0
  │ │ │ │ └── ansi-regex@2.0.0
  │ │ │ └── supports-color@2.0.0
  │ │ ├─┬ commander@2.8.1
  │ │ │ └── graceful-readlink@1.0.1
  │ │ └─┬ is-my-json-valid@2.12.2
  │ │   ├── generate-function@2.0.0
  │ │   ├─┬ generate-object-property@1.2.0
  │ │   │ └── is-property@1.0.2
  │ │   ├── jsonpointer@2.0.0
  │ │   └── xtend@4.0.0
  │ ├─┬ hawk@3.1.0
  │ │ ├── boom@2.8.0
  │ │ ├── cryptiles@2.0.5
  │ │ ├── hoek@2.14.0
  │ │ └── sntp@1.0.9
  │ ├─┬ http-signature@0.11.0
  │ │ ├── asn1@0.1.11
  │ │ ├── assert-plus@0.1.5
  │ │ └── ctype@0.5.3
  │ ├── isstream@0.1.2
  │ ├── json-stringify-safe@5.0.1
  │ ├─┬ mime-types@2.1.6
  │ │ └── mime-db@1.18.0
  │ ├── node-uuid@1.4.3
  │ ├── oauth-sign@0.8.0
  │ ├── qs@4.0.0
  │ ├── stringstream@0.0.4
  │ ├── tough-cookie@2.0.0
  │ └── tunnel-agent@0.4.1
  └── rsvp@3.1.0

山のように入っています(とはいえ合計で10MBぐらい)。

2016/7/20追記:
思い出したように再開しました。まず更新。

$ npm update cheerio-httpcli
\
> spawn-sync@1.0.15 postinstall /Volumes/JetDriveLite330/prog/NodeJS/JS+Node.jsによるWebクローラー:ネットエージェント開発テクニック/ch02/02-analize/node_modules/cheerio-httpcli/node_modules/spawn-sync
> node postinstall

cheerio-httpcli@0.6.8 node_modules/cheerio-httpcli
├── he@1.1.0
├── type-of@2.0.1
├── object-assign@4.1.0
├── foreach@2.0.5
├── valid-url@1.0.9
├── tough-cookie@2.2.2
├── constants@0.0.2
├── colors@1.1.2
├── iconv-lite@0.4.13
├── rsvp@3.2.1
├── jschardet@1.4.1
├── prettyjson@1.1.3 (minimist@1.2.0)
├── require-uncached@1.0.2 (resolve-from@1.0.1, caller-path@0.1.0)
├── os-locale@1.4.0 (lcid@1.0.0)
├── spawn-sync@1.0.15 (os-shim@0.1.3, concat-stream@1.5.1)
├── request@2.73.0 (forever-agent@0.6.1, aws-sign2@0.6.0, tunnel-agent@0.4.3, oauth-sign@0.8.2, caseless@0.11.0, is-typedarray@1.0.0, isstream@0.1.2, json-stringify-safe@5.0.1, stringstream@0.0.5, aws4@1.4.1, extend@3.0.0, qs@6.2.0, node-uuid@1.4.7, combined-stream@1.0.5, mime-types@2.1.11, form-data@1.0.0-rc4, hawk@3.1.3, bl@1.1.2, http-signature@1.1.1, har-validator@2.0.6)
├── async@2.0.0 (lodash@4.13.1)
└── cheerio@0.20.0 (entities@1.1.1, dom-serializer@0.1.0, css-select@1.2.0, htmlparser2@3.8.3, jsdom@7.2.2, lodash@4.13.1)

やはりこの状況でのモジュールインストール状況を確認。

npm ls
/Users/riocampos/Documents/ch02/02-analize
├─┬ cheerio-httpcli@0.6.8
│ ├─┬ async@2.0.0
│ │ └── lodash@4.13.1
│ ├─┬ cheerio@0.20.0
│ │ ├─┬ css-select@1.2.0
│ │ │ ├── boolbase@1.0.0
│ │ │ ├── css-what@2.1.0
│ │ │ ├─┬ domutils@1.5.1
│ │ │ │ └── domelementtype@1.3.0
│ │ │ └── nth-check@1.0.1
│ │ ├─┬ dom-serializer@0.1.0
│ │ │ └── domelementtype@1.1.3
│ │ ├── entities@1.1.1
│ │ ├─┬ htmlparser2@3.8.3
│ │ │ ├── domelementtype@1.3.0
│ │ │ ├── domhandler@2.3.0
│ │ │ ├── domutils@1.5.1
│ │ │ ├── entities@1.0.0
│ │ │ └─┬ readable-stream@1.1.14
│ │ │   ├── core-util-is@1.0.2
│ │ │   ├── inherits@2.0.1
│ │ │   ├── isarray@0.0.1
│ │ │   └── string_decoder@0.10.31
│ │ ├─┬ jsdom@7.2.2
│ │ │ ├── abab@1.0.3
│ │ │ ├── acorn@2.7.0
│ │ │ ├── acorn-globals@1.0.9
│ │ │ ├── cssom@0.3.1
│ │ │ ├── cssstyle@0.2.36
│ │ │ ├─┬ escodegen@1.8.0
│ │ │ │ ├── esprima@2.7.2
│ │ │ │ ├── estraverse@1.9.3
│ │ │ │ ├── esutils@2.0.2
│ │ │ │ ├─┬ optionator@0.8.1
│ │ │ │ │ ├── deep-is@0.1.3
│ │ │ │ │ ├── fast-levenshtein@1.1.3
│ │ │ │ │ ├── levn@0.3.0
│ │ │ │ │ ├── prelude-ls@1.1.2
│ │ │ │ │ ├── type-check@0.3.2
│ │ │ │ │ └── wordwrap@1.0.0
│ │ │ │ └─┬ source-map@0.2.0
│ │ │ │   └── amdefine@1.0.0
│ │ │ ├── nwmatcher@1.3.8
│ │ │ ├── parse5@1.5.1
│ │ │ ├── sax@1.2.1
│ │ │ ├── symbol-tree@3.1.4
│ │ │ ├── webidl-conversions@2.0.1
│ │ │ ├─┬ whatwg-url-compat@0.6.5
│ │ │ │ └── tr46@0.0.3
│ │ │ └── xml-name-validator@2.0.1
│ │ └── lodash@4.13.1
│ ├── colors@1.1.2
│ ├── constants@0.0.2
│ ├── foreach@2.0.5
│ ├── he@1.1.0
│ ├── iconv-lite@0.4.13
│ ├── jschardet@1.4.1
│ ├── object-assign@4.1.0
│ ├─┬ os-locale@1.4.0
│ │ └─┬ lcid@1.0.0
│ │   └── invert-kv@1.0.0
│ ├─┬ prettyjson@1.1.3
│ │ └── minimist@1.2.0
│ ├─┬ request@2.73.0
│ │ ├── aws-sign2@0.6.0
│ │ ├── aws4@1.4.1
│ │ ├─┬ bl@1.1.2
│ │ │ └─┬ readable-stream@2.0.6
│ │ │   ├── core-util-is@1.0.2
│ │ │   ├── inherits@2.0.1
│ │ │   ├── isarray@1.0.0
│ │ │   ├── process-nextick-args@1.0.7
│ │ │   ├── string_decoder@0.10.31
│ │ │   └── util-deprecate@1.0.2
│ │ ├── caseless@0.11.0
│ │ ├─┬ combined-stream@1.0.5
│ │ │ └── delayed-stream@1.0.0
│ │ ├── extend@3.0.0
│ │ ├── forever-agent@0.6.1
│ │ ├─┬ form-data@1.0.0-rc4
│ │ │ └── async@1.5.2
│ │ ├─┬ har-validator@2.0.6
│ │ │ ├─┬ chalk@1.1.3
│ │ │ │ ├── ansi-styles@2.2.1
│ │ │ │ ├── escape-string-regexp@1.0.5
│ │ │ │ ├─┬ has-ansi@2.0.0
│ │ │ │ │ └── ansi-regex@2.0.0
│ │ │ │ ├─┬ strip-ansi@3.0.1
│ │ │ │ │ └── ansi-regex@2.0.0
│ │ │ │ └── supports-color@2.0.0
│ │ │ ├─┬ commander@2.9.0
│ │ │ │ └── graceful-readlink@1.0.1
│ │ │ ├─┬ is-my-json-valid@2.13.1
│ │ │ │ ├── generate-function@2.0.0
│ │ │ │ ├─┬ generate-object-property@1.2.0
│ │ │ │ │ └── is-property@1.0.2
│ │ │ │ ├── jsonpointer@2.0.0
│ │ │ │ └── xtend@4.0.1
│ │ │ └─┬ pinkie-promise@2.0.1
│ │ │   └── pinkie@2.0.4
│ │ ├─┬ hawk@3.1.3
│ │ │ ├── boom@2.10.1
│ │ │ ├── cryptiles@2.0.5
│ │ │ ├── hoek@2.16.3
│ │ │ └── sntp@1.0.9
│ │ ├─┬ http-signature@1.1.1
│ │ │ ├── assert-plus@0.2.0
│ │ │ ├─┬ jsprim@1.3.0
│ │ │ │ ├── extsprintf@1.0.2
│ │ │ │ ├── json-schema@0.2.2
│ │ │ │ └── verror@1.3.6
│ │ │ └─┬ sshpk@1.8.3
│ │ │   ├── asn1@0.2.3
│ │ │   ├── assert-plus@1.0.0
│ │ │   ├── dashdash@1.14.0
│ │ │   ├── ecc-jsbn@0.1.1
│ │ │   ├── getpass@0.1.6
│ │ │   ├── jodid25519@1.0.2
│ │ │   ├── jsbn@0.1.0
│ │ │   └── tweetnacl@0.13.3
│ │ ├── is-typedarray@1.0.0
│ │ ├── isstream@0.1.2
│ │ ├── json-stringify-safe@5.0.1
│ │ ├─┬ mime-types@2.1.11
│ │ │ └── mime-db@1.23.0
│ │ ├── node-uuid@1.4.7
│ │ ├── oauth-sign@0.8.2
│ │ ├── qs@6.2.0
│ │ ├── stringstream@0.0.5
│ │ └── tunnel-agent@0.4.3
│ ├─┬ require-uncached@1.0.2
│ │ ├─┬ caller-path@0.1.0
│ │ │ └── callsites@0.2.0
│ │ └── resolve-from@1.0.1
│ ├── rsvp@3.2.1
│ ├─┬ spawn-sync@1.0.15
│ │ ├─┬ concat-stream@1.5.1
│ │ │ ├── inherits@2.0.1
│ │ │ ├─┬ readable-stream@2.0.6
│ │ │ │ ├── core-util-is@1.0.2
│ │ │ │ ├── isarray@1.0.0
│ │ │ │ ├── process-nextick-args@1.0.7
│ │ │ │ ├── string_decoder@0.10.31
│ │ │ │ └── util-deprecate@1.0.2
│ │ │ └── typedarray@0.0.6
│ │ └── os-shim@0.1.3
│ ├── tough-cookie@2.2.2
│ ├── type-of@2.0.1
│ └── valid-url@1.0.9
└─┬ request@2.61.0
  ├── aws-sign2@0.5.0
  ├─┬ bl@1.0.0
  │ └─┬ readable-stream@2.0.2
  │   ├── core-util-is@1.0.1
  │   ├── inherits@2.0.1
  │   ├── isarray@0.0.1
  │   ├── process-nextick-args@1.0.3
  │   ├── string_decoder@0.10.31
  │   └── util-deprecate@1.0.1
  ├── caseless@0.11.0
  ├─┬ combined-stream@1.0.5
  │ └── delayed-stream@1.0.0
  ├── extend@3.0.0
  ├── forever-agent@0.6.1
  ├─┬ form-data@1.0.0-rc3
  │ └── async@1.4.2
  ├─┬ har-validator@1.8.0
  │ ├── bluebird@2.10.0
  │ ├─┬ chalk@1.1.1
  │ │ ├── ansi-styles@2.1.0
  │ │ ├── escape-string-regexp@1.0.3
  │ │ ├─┬ has-ansi@2.0.0
  │ │ │ └── ansi-regex@2.0.0
  │ │ ├─┬ strip-ansi@3.0.0
  │ │ │ └── ansi-regex@2.0.0
  │ │ └── supports-color@2.0.0
  │ ├─┬ commander@2.8.1
  │ │ └── graceful-readlink@1.0.1
  │ └─┬ is-my-json-valid@2.12.2
  │   ├── generate-function@2.0.0
  │   ├─┬ generate-object-property@1.2.0
  │   │ └── is-property@1.0.2
  │   ├── jsonpointer@2.0.0
  │   └── xtend@4.0.0
  ├─┬ hawk@3.1.0
  │ ├── boom@2.8.0
  │ ├── cryptiles@2.0.5
  │ ├── hoek@2.14.0
  │ └── sntp@1.0.9
  ├─┬ http-signature@0.11.0
  │ ├── asn1@0.1.11
  │ ├── assert-plus@0.1.5
  │ └── ctype@0.5.3
  ├── isstream@0.1.2
  ├── json-stringify-safe@5.0.1
  ├─┬ mime-types@2.1.6
  │ └── mime-db@1.18.0
  ├── node-uuid@1.4.3
  ├── oauth-sign@0.8.0
  ├── qs@4.0.0
  ├── stringstream@0.0.4
  ├── tough-cookie@2.0.0
  └── tunnel-agent@0.4.1
cheerio-httpcliモジュールについて

作者による紹介など。特に Qiita 記事は読んでおいて良かった。
でも。 npm の readme が日本語なんだけど、いいのかい?

2016/7/20追記:

cheerio-httpcliの特徴
  • WEBページの文字コードを自動判定してUTF-8に統一してくれる
  • WEBページのhtmlをcheerioというモジュールでjQueryライクな操作ができるオブジェクトに変換してくれる

といった処理をしてくれるので、利用する側ではhtmlの内容に関する部分を書くだけで済みます。
JavaScript - Node.js用のスクレイピングモジュール「cheerio-httpcli」の紹介 - Qiita

この「jQuery ライク」というところを知っていないと、

var client = require('cheerio-httpcli');
 :
client.fetch(url, param, function (err, $, res) {
 :
});

の cheerio-httpcli.fetch() メソッドの関数の2番目の引数がなぜ $ なのか、がよく分からなかったりする、ように思う。逆に、 jQuery ライクであると分かれば、 $ 以外の引数名で使いたくないようになると思います。
では、引数のことをもうちょっとつっこんでいきましょう。

fetch() メソッドのcallback関数の第2引数($
fetch(url[, get-param, callback])

urlで指定したWEBページをGETメソッドで取得し、文字コードの変換とHTMLパースを行いcallback関数に返します。
callback関数には以下の4つの引数が渡されます。

  1. Errorオブジェクト
  2. cheerio.load()でHTMLコンテンツをパースしたオブジェクト(独自拡張版)
  3. requestモジュールのresponseオブジェクト(独自拡張版)
  4. UTF-8に変換したHTMLコンテンツ

GET時にパラメータを付加する場合は第2引数のget-paramに連想配列で指定します。
cheerio-httpcli : npm

第2引数

2つ目はcheerio-httpcliの肝であるjQueryっぽいオブジェクトです。別に変数名は何でもいいんですがjQueryっぽさを出すために$にしています。
JavaScript - Node.js用のスクレイピングモジュール「cheerio-httpcli」の紹介 - Qiita

「cheerio.load()でHTMLコンテンツをパースしたオブジェクト」ってのが cheerio の jQuery ライクなオブジェクトですね。
では cheerio のドキュメントを確認。

Selectors

Cheerio's selector implementation is nearly identical to jQuery's, so the API is very similar.
cheerio : npm

Attributes のところを見ていると、 jQuery で見かけたメソッドと同名のものばかりです。私はさほど jQuery を使っていないのですが、でもこれならば理解出来ます。

fetch() メソッドのcallback関数の第3引数(res

「requestモジュールのresponseオブジェクト(独自拡張版)」とありました。

第3引数

cheerio-httpcliの内部ではrequestモジュールを使用してWEBページを取得しています。そのrequestモジュールによるWEBページ取得結果のresponseオブジェクトが3つ目の引数に入ってきます。
JavaScript - Node.js用のスクレイピングモジュール「cheerio-httpcli」の紹介 - Qiita

では request モジュールの response オブジェクトを見てみましょう。

Request emits a "response" event when a response is received. The response argument will be an instance of http.IncomingMessage.
request : npm

http.IncomingMessage のインスタンスだと書いてますね。リンク先を見にいってみます。

http.IncomingMessage

An IncomingMessage object is created by http.Server or http.ClientRequest and passed as the first argument to the 'request' and 'response' event respectively. It may be used to access response status, headers and data.
HTTP Node.js v4.0.0 Manual & Documentation

このあとにプロパティとして .httpVersion, .headers, .rawHeaders, .statusCode, .statusMessage などがリストされていました。ので、第3引数 res にはそれらのプロパティが含まれているわけですね。
なお、Object.keys() でプロパティを列挙させてみると以下のようになりました。

['_readableState', 'readable', 'domain', '_events', '_eventsCount', '_maxListeners', 'socket', 'connection', 'httpVersionMajor', 'httpVersionMinor', 'httpVersion', 'complete', 'headers', 'rawHeaders', 'trailers', 'rawTrailers', 'upgrade', 'url', 'method', 'statusCode', 'statusMessage', 'client', '_consuming', '_dumped', 'req', 'request', 'toJSON', 'caseless', 'read', 'elapsedTime', 'body', 'cookies']
$("a").each(function(idx) {…

jQuery に慣れていない私には疑問が生じました。

  $("a").each(function(idx) {
    var text = $(this).text();
    var href = $(this).attr('href');
    console.log(text+":"+href);
  });

なぜ jQuery.each() の引数の関数にある引数 idx が関数内で使われていないんだろう。

jQuery のマニュアルに頼ります。

function(index, element) 各繰り返し処理で実行したい関数を指定します。

  • index:各要素のインデックス番号です。
  • element:繰り返し処理中の要素を参照します。

.each() | jQuery 1.9 日本語リファレンス | js STUDIO

第1引数はインデックスだったのですね。
ここのサンプルを見ていると、どうやらどちらの引数も省略可能だと知りました。ので

  $("a").each(function() {
    var text = $(this).text();
    var href = $(this).attr('href');
    console.log(text+":"+href);
  });

でも問題なく動作しました。
さらに $(this) の代わりに第2引数 elem を使った $(elem) を用いて

  $("a").each(function(idx, elem) {
    var text = $(elem).text();
    var href = $(elem).attr('href');
    console.log(text+":"+href);
  });

でも同様に動作すると知りました。
しかし、世間のだいたいのスクリプトにおいて、このような $(elem) は使われないですね。関数内だと $(this) にしておけば確実だからでしょうか。

相対URLを絶対URLに変換

url モジュールの url.resolve() メソッドを使います。var url = require('url')を宣言してから使います。

url.resolve(from, to)
Take a base URL, and a href URL, and resolve them as a browser would for an anchor tag. Examples:

url.resolve('/one/two/three', 'four')         // '/one/two/four'
url.resolve('http://example.com/', '/one')    // 'http://example.com/one'
url.resolve('http://example.com/one', '/two') // 'http://example.com/two'

URL Node.js v4.0.0 Manual & Documentation

絶対 URL に変換するというのは正確では無く、基準となる URL from から見て相対 URL to がどう表現されるかなるか、と見るわけですね。もちろん基準となる URL が絶対 URL であれば結果も絶対 URL になるわけで。
ちなみに url モジュールのプロパティは .href, .protocol, .slashes, .host, .auth, .hostname, .port, .pathname, .search, .path, .query, .hash、メソッドは .parse(), .format(), .resolve() です。

2016/7/20追記:
cheerio-httpcli バージョン0.6.0以降では以下のようなメソッドが追加されたそうです。

【新機能3】完全な形のURLを取得
<a id="top" href="../index.html">トップページ</a>

上記のようなリンクの場合、$('a').attr('href')とすると../index.htmlが返ってきますが、ここから完全なURLにするという手間を省くことができるメソッドを追加しました。
先ほどのリンクの完全な形のURLを取得したい場合は以下のようにします。

console.log($('a#top').url()); // => http://foo.bar.baz/index.html

url()はa要素およびimg要素で使用できます。

a要素およびimg要素のときは .attr('href') の代わりに .url() を使えばフルパスになる、ってことで。

画像ファイルを抽出

ソースコードにはダウンロード先 URL として

var url = "http://ja.wikipedia.org/wiki/イヌ";

とあるのですが、OS X Yosemiteの環境では「404 Not Found」になってしまいます。http でも https でも同様。やはり URL エンコードしておかないとダメなようです。 URL を一旦ブラウザに貼り付けてアクセスし、エンコードされたものをソースコードに戻してみました。

var url = "https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%8C";

とすればOKでした(http でも OK)。
ちなみにデバッグとして、client.fetch() のコールバック関数の第3引数 res を使ってコールバック関数内で

  console.log(res.statusCode);
  console.log(res.statusMessage);

を追加してみました。すると

404
Not Found

と返ってきました。

requestモジュール

ではインストールしましょう。

$ npm install request
request@2.61.0 node_modules/request
├── aws-sign2@0.5.0
├── forever-agent@0.6.1
├── caseless@0.11.0
├── stringstream@0.0.4
├── oauth-sign@0.8.0
├── tunnel-agent@0.4.1
├── isstream@0.1.2
├── json-stringify-safe@5.0.1
├── extend@3.0.0
├── node-uuid@1.4.3
├── combined-stream@1.0.5 (delayed-stream@1.0.0)
├── qs@4.0.0
├── form-data@1.0.0-rc3 (async@1.4.2)
├── mime-types@2.1.6 (mime-db@1.18.0)
├── http-signature@0.11.0 (assert-plus@0.1.5, asn1@0.1.11, ctype@0.5.3)
├── bl@1.0.0 (readable-stream@2.0.2)
├── tough-cookie@2.0.0
├── hawk@3.1.0 (cryptiles@2.0.5, sntp@1.0.9, boom@2.8.0, hoek@2.14.0)
└── har-validator@1.8.0 (commander@2.8.1, chalk@1.1.1, bluebird@2.10.0, is-my-json-valid@2.12.2)

またたくさん入った…。
そういえば http モジュールも fs モジュールも理解せずに飛ばしてきてしまった。確認しなきゃ。

(以後更新予定)

*1:callback は Ruby だとブロックに相当するわけでしょう。Ruby のブロックは引数の位置に入らないので見やすい。でも callback は引数の丸括弧のなかに入ってくるので、引数の丸括弧の中身がとても長くなってしまって、ホント見辛いんですよね。ぶつぶつ。

*2:誤解してる。jQuery.on はイベント登録メソッド。