Node.jsのスクレイピングモジュール「cheerio-httpcli」が大規模アップデートして帰ってきた

なんか大げさなタイトルですが要するにバージョンアップのお知らせだったります。

「cheerio-httpcliって何よ」という場合は[紹介記事](http://qiita.com/ktty1220/items/e9e42247ede476d04ce2)をご覧になると、こちらの記事もより分かりやすくなると思います。

簡単に説明するとこんな感じのことができるモジュールです。

```js:Google検索
var client = require('cheerio-httpcli');

// Googleで「node.js」について検索する。
client.fetch('http://www.google.com/search', { q: 'node.js' }, function (err, $, res) {
  // レスポンスヘッダを参照
  console.log(res.headers);

  // HTMLタイトルを表示
  console.log($('title').text());

  // リンク一覧を表示
  $('a').each(function (idx) {
    console.log($(this).attr('href'));
  });
});
```

さて、今回のバージョンアップで結構機能を追加しました。npmにも登録済みなので

```sh
$ npm install cheerio-httpcli
```

でインストールできます(バージョン0.3.0)。

ソースやサンプルは[こちら](https://github.com/ktty1220/cheerio-httpcli)。

では新機能の紹介です。

## :eyes: 目玉機能1: コールバック形式とプロミス形式のハイブリッド化

今まではNode.jsお馴染みのコールバック形式での実装だったので、順を追ってWEBサイトの中に潜っていく場合はいわゆるコールバック地獄が発生してしまう状態でした。

```js:コールバック形式
var client = require('cheerio-httpcli');

client.fetch('http://hogehoge/', function (err, $, res) {
  if (err) {
    return console.log(err);
  }
  client.fetch('http://hogehoge/page-1.html', function (err, $, res) {
    if (err) {
      return console.log(err);
    }
    client.fetch('http://hogehoge/page-1-1.html', function (err, $, res) {
      if (err) {
        return console.log(err);
      }
      client.fetch('http://hogehoge/page-1-2.html', function (err, $, res) {
        if (err) {
          return console.log(err);
        }
        // リンク一覧を表示
        $('a').each(function (idx) {
          console.log($(this).attr('href'));
        });
      });
    });
  });
});
```

今回のバージョンアップにより今流行のプロミス形式で呼び出すこともできるようになりました。

上記コールバック地獄もプロミス形式のメソッドチェーンを使用すればこんな感じにスッキリさせることができます。エラーの捕捉も下の方にある`catch`でまとめて面倒見てくれます。

```js:プロミス形式
client.fetch('http://hogehoge/')
.then(function (result) {
  return client.fetch('http://hogehoge/page-1.html');
})
.then(function (result) {
  return client.fetch('http://hogehoge/page-1-1.html');
})
.then(function (result) {
  return client.fetch('http://hogehoge/page-1-2.html');
})
.then(function (result) {
  var $ = result.$;
  // リンク一覧を表示
  $('a').each(function (idx) {
    console.log($(this).attr('href'));
  });
})
.catch(function (err) {
  console.log(err);
})
.finally(function () {
  // 処理完了でもエラーでも最終的に必ず実行される
});
```

### それぞれの形式の使い分け方

`fetch()`でWEBページを取得する際の引数にコールバック関数を指定した場合は処理後にそのコールバック関数が呼ばれ、コールバック関数を省略した場合は戻り値にPromiseオブジェクトが返る仕様となっています。

コールバック関数を指定した場合はPromiseオブジェクトは返しませんので、コールバック関数で処理しつつ同時進行でプロミス形式でなんちゃらするといったカオスなことはできません。

### 引数の受け取り方の違い

従来のコールバック形式の場合は

```js:コールバック形式
client.fetch('http://hogehoge/', { param1: 'fuga' }, function (err, $, res, body) {
  console.log(err);
  console.log($);
  console.log(res);
  console.log(body);
});
```

というようにコールバック関数に引数が4つ入る形で受け取りますが、プロミス形式では

```js:プロミス形式
client.fetch('http://hogehoge/', { param1: 'fuga' })
.then(function (result) {
  console.log(result.error);
  console.log(result.$);
  console.log(result.response);
  console.log(result.body);
});
```

というように1つの引数内にコールバック形式の時と同じ情報がまとめて入ります。

## :eyes: 目玉機能2: cheerioオブジェクトのプロトタイプを拡張してフォームの送信やリンクのクリックをエミュレート

`fetch`後のコールバックやプロミスの`then`で取得できるcheerioオブジェクトに以下のメソッドを独自拡張として実装しました。

### $.documentInfo()

取得したWEBページに関する情報(URLとエンコーディング)を取得できます。

```js:$.documentInfo()
client.fetch('http://hogehoge/', function (err, $, res, body) {
  var docInfo = $.documentInfo();
  console.log(docInfo.url);      // http://hogehoge/
  console.log(docInfo.encoding); // 'utf-8'
});
```

### $(link-element).click([ callback ])

`a`タグでのみ使用できます。

`href`属性に指定されているURLと取得したページのURLを組み合わせて移動先のURLを作成し、`fetch()`を実行します。`fetch()`と同様に引数の`callback`関数の有無でコールバック形式とプロミス形式の指定を切り替えられます。

```js:$(...).click()
client.fetch('http://hogehoge/')
.then(function (result) {
  // id="login"の子のリンクをクリック(プロミス形式)
  return result.$('#login a').click();
})
.then(function (result) {
  // クリックした先のURL取得後の処理
});
```

> javascriptリンクや`onclick="..."`などの動的処理には対応していません。あくまでも`href`のURLに簡単にアクセスできるための機能です。

### $(form-element).submit([ param, callback ])

`form`タグでのみ使用できます。

指定したフォーム内に配置されている`input`や`checkbox`などのフォーム部品から送信パラメータを自動作成し、`action`属性のURLに`method`属性でフォームを送信します。こちらも`callback`関数の有無でコールバック形式とプロミス形式の指定を切り替えられます。 

また、フォーム送信パラメータは`param`引数で指定した連想配列の内容で上書きできるので、利用する側ではパラメータを変更したい項目だけ指定するだけで済みます。

もちろん、cheerio-httpcliの売りである文字コードの自動判別にも対応しているので、フォームの設置されているページの文字コードに合わせたURLエンコードをして送信してくれます。

```js:$(...).submit()
client.fetch('http://hogehoge/')
.then(function (result) {
  // ユーザー名とパスワードだけ入力して、あとはフォームのデフォルト値で送信する
  var loginInfo = {
    user: 'guest',
    pass: '12345678'
  };

  // name="login"フォームを送信(コールバック形式)
  result.$('form[name=login]').submit(loginInfo, function (err, $, res, body) {
    // フォーム送信後に移動したページ取得後の処理
  });
})
```

cheerio-httpcliは内部でクッキーも保持するので、ログインが必要なページの取得なども`submit()`でログイン後に巡回できるようになります。

フォームの送信サンプルは[GitHubリポジトリ](https://github.com/ktty1220/cheerio-httpcli/tree/master/example)にもいくつか用意していあるので参考にしてみてください。

ちなみにExcite翻訳の自動実行はこんな感じです。

```js:Excite翻訳
var client = require('cheerio-httpcli');
client.fetch('http://www.excite.co.jp/world/')
.then(function (result) {
  return result.$('#formTrans').submit({
    auto_detect: 'on',
    before: '翻訳したい文字列をうんたらかんたら'
  });
})
.then(function (result) {
  console.log('翻訳結果: ' + result.$('#after').val());
});
```

> `click()`や`submit()`はスクレイピングモジュールの老舗であるPerlの[WWW::Mechanize ](http://search.cpan.org/~ether/WWW-Mechanize-1.73/lib/WWW/Mechanize.pm)にインスパイアされています。

## :eyes: 目玉機能3: ブラウザごとのUser-Agentの簡易指定機能実装

今まで特定のブラウザになりきろうとする場合は手動で`headers`にUser-Agentを指定する必要がありましたが、今バージョンから以下のようにブラウザごとのUser-Agentをセットするメソッドを実装しました。

```js:setBrowser()
var client = require('cheerio-httpcli');

client.setBrowser('chrome');    // GoogleChromeのUser-Agentに変更
client.setBrowser('android');   // AndroidのUser-Agentに変更
client.setBrowser('googlebot'); // GooglebotのUser-Agentに変更
```

ブラウザの細かいバージョンまでは指定できないので、そういった場合は今までどおり手動で`header`に指定する必要があります。

## その他のバージョンアップ内容

上記に挙げた大規模な目玉機能の他にも

* クッキーの内容を簡単に確認できるようになった
* cheerioオブジェクトにパースする前の生HTMLも取得できるようになった
* リクエスト時にRefererを自動設定するオプション追加

など、色々といい感じに取り揃えていますので、Node.jsでスクレイピングという機会があった場合にはこの記事のことも思い出してもらえればうれしいのです。

それでは素敵なスクレイピングライフを:+1: