Node.js用のスクレイピングモジュール「cheerio-httpcli」の紹介

1年程前にリリースしたモジュールなんですが、最近になって

* [Node.jsで超簡単にスクレイピングを実装してみる - もやもやエンジニア](http://rei19.hatenablog.com/entry/2014/05/19/141512)
* [node.js スクレイピング cheerio-httpcli の使い方 | まとめーたー](http://www.matomater.com/16539/)

とかで紹介していただいていたようなので、便乗して本人によるアッピールもしてみようかな、と。

## Node.jsでスクレイピングする利点

何と言っても非同期で多数のサイトをガンガンスクレイピングできるところじゃないでしょうか。

一つのサイトに大量にアクセスするのは迷惑になるのでイカンですが、不特定多数のサイトに対してであるならば同時並行で処理できると処理時間の短縮にも繋がるかと思います。

## cheerio-httpcliの特徴

* WEBページの文字コードを自動判定してUTF-8に統一してくれる
* WEBページのhtmlを[cheerio](https://npmjs.org/package/cheerio)というモジュールでjQueryライクな操作ができるオブジェクトに変換してくれる

といった処理をしてくれるので、利用する側ではhtmlの内容に関する部分を書くだけで済みます。

例としてはこんな感じです。

```js
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'));
  });
});
```

まあこの例だと、Googleは元々UTF-8なのであまり恩恵はないんですが、例えば2chならShift_JISであったり、楽天ならEUC-JPだったりと、古くからあるサイトなんかは色々な文字コードで書かれているので不特定多数のWEBサイトを見に行く場合には助かると思います。

あと、`$`を使って要素を取得するあたり、まるでクライアントサイドでjQueryを使っているかのようなスマートな書き方。一昔前のように正規表現とかでゴリゴリ頑張る必要はありません。

## cheerio-httpcliの導入〜使い方

### インストール

```shell-session
$ npm install cheerio-httpcli
```

でOKです。

ちなみにソースは[GitHub](https://github.com/ktty1220/cheerio-httpcli)にあります。

### 使い方

先ほどのサンプルで大体分かってしまうと思われますので簡単な捕捉説明程度で。

まず、

```js
var client = require('cheerio-httpcli');
```
でcheerio-httpcliモジュールをロードします。受取り先の`client`変数を使ってスクレイピングを行います。

```js
client.fetch([url], [param], [callback]);
```

がWEBページの取得処理です。

`url`は取得するWEBページのURL、`param`はURLパラメータです。

http://www.hoge.jp/fuga.html?piyo=test&puyo=hehehe

みたいなWEBページを取得する場合は、

```js
client.fetch('http://www.hoge.jp/fuga.html', { piyo: 'test', puyo: 'hehehe' }, function (err, $, res) { ...
```

という指定になります。URLパラメータが必要ないページの場合は、

```js
client.fetch('http://www.hoge.jp/fuga.html', function (err, $, res) { ...
```

という感じで省略できます。省略しないで`{}`でもOKです。

`url`と`param`で指定したページを取得してUTF-8化やらjQueryっぽいオブジェクト化をした結果が`callback`に渡されてくるので、そこでWEBページの内容に対する処理を行うわけです。

`callback`には3つの引数が渡されてきます。

1. Errorオブジェクト
2. cheerio.load()でHTMLコンテンツをパースしたオブジェクト
3. requestモジュールのresponseオブジェクト

### 第1引数
Errorオブジェクトは、まあそのまんまです。URLが不正だったり、サーバーが見つからなかったり、あと文字コードの変換中にエラーが発生した場合なんかにその情報が入っています。正常にWEBページの取得ができた場合は`undefined`なので、まず1つ目の引数の内容でエラーの有無を確認しましょう。

### 第2引数
2つ目はcheerio-httpcliの肝であるjQueryっぽいオブジェクトです。別に変数名は何でもいいんですがjQueryっぽさを出すために`$`にしています。使い方はサンプルの通りです。

ちなみにcheerioには`$(XXX).remove()`とか`$(XXX).hasClass()`とか`$(XXX).append()`なんかも実装されているので取得だけでなく加工も可能です。詳しくは[cheerio本家](https://npmjs.org/package/cheerio)でご確認ください。

### 第3引数

cheerio-httpcliの内部では[request](https://npmjs.org/package/request)モジュールを使用してWEBページを取得しています。そのrequestモジュールによるWEBページ取得結果のresponseオブジェクトが3つ目の引数に入ってきます。

使い道としては、サンプルにあるとおり、レスポンスヘッダとかステータスコードとか見たい場合に使用します。

--

以上、やけに濃密になってしまった簡単な補足情報でした。

## 知っておくとより便利なネタ

### jQueryっぽいオブジェクトじゃなくて素のhtmlが欲しいんだけど

`$.html()`で取得できます。

> 追記：`$.html()`はcheerioがパースしたDOMを再構築したもののようなので、厳密には元のHTMLとは異なります。バージョン0.3.0から`fetch()`のコールバック関数の第4引数に元のHTML(UTF-8に変換済み)をセットするようにしました。

### ISO-2022-JPのページでエラーになるんだけど

cheerio-httpcliの内部で標準で使用しているモジュールは[iconv-lite](https://github.com/ashtuchkin/iconv-lite)で、こちらはISO-2022-JPの文字コードには対応していません。

そのかわり、お使いのPCのNode.js環境に[iconv](https://github.com/bnoordhuis/node-iconv)モジュールや[iconv-jp](https://github.com/kazuhisya/node-iconv-jp)モジュールがインストールされていれば実行時にそちらを優先してロードするので、ISO-2022-JP他、マイナーな文字コードのWEBページでも正常に取得できるようになります。

じゃあ何で最初からiconvとかiconv-jpを使わないかというと、これらはコンパイルが必要なネイティブモジュールで、WindowsではVisualStudioとかの開発環境がないとインストールできないというデメリットがあります。

そういったNode.jsだけ入っているライトなPCでも利用できるようにするため、コンパイルの必要のないiconv-liteを標準として採用しているという事情があるわけだったりします。

### `<header>`の`charset`にはShift_JISって書いてあって実際はEUC-JPみたいなページは大丈夫なの？

文字コードの判定には[jschardet](https://github.com/aadsm/jschardet)を使用しています。いわゆる自動判定です。このモジュールでWEBページの文字コードを確定できなかった時は`<header>`情報を参照します。

なので、jschardetで文字コードを判定できなかった、かつ、`<header>`の`charset`指定が間違えているという状況においては変換エラーや文字化けが発生すると思いますが、基本的にはjschardetが上手いことやってくれると思うので、個人的には「よっぽど運が悪ければそういうこともあるかもしれないね」位の認識でいたりします。

### POSTメソッドでWEBページを取得したいんだけど(formからsubmitした検索結果とか)

残念ながら今のところはGETメソッドのみの対応となっております。

> 追記：POSTを直接送信することはできませんが、バージョン0.3.0からフォーム送信をエミュレートできるようになりました([詳細](http://qiita.com/ktty1220/items/64168e8d416d6d8ffb45))。

### 最近人気の[note.mu](https://note.mu/)のページを取得しても中身スッカラカンなんだけど

note.muは枠だけ用意したWEBページをブラウザにロードしてから記事の内容を別途取得するという形式のサイトです。cheerio-httpcliで取得できるのはその枠部分のみの状態のhtmlなので中身スッカラカンになってしまいます。対応としては(パッと思いつくところでは)2つあります。

1. 記事を別途取得するという通信APIを調べて個別対応する

    note.muに限定するなら「["note"がAngularJSでどうやってSEO, Open Graphの対応をしているか｜和田 晃一良｜note](https://note.mu/wadako/n/n01733b19bc9c)」の記事が参考になると思います。ただ、基本的にそういったサイト毎に調査するという個別対応になるというのと、別途発生するAPI通信を解析して擬似的に実行するということをサービス側がどう感じるか、というマナーとかモラル的な問題も発生するので一概に何とも言えないところではあります。公開APIとかならまあいいとは思うんですけど。

2. GoogleBotのフリをする

    1で紹介した記事内でも触れていますが、こういった動的にコンテンツを別途取得するようなサイトはSEO対策としてGoogleBotからのアクセスに対応している場合があります。cheerio-httpcliではリクエストヘッダを指定できるので、GoogleBotのフリをすれば記事付きのhtmlが取得できるかもしれません。

    ```js
    // リクエストヘッダのUser-AgentをGoogleBotのものにする
    client.headers['User-Agent'] = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
    // GoogleBotがアクセスする _escaped_fragment_ パラメータをつけてWEBページを取得
    client.fetch('https://note.mu/〜', { _escaped_fragment_: '' }, function (err, $, res) { ...
    ```

    note.muではこの方法で記事付きのWEBページが取得できました。

__注意: どちらの方法もあまり行儀がいいとは言えないのでオススメしているわけではありません。仮にこういった方法でスクレイピングをする場合は、いわゆる自己責任というやつになります。__

## 最後に

さんざん胡散臭い方法を書いておきながらアレですけど、冒頭でも書きましたがスクレイピングをするにあたっては相手先のサーバーに迷惑がかからないよう各種マナーを守りましょう的な。

> 追記：バージョン0.3.0でいろいろ機能が追加されたので、こちらも[記事](http://qiita.com/ktty1220/items/64168e8d416d6d8ffb45)にしました。