node package manager

cheerio-httpcli

http client module with cheerio & iconv(-lite) & promise

cheerio-httpcli - Node.js用WEBスクレイピングモジュール

Node.jsでWEBページのスクレイピングを行う際に必要となる文字コードの変換と、cheerioによってパースしたHTMLをjQueryのように操作できるHTTPクライアントモジュールです。

  1. 取得先WEBページの文字コードを自動で判定してHTMLをUTF-8に変換
  2. UTF-8に変換したHTMLをjQueryのように操作可能
  3. フォームの送信やリンクのクリックをエミュレート
  4. Node.jsお馴染みのコールバック形式と最近の流行であるプロミス形式どちらにも対応
  5. 同期リクエスト対応
  6. $('img')要素画像のダウンロード(LazyLoad対応)
  7. $('a,img')要素のURLを絶対パスで取得可能
  8. ブラウザ指定による簡単User-Agent切り替え機能
  9. 現在のクッキーの内容を簡単に取得(読み取り専用)

静的なHTMLをベースに処理するモジュールなのでSPAなどクライアントサイドのJavaScriptによってコンテンツを取得/変更するタイプのWEBページには対応していません。

var client = require('cheerio-httpcli');
 
// Googleで「node.js」について検索する。 
var word = 'node.js';
 
client.fetch('http://www.google.com/search', { q: word }, function (err$resbody) {
  // レスポンスヘッダを参照 
  console.log(res.headers);
 
  // HTMLタイトルを表示 
  console.log($('title').text());
 
  // リンク一覧を表示 
  $('a').each(function (idx) {
    console.log($(this).attr('href'));
  });
});

同梱の「example/google.js」はGoogle検索結果の一覧を取得するサンプルです。参考にしてください。

npm install cheerio-httpcli

urlで指定したWEBページをGETメソッドで取得し、文字コードの変換とHTMLパースを行いcallback関数に返します。

callback関数には以下の4つの引数が渡されます。

  1. Errorオブジェクト
  2. cheerio.load()でHTMLコンテンツをパースしたオブジェクト(独自拡張版)
  3. requestモジュールのresponseオブジェクト(独自拡張版)
  4. UTF-8に変換したHTMLコンテンツ
  • GET時にパラメータ(?foo=bar&hoge=fuga)を付加する場合は第2引数のget-paramに連想配列で指定します。

  • 予め取得対象のWEBページのエンコーディングが分かっている場合はencodesjiseuc-jpなどの文字列をセットすることで自動判定による誤判定(滅多に発生しませんが)を防止することができます。

  • get-paramencode、場合によってはcallbackも省略可能です。

    // get-paramとencodeを省略 => GETパラメータ指定なし & エンコーディング自動判定 
    client.fetch('http://hogehoge.com/fuga.html', function (err$resbody) {
      ...
    });
     
    // get-paramを省略 => GETパラメータ指定なし & エンコーディング指定(sjis) 
    client.fetch('http://hogehoge.com/fuga.html', 'sjis', function (err$resbody) {
      ...
    });
     
    // encodeを省略 => GETパラメータ指定(?foo=bar) & エンコーディング自動判定 
    client.fetch('http://hogehoge.com/fuga.html', { foo: 'bar' }, function (err$resbody) {
      ...
    });
     
    // url以外全部省略 => GETパラメータ指定なし & エンコーディング自動判定 & プロミス形式(後述) 
    client.fetch('http://hogehoge.com/fuga.html')
    .then(function (result) {
      ...
    });

fetch()の第3引数であるcallback関数を省略すると、戻り値としてPromiseオブジェクトが返ります。先ほどのサンプルをプロミス形式で呼び出すと以下のようになります。

var client = require('cheerio-httpcli');
 
// Googleで「node.js」について検索する。 
var word = 'node.js';
 
// callbackを指定しなかったのでPromiseオブジェクトが返る 
var p = client.fetch('http://www.google.com/search', { q: word })
p.then(function (result) {
  // レスポンスヘッダを参照 
  console.log(result.response.headers);
 
  // HTMLタイトルを表示 
  console.log(result.$('title').text());
 
  // リンク一覧を表示 
  result.$('a').each(function (idx) {
    console.log(result.$(this).attr('href'));
  });
})
 
p.catch(function (err) {
  console.log(err);
});
 
p.finally(function () {
  console.log('done');
});

callback関数を指定しないfetch()の戻り値をp変数が受け取り、そのp変数を通してthen(正常終了時)およびcatch(エラー発生時)の処理を行っています。また、正常終了でもエラーでも必ず最後に通る処理であるfinallyも使用できます。

thenに渡されるパラメータはコールバック形式で呼び出した際にcallback関数に渡されるものと同じですが、第1引数のオブジェクトにまとめて入っているという点で異なるのでご注意ください。

  • error ... Errorオブジェクト
  • $ ... cheerio.load()でHTMLコンテンツをパースしたオブジェクト(独自拡張版)
  • response ... requestモジュールのresponseオブジェクト(独自拡張版)
  • body ... UTF-8に変換したHTMLコンテンツ
.then(function (result) {
  console.log(result);
  // => { 
  //      error: ..., 
  //      $: ..., 
  //      response: ..., 
  //      body: ... 
  //    }; 
});

とあるサイトのトップページにアクセスして、その中のとあるページに移動して...というように順を追ってWEBページに潜っていきたい場合などもメソッドチェーンでこんな感じに書くことができます。

var client = require('cheerio-httpcli');
 
client.fetch(<TOPページのURL>)
.then(function (result) {
  // 何か処理 
  return client.fetch(<ページAのURL>);    // Promiseオブジェクトを返す 
})
.then(function (result) {
  // 何か処理 
  return client.fetch(<ページA-1のURL>);  // Promiseオブジェクトを返す 
})
.then(function (result) {
  // 何か処理 
  return client.fetch(<ページA-2のURL>);  // Promiseオブジェクトを返す 
})
.catch(function (err) {
  // どこかでエラーが発生 
  console.log(err);
})
.finally(function () {
  // TOPページ => ページA => ページA-1 => ページA-2の順にアクセスした後に実行される 
  // エラーが発生した場合もcatchの処理後に実行される 
  console.log('done');
});

実体はrsvpのPromiseオブジェクトなので、詳細はそちらのドキュメントをご覧ください。

fetch()の第3引数のcallback関数を指定した場合はPromiseオブジェクトは返しません。したがってコールバック形式で呼び出しつつPromiseオブジェクトで何かをするということはできません。

非同期で実行されるfetch()の同期版(リクエストが完了するまで次の行に進まない)となります。fs.readFile()に対するfs.readFileSync()の関係と同じような意味合いになります。

  • 呼び出し時のパラメータはfetch()のプロミス形式と同様です。
  • 戻り値はプロミス形式のthenに渡されるオブジェクトと同様の形式です。
var client = require('cheerio-httpcli');
 
var result1 = client.fetchSync('http://foo.bar.baz/');
console.log(result1);
// => { 
//    error: ..., 
//    $: ..., 
//    response: ..., 
//    body: ... 
// } 
 
console.log(result1.$('title')); // => http://foo.bar.baz/のタイトルが表示される 
 
var result2 = client.fetchSync('http://hoge.fuga.piyo/');
console.log(result2.$('title')); // => http://hoge.fuga.piyo/のタイトルが表示される 
  • 同期リクエストは、外部スクリプトをspawnSync()で実行して処理が完了するまで待つ、という形で実装しているのでパフォーマンスは非常に悪いです(非同期リクエストの10倍程度は時間がかかります)。したがって、実装しておいてなんですが、基本は非同期リクエストで処理を行い、どうしてもここだけは同期リクエストにしたいといった場合のみ、という使い方をお勧めします。
  • 同期リクエストの戻り値内のレスポンスはresponse.toJSON()されたものなので非同期版とは内容が若干異なります。statusCodeやheaders、requestなどの主要プロパティは共通して使用できるので特に大きな問題はないかと思いますが、特殊な使い方をする場合には注意が必要です。

ブラウザごとのUser-Agentをワンタッチで設定するメソッドです。

var client = require('cheerio-httpcli');
 
client.setBrowser('chrome');    // GoogleChromeのUser-Agentに変更 
client.setBrowser('android');   // AndroidのUser-Agentに変更 
client.setBrowser('googlebot'); // GooglebotのUser-Agentに変更 

User-Agentを指定したブラウザのものに変更した場合はtrue、対応していないブラウザを指定するとUser-Agentは変更されずにfalseが返ります。

対応しているブラウザは以下のとおりです。

  • ie
  • edge
  • chrome default
  • firefox
  • opera
  • vivaldi
  • safari
  • ipad
  • iphone
  • ipod
  • android
  • googlebot

なお、細かいバージョンの指定まではできないので、そういった指定も行いたい場合は手動で以下のようにUser-Agentを指定してください。

// IE6のUser-Agentを手動で指定 
client.headers['User-Agent'] = 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)';

cheerio-httpcliは、実行時にインストールされているiconv系のモジュールをチェックして利用するモジュールを自動的に決定しています。優先順位は以下のとおりです。

  1. iconv
  2. iconv-lite

iconv-liteはcheerio-httpcliのインストール時に依存モジュールとして一緒にインストールされますが、ネイティブモジュールであるiconvがインストールされている場合、処理速度や対応文字コードの多さというメリットがあるそちらを優先してロードするようになっています。

バージョン0.3.1までは最優先はiconv-jpでしたが、長期間メンテされていないこととNode.js 0.12系でコンパイルできなくなっている現状を考慮してデフォルトの変換エンジン候補から除外しました。

あえてsetIconvEngine()でiconv-jpを指定することは可能ですが非推奨です。

このメソッドは自動的にロードされたiconv系モジュールを破棄して、使用するiconv系モジュールを手動で指定するためのものです。モジュールテスト時の切り替え用メソッドなので基本的には実用性はありません。

iconv-module-nameには使用するiconv系モジュール名('iconv', 'iconv-lite', 'iconv-jp')のいずれかの文字列を指定します。

var client = require('cheerio-httpcli');
 
// あえてiconv-liteを使用 
client.setIconvEngine('iconv-lite');
client.fetch( ...

cheerio-httpcliはシングルインスタンスで動作するモジュールなので、そのプロセスが動作している間は各種設定やクッキーを共有して保持し続けます。

reset()を実行すると、設定情報やクッキーをすべて初期化してプロセス起動時と同じ状態に戻します。

cheerio-httpcliのバージョン情報です。

requestモジュールで使用するリクエストヘッダ情報の連想配列です。デフォルトでは何も指定されていませんが、fetch()実行時にUser-Agentが空の場合は自動的にUser-AgentにGoogleChromeの情報が入ります。

requestモジュールで指定するタイムアウト情報をミリ秒で指定します。デフォルトは30000(30秒)となっています。

サーバーとの通信にgzip転送を使用するかどうかを真偽値で指定します。デフォルトはtrue(gzip転送する)です。

リファラを自動でセットするかどうかの指定です。trueにすると1つ前にfetch()したページのURLが自動でリクエストヘッダのRefererにセットされます。デフォルトはtrueです。

<meta http-equiv="refresh" content="0;URL=...">といったMETAタグをHTML内に発見した場合に自動でそのURLにリダイレクトします。ただし、<!--[if IE]>~<![endif]-->のようなIE条件付きコメント内にある場合はリダイレクトしません。デフォルトはfalseです。

Google検索をする場合はfollowMetaRefreshfalseにしてください。

Googleの検索結果HTMLには常にMETAタグのRefresh指定が入っているので(しかも毎回微妙に異なるURL)、リダイレクトがループして最終的にエラーになります。

fetch()などで受信するデータの限界量を数値(バイト数)で指定します。この値を超えるサイズを受信した段階でエラーが発生します。ユーザーから入力されたURLを解析する用途などにおいて、不用意に大きいデータを読み込んでしまい回線を占有する可能性がある場合に指定しておいた方が良いでしょう。

画像のダウンロード時には適用されません。

デフォルトはnull(制限なし)です。

var client = require('cheerio-httpcli');
 
// 受信料制限を1MBに指定 
client.maxDataSize = 1024 * 1024;
 
// 1MB以上ののHTMLを指定 
client.fetch('http://big.large.huge/data.html', function (err$resbody) {
  console.log(err.message); // => 'data size limit over' 
});

なお、maxDataSizeを超えた場合は途中まで受信したデータは破棄されます。

trueにするとリクエストの度にデバッグ情報を出力します(stderr)。デフォルトはfalseです。

var client = require('cheerio-httpcli');
 
// デバッグ表示ON 
client.debug = true;
client.fetch( ...

ファイルダウンロードマネージャーオブジェクトです。このオブジェクトを通してファイルダウンロードに関する設定を行います(詳細は$(image-element).download()を参照)。

cheerio-httpcliではcheerioオブジェクトのprototypeを拡張していくつかの便利メソッドを実装しています。

取得したWEBページに関する情報(urlencoding)を取得できます。

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

fetch()で指定したURLがリダイレクトされた場合はリダイレクト先のURLがurlに入ります。encodingに関しても同様で、最終的に到達したページのエンコーディングが入ります。

a要素もしくは送信ボタン系要素で使用可能ですが、それぞれ挙動が異なります。

href属性に指定されているURLと取得したページのURLを組み合わせて移動先のURLを作成し、fetch()を実行します。

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

注意点として、このclick()メソッドはjavascriptリンクやonclick="..."などの動的処理には対応していません。あくまでもhrefのURLに簡単にアクセスできるための機能です。

input[type=submit]button[type=submit]input[type=image]要素が対象となります。

押された送信ボタンが所属するフォーム内に配置されているinputcheckboxなどのフォーム部品から送信パラメータを自動作成し、action属性のURLにmethod属性でフォーム送信を実行します。

client.fetch('http://hogehoge/')
.then(function (result) {
  var form = $('form[name=login]');
 
  // ユーザー名とパスワードをセット(field()については後述) 
  form.field({
    user: 'guest',
    pass: '12345678'
  });
 
  // 送信ボタンを押してフォームを送信(コールバック形式) 
  // ※上で指定したuserとpass以外はデフォルトのパラメータとなる 
  form.find('input[type=submit]').click(function (err$resbody) {
    // フォーム送信後に移動したページ取得後の処理 
  });
})

cheerio-httpcliは内部でクッキーも保持するので、ログインが必要なページの取得などもこのフォーム送信でログインした後に巡回できるようになります。

なお、こちらも動的処理であるonsubmit="xxx"や送信ボタンのonclick="..."には対応していません。

  • $(...).click()時の対象要素が複数ある場合は先頭の要素に対してのみ処理が行われます。
  • fetch()と同様に引数のcallback関数の有無でコールバック形式とプロミス形式の指定を切り替えられます。

非同期で実行されるclick()の同期版となります。

  • 戻り値はプロミス形式のthenに渡されるオブジェクトと同様の形式です。
var client = require('cheerio-httpcli');
 
// fetch()は非同期で行ってその中で同期リクエストする場合 
client.fetch('http://foo.bar.baz/', function (err$resbody) {
  var result = $('a#login').clickSync();
  console.log(result);
  // => { 
  //      error: ..., 
  //      $: ..., 
  //      response: ..., 
  //      body: ... 
  //    } 
});
var client = require('cheerio-httpcli');
 
// フォームのあるページに同期リクエスト 
var result1 = client.fetch('http://foo.bar.baz/');
var form = result1.$('form[name=login]');
 
form.field({
  user: 'guest',
  pass: '12345678'
});
 
// フォーム送信も同期リクエスト 
var result2 = form.find('input[type=submit]').clickSync();
 
// フォーム送信後に移動したページ取得後の処理 
  .
  .
  .

form要素でのみ使用できます。

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

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

client.fetch('http://hogehoge/')
.then(function (result) {
  // ユーザー名とパスワードだけ入力して、あとはフォームのデフォルト値で送信する 
  var loginInfo = {
    user: 'guest',
    pass: '12345678'
  };
 
  // name="login"フォームを送信(コールバック形式) 
  result.$('form[name=login]').submit(loginInfo, function (err$resbody) {
    // フォーム送信後に移動したページ取得後の処理 
  });
})

その他の仕様は$(submit-element).click()と同様です。

  • onsubmit="xxx"には対応していません。
  • $(...)で取得したform要素が複数ある場合は先頭の要素に対してのみ実行されます。

$(submit-element).click()は押したボタンのパラメータがサーバーに送信されますが、$(form-element).submit()は送信系ボタンのパラメータをすべて除外した上でサーバーに送信します。

<form>
  <input type="text" name="user" value="guest">
  <input type="submit" name="edit" value="edit">
  <input type="submit" name="delete" value="delete">
</form>

上記フォームは1フォーム内に複数のsubmitボタンがあります。それぞれのメソッドによるこのフォームの送信時のパラメータは以下のようになります。

// $(submit-element).click()の場合 
$('[name=edit]').click(); // => '?user=guest&edit=edit' 
 
// $(form-element).submit()の場合 
$('form').submit(); // => '?user=guest' 

このように1フォーム内に複数のsubmitボタンがある場合、サーバー側では押されたボタンのパラメータで処理を分岐させている可能性があるので、$('form').submit()だと正常な結果が得られないかもしれません。

実際にブラウザから手動でフォームを送信した挙動に近いのは$(submit-element).click()になります。

非同期で実行されるsubmit()の同期版となります。戻り値はプロミス形式のthenに渡されるオブジェクトと同様の形式です。

  • 呼び出し時のパラメータはsubmit()のプロミス形式と同様です。
  • 戻り値はプロミス形式のthenに渡されるオブジェクトと同様の形式です。
var client = require('cheerio-httpcli');
 
// トップページにアクセス(ここも同期リクエストにすることも可能) 
client.fetch('http://foo.bar.baz/', function (err$resbody) {
  // 同期リクエストでログインページに移動 
  var result1 = $('a#login').clickSync();
  // 同期リクエストでログインフォーム送信 
  var result2 = result1.$('form[name=login]').submitSync({
    account: 'guest',
    password: 'guest'
  });
  // ログイン結果確認 
  console.log(result2.response.statusCode);
});

$(...).css()$(...).attr()と同じ感覚でフォーム部品の値を取得/指定できるメソッドです。呼び出し時の引数によって動作が変わります。form要素で使用可能です。

form-element内の部品nameの現在の値を取得します。

// userのvalueを取得 
$('form[name=login]').field('user'); // => 'guest' 

form-element内の部品nameの値をvalueに変更します。同一nameの複数チェックボックスや複数選択selectの場合は配列でまとめて選択値を指定できます。

// passのvalueを設定 
$('form[name=login]').field('pass', 'admin');
 
// 複数選択可能部品の場合 
$('form[name=login]').field('multi-select', [ 'hoge', 'fuga', 'piyo' ]);

指定された連想配列内のname:valueを一括でform-element内の部品に反映します。

// 一括で設定 
$('form[name=login]').field({
  user: 'foo',
  pass: 'bar'
});

form-element内の全部品のnamevalueを連想配列で取得します。

// 一括で取得 
$('form[name=login]').field();
// => { 
//      user: 'foo', 
//      pass: 'bar', 
//      remember: 1 
//    } 

第3引数のonNotFoundは、部品に値を設定する際に参照されるオプションです。指定したnameの部品がフォーム内に存在しなかった時の動作を以下のいずれかの文字列で指定します。

  • throw ... 例外が発生します。
  • append ... 新規にそのname部品を作成してフォームに追加します(文字列の場合はhidden、配列の場合はcheckbox)。

onNotFoundを指定しなかった場合は例外は発生せず、新規にname部品の追加もしません(何もしない)。

// loginフォーム内にabcというnameの部品がない時の動作 
 
$('form[name=login]').field('abc', 'hello', 'throw');
// => 例外: Element named 'abc' could not be found in this form 
 
$('form[name=login]').field('abc', 'hello', 'append');
// => <input type="hidden" name="abc" value="hello"> を追加 
 
$('form[name=login]').field('abc', [ 'hello', 'world' ], 'append');
// => <input type="checkbox" name="abc" value="hello" checked> 
//    <input type="checkbox" name="abc" value="world" checked> を追加 
 
$('form[name=login]').field('abc', 'hello');
// => 何もしない 

指定したチェックボックス、ラジオボタンの要素を選択状態にします。対象の要素が元から選択状態の場合は何も変化しません。

対象要素が複数ある場合は対象すべてを選択状態にしますが、ラジオボタンに関しては同グループ内で複数を選択状態にすることはできないので、最初に該当した要素を選択状態にします。

$('input[name=check_foo]').tick();          // => check_fooを選択状態に 
$('input[type=checkbox]').tick();           // => 全チェックボックスを選択状態に 
$('input[name=radio_bar][value=2]').tick(); // => radio_barのvalueが2のラジオボタンを選択状態に 
$('input[type=radio]').tick();              // => 各ラジオボタングループの先頭を選択状態に 

指定したチェックボックス、ラジオボタンの要素を非選択状態にします。対象の要素が元から非選択状態の場合は何も変化しません。

対象要素が複数ある場合は対象すべてを非選択状態にします。

$('input[name=check_foo]').untick();          // => check_fooを非選択状態に 
$('input[type=checkbox]').untick();           // => 全チェックボックスを非選択状態に 
$('input[name=radio_bar][value=2]').untick(); // => radio_barのvalueが2のラジオボタンを非選択状態に 
$('input[type=radio]').untick();              // => 全ラジオボタンを非選択状態に 

a要素のhref、もしくはimg要素のsrcのURLを完全な形(絶対パス)にしたものを取得します。元から完全なURLになっている場合(外部リンクなど)やjavascript:void(0)といったURLでないリンクはその内容をそのまま返します。

<a id="top" href="../index.html">トップページ</a>

http://foo.bar.baz/hoge/というページ内に上記のようなリンクがある場合、$(...).attr('href')$(...).url()の戻り値はそれぞれ以下のようになります。

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

また、対象の要素が複数ある場合は各要素の絶対URLを配列に格納して返します。

console.log($('a').url());
// => [ 
//      'http://foo.bar.baz/index.html', 
//      'http://foo.bar.baz/xxx.html', 
//      'https://www.google.com/' 
//    ] 

第1引数のfilterは、対象要素のhrefsrcのURLを3種類に分類して、取得対象から除外するかどうかフィルタリングするオプションです。

  1. relative ... 相対URL(サイト内リンク)
  2. absolute ... 絶対URL(http(s)から始まるリンク(主にサイト外リンク))
  3. invalid ... URL以外(JavaScriptなど)

サイト内リンクを絶対URLで指定しているページもあるので、絶対URL = サイト外リンクとは限りません。

各フィルタをtrueにすると取得、falseにすると除外という意味になります。デフォルトはすべてtrueになっています。

<a href="./page2.html">
<a href="./#foo">
<a href="javascript:hogehoge();">
<a href="http://www.yahoo.com/">

このようなHTMLに対して$('a').url()を各種filterオプション指定で実行した時の戻り値は以下のようになります。

// 指定無し 
console.log($('a').url();
// => [ 
//      'http://foo.bar.baz/page2.html', 
//      'http://foo.bar.baz/#foo', 
//      'javascript:hogehoge();', 
//      'https://www.yahoo.com/' 
//    ] 
 
// 相対リンクのみ取得 
console.log($('a').url({
  relative: true,
  absolute: false,
  invalid: false
}));
// => [ 
//      'http://foo.bar.baz/page2.html', 
//      'http://foo.bar.baz/#foo' 
//    ] 
 
// URLとして有効なもののみ取得(除外するものだけfalseの指定でもOK) 
console.log($('a').url({ invalid: false }));
// => [ 
//      'http://foo.bar.baz/page2.html', 
//      'http://foo.bar.baz/#foo', 
//      'https://www.yahoo.com/' 
//    ] 
 

なお、対象となる要素が1つのみの時の戻り値は配列ではなく絶対URLの文字列になりますが、その際のfilterオプションの指定とその結果は以下のようになります。

<a id="top" href="index.html">Ajax</a>

上記リンクは相対リンクなので分類としてはrelativeに入ります。この時relativeを除外するオプションでurl()を呼び出すと戻り値はundefinedとなります。

console.log($('#top').url({ relative: false })); // => undefined 

第2引数のsrc-attrは、img要素から画像URLとして取得する属性名を指定するオプションです(文字列 or 配列)。

取得対象のWEBページでLazyLoad系のjQueryプラグインなどを使っている場合はsrc属性にダミーの画像URLが入っていたりしますが、そのようなimg要素でsrc属性以外からURLを取得する際に指定します。

<img src="blank.gif" data-original-src="http://this.is/real-image.png">

このようなHTMLで、srcblank.gifではなくdata-original-srchttp://this.is/real-image.pngをダウンロードしたい場合は以下のように指定します。

// filterオプションは省略可能 
$('img').url('data-original-src');

data-original-srcがその要素に存在しない場合はsrc属性のURLをダウンロードします。

なお、デフォルトではdata-original>data-lazy-src>data-src>srcの優先順になっています。デフォルトの優先順位を破棄してsrc属性の画像を最優先でダウンロードしたい場合は、

$('img').url({ invalid: false }, []);

のように空配列を指定します。

拡張cheerioオブジェクトからダウンロードマネージャーへの登録を行います。<img src="data:image/png;base64,/9j/4AAQSkZJRgABA ...">といった埋め込み画像もバイナリ化してダウンロードできます。

現在対応しているのはimg要素だけなので、img要素以外でdownload()を実行すると例外が発生します。

また、download()を実行する際にはダウンロードマネージャーの設定が必要になります。

var fs = require('fs');
var client = require('cheerio-httpcli');
 
// ①ダウンロードマネージャーの設定(全ダウンロードイベントがここで処理される) 
client.download
.on('ready', function (stream) {
  stream.pipe(fs.createWriteStream('/path/to/image.png'));
  console.log(stream.url.href + 'をダウンロードしました');
})
.on('error', function (err) {
  console.error(err.url + 'をダウンロードできませんでした: ' + err.message);
})
.on('end', function () {
  console.log('ダウンロードが完了しました');
});
 
// ④並列ダウンロード制限の設定 
client.download.parallel = 4;
 
// ②スクレイピング開始 
client.fetch('http://foo.bar.baz/', function (err$resbody) {
  // ③class="thumbnail"の画像を全部ダウンロード 
  $('img.thumbnail').download();
  console.log('OK!');
});

①のclient.downloadというのがcheerio-httpcliに内蔵されているダウンロードマネージャーになります。

スクレイピング中に$(...).download()メソッドで実行された画像のダウンロードが始まるとclient.downloadreadyイベントが発生します(エラーが発生した場合はerrorイベント)。

endイベントはダウンロード待ちのURLがなくなった時に発生します。

①では色々な場所から実行される画像ダウンロード時の共通処理を設定しています。この例では引に渡されたダウンロード元画像ファイルのStreamを/path/to/image.pngに保存しています。

client.downloadのイベント処理設定が完了したら②スクレイピングに入ります。

②でWEBページを取得し、その中の③で$(...).download()メソッドを実行しています。

この時、$('img.thumbnail')に該当する画像要素が10個あったとすると、その10個の画像要素がまとめてダウンロードマネージャーに登録されます(すでに登録済みのURLは除外されます)。

少し戻って④を見ると並列ダウンロード数制限が設定されています。今回の例では4なので、登録された10個の画像要素の内、即座に4つがダウンロード処理に入ります。

残りの6要素はダウンロード待ちキューに入り、最初の4つの内のどこかのダウンロードが完了して空きができると、次の画像URLがその空き部分にに登録されてダウンロードが実行される ... という流れです。

画像ダウンロードは本線である②③のスクレイピングとは非同期で行われます。

上記の例では③を実行して「OK!」が表示された段階で本線のスクレイピングは終わりますが、画像のダウンロードはまだ途中であり、また、ダウンロードマネージャーに登録した全画像のダウンロードが完了するまではこのスクリプト自体は終了しません。

「OK!」が表示されてもなかなかコンソールに制御が戻ってこないからといってCtrl+Cとかはせずに、ダウンロード完了までお待ちください。

第1引数のsrc-attrオプションは、url()と同様にimg要素から画像URLとして取得する属性名を指定可能です(文字列 or 配列)。

<img src="blank.gif" data-original-src="http://this.is/real-image.png">

上記のようなHTMLで、srcblank.gifではなくdata-original-srchttp://this.is/real-image.pngをダウンロードしたい場合は以下のように指定します。

$('img').download('data-original-src');

その他仕様はurl()src-attr項を参照

$(...).download()で登録されたURLのダウンロード時共通設定になります。

ダウンロードの同時並列実行数を指定します。15の間で指定します(デフォルトは3)。すでにダウンロードが始まっている段階で値を変更した場合は、現在実行中のダウンロードがすべて完了してから反映されます。

ダウンロードマネージャーの現在の処理状況を確認できます。読み取り専用なので数値を変更してもダウンロードの状況は変化しません。

stateには以下の2項目が登録されています。

  • queue ... ダウンロード待ち件数
  • complete ... ダウンロード完了件数
  • error ... エラー件数
console.log(client.download.state); // => { queue: 10, complete: 3, error: 0 } 

また、ダウンロードイベント内でthis.stateでも確認できます。

client.download
.on('ready', function (stream) {
  console.log(this.state); // => { queue: 2, complete: 5, error: 1 } 
  ...

ダウンロードマネージャーは重複したURLを除外するためにURLキャッシュを内部で持っています。何らかの理由でそのキャッシュをクリアする場合に使用します。

download.onで設定可能はイベントは以下の通りです。

ダウンロード開始時に発生するイベント時の処理です。URL毎に発生します。引数のstreamにはダウンロード元画像ファイルのストリームが入ります。streamに実装されているプロパティ/メソッドは以下のとおりです。

  • url ... 画像ファイルのURLオブジェクトです。URLの文字列はstream.url.hrefで取得できます。Base64埋め込み画像の場合はURLオブジェクトではなくbase64という文字列が入ります。
  • type ... Content-Typeが入ります。サーバーから返されたレスポンスヘッダにContent-Typeがない場合はundefinedになります。
  • length ... Content-Lengthが入ります。サーバーから返されたレスポンスヘッダにContent-Lengthがない場合は-1になります。
  • toBuffer(callback) ... ストリームをBufferに変換してコールバック関数(err, buffer)に返します。画像ファイルの内容をすべてメモリ上に読み込むので巨大な画像の場合はそれだけメモリを消費します。
  • end() ... ストリームの読み込みを終了します。readyイベント内で__ストリームを読み込まずに処理を抜ける場合などは必ず呼び出してください__(そのままにしておくとキューが詰まって次のダウンロードができなくなることがあります。また、ストリームが読み込まれずに放置されたままtimeout時間が経過するとerrorイベントが発生して強制的にエラー扱いとなります)。

ダウンロード中にエラーが発生した時に発生するイベント時の処理です。引数のerrオブジェクトにはurlプロパティ(ダウンロード元の画像URL)が入っています。

ダウンロード待ちキューが空になった時に発生するイベント時の処理です。引数はありません。

client.download
.on('ready', function (stream) {
  // gif画像以外はいらない 
  if (! /\.gif$/i.test(stream.url.pathname)) {
    return stream.end();
  }
 
  // 各種情報表示 
  console.log(stream.url.href); // => 'http://hogehoge.com/foobar.png' 
  console.log(stream.type);     // => 'image/png' 
  console.log(stream.length);   // => 10240 
 
  // Buffer化してファイルに保存 
  stream.toBuffer(function (errbuffer) {
    fs.writeFileSync('foobar.png', buffer, 'binary');
  });
})
.on('error', function (err) {
  console.error(err.url + '' + err.message);
})
.on('end', function (err) {
  console.log('queue is empty');
});
 

対象要素のHTML部分をすべてHTMLエンティティ化した文字列を返します。基本的には使い道はないと思います。

// <h1>こんにちは</h1> 
console.log($('h1').html())        // => 'こんにちは' 
console.log($('h1').entityHtml()); // => '&#x3053;&#x3093;&#x306B;&#x3061;&#x306F;' 

fetch()cheerio.click()cheerio.submit()などで取得できるresponseオブジェクトはrequestモジュールで取得したものですが、独自拡張としてcookiesプロパティを付け足しています。

client.fetch('http://hogehoge/')
.then(function (result) {
  // プロミス形式でログインフォーム送信 
  return result.$('form[name=login]').submit({ user: 'hoge', pass: 'fuga' })
})
.then(function (result) {
  // ログイン後のクッキー内容確認 
  console.log(result.response.cookies);
});

このcookiesプロパティには現在取得したページのサーバーから送られてきたクッキーのキーと値が連想配列で入っています。セッションIDやログイン状態の確認などに使えるかもしれません。

なお、このcookiesの値を変更してもリクエスト処理には反映されません。クッキー確認専用のプロパティです。

Basic認証が必要なページには以下の二通りの方法でアクセスできます。

var client = require('cheerio-httpcli');
var user = 'hoge';
var password = 'foobarbaz';
 
client.headers['Authorization'] = 'Basic ' + new Buffer(user + ':' + password).toString('base64');
client.fetch('http://securet.example.com', function (err$resbody) {
  .
  .
  .
  // 不要になったら消去 
  delete(client.headers['Authorization']);
});
var client = require('cheerio-httpcli');
var user = 'hoge';
var password = 'foobarbaz';
 
client.fetch('http://' + user + ':' + password + '@securet.example.com', function (err$resbody) {

詳細はこちら

文字コードの判別はjschardetで高精度で判別できた場合はその情報を使用しますが、そうでない場合は<head>タグのcharset情報を参照します。後者での判別時においてcharsetで指定された文字コードとWEBページの実際の文字コードが異なる場合は変換エラーや文字化けが発生します。

MIT licenseで配布します。

© 2013-2015 ktty1220