まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:nodejs

JavaScript で JSON 配列をソートする、意外と簡単な方法があります。具体例として、例えば以下のような JSON 配列: myArray があったときに、
var myArray = [
 { id: 1, name: "鈴木", age: 30 },
 { id: 2, name: "田中", age: 29 },
 { id: 3, name: "佐藤", age: 35 },
   :
];

この JSON 配列を age 要素の小さい順にソートしたいという場合にどうやるか、というケースを紹介します。


まず、JavaScript の配列には sort 関数が標準で用意されています。直感的にこんな感じで使えます:
var array1 = [ 30, 29, 35 ];
array1.sort();                  //. => array1 = [ 29, 30, 35 ];

注意点として sort() 実行後の返り値がソート結果になるのではなく、実行すると array1 そのものが書き換えられる、という点に注意が必要です。

sort() 関数には比較関数をパラメータで指定して、比較方法を指定することもできます。例えば以下の例では比較関数 compare を「小さい順に並べる」ような内容にしているので、sort() 実行後に小さい順にソートされます(要するに普通の sort() と同じ結果になります):
//. 比較関数
function compare( a, b ){ var r = 0; if( a < b ){ r = -1; } else if( a > b ){ r = 1; } return r; }
var array2 = [ 30, 29, 35 ]; array2.sort( compare ); //. => array2 = [ 29, 30, 35 ];

比較関数はパラメータ a と b の大小関係をその返り値によって定義します:
・返り値が 0 より小さい場合、a < b(aはbよりも前に来る)
・返り値が 0 より大きい場合、a > b(aはbよりも後ろに来る)
・返り値が 0 の場合、a = b(aとbの位置関係は変わらない)

したがって、compare 関数の返り値の符号を逆にすると「大きい順に並べる」という比較関数を指定したことになり、sort() 実行後に大きい順にソートされます:
//. 比較関数
function compare( a, b ){ var r = 0; if( a < b ){ r = -1; } else if( a > b ){ r = 1; } return ( -1 * r ); } var array3 = [ 30, 29, 35 ]; array3.sort( compare ); //. => array3 = [ 35, 30, 29 ];

上記 JSON 配列のソートもこの比較関数を使った sort() の応用で可能になります。具体的には以下のように age 要素で大小比較するような比較関数を用意します:
//. 比較関数
function compare( a, b ){ var r = 0; if( a.age < b.age ){ r = -1; } else if( a.age > b.age ){ r = 1; } return r; }
var myArray = [ { id: 1, name: "鈴木", age: 30 }, { id: 2, name: "田中", age: 29 }, { id: 3, name: "佐藤", age: 35 }, : ]; myArray.sort( compare );

これだけ。応用も簡単にできそうで超便利。


「国際化」に対応したウェブアプリケーションを Node.js で作る方法を調べたので、メモ替わりに残しておきます。

ここでの「国際化(internationalization, i18n)」はウェブブラウザで設定した言語によって自動的に英語表記にしたり、日本語にしたり、・・・という切り替えを行えるようなものです。自動翻訳とかそういうものではありません。またブラウザで設定した言語は HTTP リクエスト時に "Accept-Language" ヘッダで送信されることになるので、後述の動作確認は curl コマンドでこのヘッダを指定して行っています。

このような国際化対応アプリケーションを Node.js で、正確には Node.js + Express + EJS の環境で作ってみました。

Node.js で国際化対応アプリケーションを作る場合、i18n というパッケージを使うのが手っ取り早いです:
https://www.npmjs.com/package/i18n

ソースコード(app.js)はこんな感じにしました。余計な部分を削ぎ落として、最小限必要な部分だけを残しています(赤字部分が i18n 関連の箇所です):
//. app.js

var express = require( 'express' ),
    fs = require( 'fs' ),
    ejs = require( 'ejs' ),
    i18n = require( 'i18n' ),
    request = require( 'request' ),
    session = require( 'express-session' ),
    app = express();

var port = 3000;

app.set( 'views', __dirname + '/public' );
app.set( 'view engine', 'ejs' );

i18n.configure({
  locales: ['en', 'ja'],
  directory: __dirname + '/locales'
});
app.use( i18n.init );

app.get( '/', function( req, res ){
  res.render( 'index' );
});

app.listen( port );
console.log( "server starting on " + port + " ..." );

今回は英語(en)と日本語(ja)に対応したアプリケーションにしました。

また '/' にアクセスした時に ejs の index テンプレートを使った画面が表示されるような内容にしています。ちなみに index テンプレート(public/index.ejs)の内容は以下のようになっています:
<html>
<head>
<title><%= __('subject') %></title>
</head>
<body>
<h1><%= __('subject') %></h1>
<hr/>
<%= __('body') %>
</body>
</html>


テンプレート内で subject と body という2つの変数を使った表記を行っています。実際にはこれらの部分に言語設定に合わせた内容が表示されることになります。

そして言語ファイルを以下のように用意します:

(英語用: locales/en.json)
{
  "subject": "subject",
  "body": "body"
}


(日本語用: locales/ja.json)
{
  "subject": "サブジェクト",
  "body": "本文"
}

英語設定で利用した場合、上記の subject 変数部分は "subject", body 変数部分は "body" と表示されます。また日本語設定の場合、それぞれ "サブジェクト" と "本文" となります。


これで準備できました。 npm install して実行(node app)します:
$ npm install
$ node app

確認は別の端末から curl で行いました。まずは Accept-Language を en(英語)にしてアクセス:
$ curl http://localhost:3000/ -H 'Accept-Language: en'

<html>
<head>
<title>subject</title>
</head>
<body>
<h1>subject</h1>
<hr/>
body
</body>
</html>
>

次は日本語設定でアクセスした場合:
$ curl http://localhost:3000/ -H 'Accept-Language: ja'

<html>
<head>
<title>サブジェクト</title>
</head>
<body>
<h1>サブジェクト</h1>
<hr/>
本文
</body>
</html>


期待通りに動いています!


アプリケーションの国際化そのものはこれだけで出来ました。そして問題になるのは「どうやって色んな言語用の JSON リソースファイルを用意するか?」です。1つ1つ翻訳サービスなどを使いながら作る、という方法もありますが、そんな言語リソースファイルの翻訳作業は IBM Cloud の Globalization Pipeline サービスを使うと英語のリソースファイルから各言語に翻訳したリソースファイルをまとめて作ることができてとても便利です。このサービスについては以前のブログで使い方も含めて紹介しているので参照ください:
Globalization Pipeline サービスがリリースされました!


と、最後は宣伝でしたw

AI やコグニティブエンジンで画像認識機能を使ったり(特にそのためのデータ学習を行おうと)すると、大量の画像データが必要になります。その画像データが手元にある場合はそれらを使えばいいのですが、充分な画像がない場合にどうするか? という問題があります。

ネットから画像検索して見つかるようなものであればそれも方法の1つですが、学習に使えるようなシーンが映った動画があれば、その動画から静止画像を切り取って使う、という方法もあります。この処理をNode.js のプログラムで自動化する方法を紹介します。

まず、このような動画から静止画を切り出すには ffmpeg を使うと便利です。ffmpeg はクロスプラットフォームで動くフリーな動画処理ソフトで、コーデックやライブラリ等も含んでいます。Node.js からであれば fluent-ffmpeg ライブラリをインストールすることで、この ffmpeg のネイティブ機能が使えるようになります(なので ffmpeg をインストールした上で、fluent-ffmpeg を使う必要があります。ちなみに Ubuntu 16.04 では ffmpeg がデフォルトで導入されています)
2017102000


というわけで、まずはライブラリを準備します。以下のサンプルソースで使う path ライブラリと合わせてこんな感じ:
$ npm install path fluent-ffmpeg

そして以下のようなサンプルコードを用意します:
var ffmpeg = require( 'fluent-ffmpeg' );
var path = require( 'path' );

var mpegfile = './abc.mp4';  //. 動画ファイル

var command = ffmpeg( mpegfile );

command.on( 'end', () => {
  console.log( 'ok' );
}).screenshots({
  count: 4,                               //. 20%, 40%, 60%, 80% のタイミングで静止画を取得する
  folder: path.join( __dirname, 'tmp' );  //. ./tmp/ フォルダに静止画を出力
  filename: 'abc-%i:%s.png',              //. ファイル名は abc-(インデックス番号):(秒).png
  size: '320x?'                           //. 画像サイズは横幅 320 px で、縦を可変(同一縮尺)にする
});


このコードでは実行結果の静止画像を ./tmp フォルダ以下に作成するよう指定しています。なので、まずこのフォルダを作成しておきます:
$ mkdir tmp

そして上記のコードを Node.js で実行。成功すると tmp/ 以下に abc-****.png という画像ファイルが4枚出力されます。


上記例では静止画像を取得するタイミングを同間隔で4枚という指定をしましたが、timemarks を指定することで何秒後、という指定も可能です:
      :
  timemarks: [ 0.5, 1, 2 ],             //. 0.5秒後、1秒後、2秒後に切り取り
      :





Node.js のアプリケーションで、以下のような処理を実装してみました:
 - 認証(ログイン)用の API には誰でもアクセスできる
 - 認証 API では ID とパスワードを与えて認証し、正しいユーザーにはトークンを発行する
 - 認証以外の主な API はこの発行されたトークンを使ってアクセスした時だけ実行を許可する
 - API 呼び出し時にトークンがなかったり、正しくなかった場合は実行せずにエラー


この仕組を実現するために JSON Web Tokens (以下 "JWT")を使いました:
https://jwt.io/

2017081400


JWT はオープンかつ Node.js ではスタンダードなトークンベースの認証ライブラリです。以下で紹介するサンプルでは Web フレームワークである Express や、POST データを扱う body-parser も合わせて使うので、まとめてインストールしておきます:
$ npm install express body-parser jsonwebtoken

これらのライブラリを使って以下のようなアプリケーションを作成します:
//. app.js

var express = require( 'express' );
var app = express();
var bodyParser = require( 'body-parser' );
var jwt = require( 'jsonwebtoken' );

//. アプリケーションサーバーの稼働ポート番号
var port = process.env.PORT || 8080

//. 任意のシークレット文字列を登録
app.set( 'superSecret', 'welovenodejs' );  //. 任意の文字列

app.use( bodyParser.urlencoded( { extended: false } ) );
app.use( bodyParser.json() );

//. ユーザー情報(本来は DB などに格納された情報を使う)
var users = [
  { name: 'user0', password: 'pass0', admin: true },
  { name: 'user1', password: 'pass1', admin: false },
  { name: 'user2', password: 'pass2', admin: false },
  { name: 'user3', password: 'pass3', admin: false }
];

//. ドキュメントルートへの GET は許可
app.get( '/', function( req, res ){
  res.send( 'Hello. The API is at http://localhost:' + port + '/api' );
});

//. API ROUTES
var apiRoutes = express.Router();

//. トークンなしでアクセスを許可する API を先に定義する

//. POST(http://localhost:8080/api/authenticate)
apiRoutes.post( '/authenticate', function( req, res ){
  for( var i = 0; i < users.length; i ++ ){
    if( users[i].name == req.body.name && users[i].password == req.body.password ){
      //. 認証したユーザーの情報を使ってトークンを生成
      var token= jwt.sign( users[i], app.get( 'superSecret' ), {
        expiresIn: '24h'
      });
      res.json( { success: true, message: 'Authentication successfully finished.', token: token } );
      return;
    }
  }

  res.json( { success: false, message: 'Authentication failed.' } );
  return;
});

//. ここより上で定義した API には認証フィルタはかけていない(そのまま使える) //. 認証フィルタ apiRoutes.use( function( req, res, next ){
//. ポスト本体、URLパラメータ、HTTPヘッダいずれかにトークンがセットされているか調べる var token = req.body.token || req.query.token || req.headers['x-access-token']; if( !token ){
//. トークンが設定されていなかった場合は無条件に 403 エラー return res.status(403).send( { success: false, message: 'No token provided.' } ); } //. 設定されていたトークンの値の正当性を確認 jwt.verify( token, app.get( 'superSecret' ), function( err, decoded ){ if( err ){ //. 正当な値ではなかった場合はエラーメッセージを返す return res.json( { success: false, message: 'Invalid token.' } ); } //. 正当な値が設定されていた場合は処理を続ける req.decoded = decoded; next(); }); }); //. 以下はトークンがないと使えない API //. GET(http://localhost:8080/api/) apiRoutes.get( '/', function( req, res ){ res.json( { message: 'Welcome to API routing.' } ); }); //. GET(http://localhost:8080/api/users) apiRoutes.get( '/users', function( req, res ){ res.json( users ); }); //. /api 以下に API をルーティング app.use( '/api', apiRoutes ); app.listen( port ); console.log( 'server started http://localhost:' + port + '/' );

肝になるのはルーティングに認証フィルタを定義している箇所です。ここよりも前(上)で定義した内容には認証フィルタは有効にならないので、認証なしで使える API となります(つまり GET / と POST /authenticate は認証していなくても使えます)。

この POST /api/authenticate API でポストデータ user, password を受取り、その値が変数 users の中で定義されているいずれかの組み合わせと一致していれば 24H 有効なトークンが発行されます(これを以下の API 実行時にパラメータ指定します)。

一方、ここよりも後(下)で定義する内容には認証フィルタが有効になり、GET /api と GET /api/users は上記方法で取得したトークンがパラメータに設定されていないと正しく処理されない API となります。


実際にこのアプリケーションを実行し($ node app)、curl コマンドで挙動を確認してみましょう。まずは問題なく実行できるはずの GET / を実行します:
$ curl -XGET 'http://localhost:8080/'
Hello. The API is at http://localhost:8080/api

問題なく実行できました。次は認証フィルタをかけた GET /api を実行してみます:
$ curl -XGET 'http://localhost:8080/api'
{"success":false,"message":"No token provided."}

API を実行した結果、「トークンがない」時のエラーメッセージが表示されました。ここも期待通りに動いています(試しませんが、ユーザー一覧を取得する GET /api/users も同様のエラーになります)。

では POST /api/authenticate で認証してトークンを取得してみますが最初はわざとパスワードを間違えてみます:
$ curl -XPOST -H 'Content-Type:application/json' 'http://localhost:8080/api/authenticate' -d '{"name":"user1","password":"pass0"}'
{"success":false,"message":"Authentication failed."}

先程同様にエラーになりましたが、エラーメッセージが「認証失敗」に変わりました。では改めて正しいパスワードを指定して実行してみます:
$ curl -XPOST -H 'Content-Type:application/json' 'http://localhost:8080/api/authenticate' -d '{"name":"user1","password":"pass1"}'
{"success":true,"message":"Authentication successfully finished.","token":"XXXXXX...XXXXXX"}

今度は認証が成功しました。レスポンスにも "token" が含まれています。最後にここで返ってきた token の値を指定して、先程アクセスできなかった GET /api を実行してみます:
$ curl -XGET 'http://localhost:8080/api?token=XXXXXX...XXXXXX'
{"message":"Welcome to API routing."}

今度は API が実行できました。同様にして GET /api/users も実行できるはずです:
$ curl -XGET 'http://localhost:8080/api/users?token=XXXXXX...XXXXXX'
[{"name":"user0","password":"pass0","admin":true},{"name":"user1","password":"pass1","admin":false},{"name":"user2","password":"pass2","admin":false},{"name":"user3","password":"pass3","admin":false}]


こんな感じで API の実行可否をトークンで制御できるようになりました。

今回の例ではソースコード内に静的に用意されたユーザー一覧を使って認証を行いましたが、実際の運用ではデータベース内に定義されたテーブル情報などを使うことになると思います。ただ JWT の基本的な考え方はこの1つのソースファイルだけで実現できているので、応用しやすいと思っています。

 

Node.js でのスクレイピングに挑戦したことがなかったのでやってみました。

一般的にスクレイピングでは http でコンテンツ文字列を取得して、正規表現などを用いて取得したコンテンツ文字列から指定のパターンに合致する箇所のテキストを取得します。今回はその部分を手でガリガリ、、ではなく、この cheerio-httpcli モジュールを使ってスクレイピングに挑戦しました:
https://www.npmjs.com/package/cheerio-httpcli

2017072600


まずはこの cheerio-httpcli モジュールを npm でインストールします:
$ npm install cheerio-httpcli

Node.js のコード内で cheerio-httpcli モジュールを利用する場合は以下のような記述を行います:
var client = require( 'cheerio-httpcli' );
 :

var url = 'https://www.google.com/finance/converter?a=1&from=EUR&to=JPY';
client.fetch( url, {}, function( err, $, res ){
  $('.bld').each( function(){
    text = $(this).text;
       :
  });
});


実はこれだけでテキストのフェッチとスクレイピングの両方が実現できてしまっています。URL を指定してその HTML テキストをフェッチします。その結果は $ に入るので、フェッチの結果は jQuery の DOM のように扱うことができます。上例では取得した HTML テキスト内において bld クラスを適用した要素内のテキスト(<xxx class="bld">XXXXX</xxx> となっている箇所の XXXXX 部分)を取得して、変数 text に代入しています。

上記のサンプルで紹介している URL: https://www.google.com/finance/converter?a=1&from=EUR&to=JPY はグーグルの通貨変換ページです(このパラメータの場合、ユーロ(EUR)から日本円(JPY)への変換レートを取得します)。この URL にアクセスすると、以下のような HTML が返って来ます:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  :
  :
</div>
 
<div id=currency_converter_result>1 EUR = <span class=bld>130.0360 JPY</span>
<input type=submit value="Convert">
</div>
<input type=hidden name=meta value=ei=E1N4WYisKN6O0QS8v4OoBA>
</form>
</body>
</div>
</html>

このようにレート変換した結果は上記赤字部分になるので、ここを取得できればよいことになります。したがって上述のように bld クラスを指定した箇所のテキスト値をスクレイピングすれば(上記の場合であれば "130.0360 JPY" という文字列が)変数 text に代入できる、ということになります。

jQuery スタイルで使えるのがとっても便利!


このページのトップヘ