エロサイトの作り方

2013年11月から勉強しながらエロサイトを作っています。

Node.jsでGETリクエストを投げる時に最低限必要な処理

つらつら書いてみたら長い長い。

誰もが引っかかる罠、

  • HTTPリクエストに対するレスポンスがなければerrorオブジェクトに中身が入る
  • errorとなった場合にはresponseオブジェクトはundefined
  • UTF-8以外のコンテンツは明示的に変換しないと文字化けする

あたりを考慮しないといけない。

特に文字コード辺りは裏でよしなにして欲しいところなのですが……

必要なライブラリ

Request

request/request · GitHub

$ npm install --save request

お手軽にGETリクエストを投げるのに必要

node-iconv

bnoordhuis/node-iconv · GitHub

$ npm install --save iconv

文字コードを内部用にUTF-8に変換するために必要

ICU Character Set Detection for Node.js

mooz/node-icu-charset-detector · GitHub

$ brew install icu4c && brew link icu4c
$ npm install --save node-icu-charset-detector

レスポンスヘッダーや<meta>タグで文字コードを指定してない場合に、本文から自動判定するために必要(最終手段なので無くてもいいかも)

逆に、最初からこれで全部判定するという手もありますが、意外と誤判定が多いんですよね……

ソース

var request = require('request');
var Iconv = require('iconv').Iconv;
var CharsetMatch = require("node-icu-charset-detector").CharsetMatch;
var inspect = require('util').inspect;

var requestUrl = 'http://hentai-kun.hatenablog.jp/';

var options = {
  followRedirect: true, // 3xx系でリダイレクトを行う(default: true)
  maxRedirects: 5,      // 最大リダイレクト回数(default: 10)
  timeout: 10000,       // タイムアウトさせるまでのミリ秒(default: タイムアウトしない)
  encoding: 'binary'    // 文字コードの指定(default: UTF-8)、EUC-JP/SHIFT_JISなサイトを開く可能性がある場合は'binary'にする
};

request.get(requestUrl, options, function (error, response, body) {
  if (error) {

    //
    // 基本的にサーバー側からレスポンスが返ってこない場合
    // なので、responseはundefined
    console.log(inspect(error));

  } else if (response.statusCode != 200) {

    //
    // 基本的にサーバー側からレスポンスが返ってきた場合
    // bodyがあるかどうかはサーバー次第
    console.log(inspect(error), inspect(response));

  } else {

    //
    // 正常終了
    // bodyがある

    // 文字コードを特定する
    var charset = findCharset(response, body);
    console.log('charset:', charset);

    // 文字コードをUTF-8に変換する
    var html = convertUTF8(charset, body);
    console.log(inspect(response), html);
  }
});


// 文字コードを特定する
function findCharset(response, bin) {
  var charset;

  // レスポンスヘッダーにあればそれを使う
  var matcher = (response.headers['content-type']||'').match(/charset=([^;\s]+)/);
  if (matcher && matcher[1]) {
    charset = matcher[1];
  } else {
    // 無ければHTMLのmetaタグから探す
    matcher = bin.match(/<meta\b[^>]+charset=["']?([\w\-_]+)/i)
    if (matcher && matcher[1]) {
      charset = matcher[1];
    } else {
      // 無ければコンテンツから推測
      charset = new CharsetMatch(new Buffer(bin, 'binary')).getName();
    }
  }
  return charset;
}


// 文字コードをUTF-8に変換する
function convertUTF8 (charset, bin) {
  var iconv, text;

  var buffer = new Buffer(bin, 'binary');
  try {
    iconv = new Iconv(charset, 'UTF-8//TRANSLIT//IGNORE');
    text  = iconv.convert(buffer).toString();
  } catch (error) {
    console.log(inspect(error));

    // 失敗なら、仕方ないのでUTF-8とみなす
    text  = buffer.toString('utf8');
  }
  return text;
}

エラーコード

error.codeの値としてあるもの。

  • 'ENOTFOUND': DNSで引けなかった
  • 'ENETUNREACH': 接続できない(ポートが開いてないなど)
  • 'ETIMEDOUT': 時間内に応答がなかった(タイムアウトした)

他にもあるかもしれないけど、出たことがない。

補足:followAllRedirectsとは何か

followRedirectと似たオプションでfollowAllRedirectsというのがあって、オプションの説明に

followAllRedirects - follow non-GET HTTP 3xx responses as redirects (default: false)

としか書いてなくて何をするものなのかソースを追ってみました。

request.js

if (self.followAllRedirects) {
  redirectTo = location
} else if (self.followRedirect) {
  switch (self.method) {
    case 'PATCH':
    case 'PUT':
    case 'POST':
    case 'DELETE':
      // Do not follow redirects
      break
    default:
      redirectTo = location
      break
  }
}

どうやら、リクエストメソッドに関係なくリダイレクトさせる、ということみたいです。