読者です 読者をやめる 読者になる 読者になる

FledglingRobin’s diary

Github: https://github.com/FledglingRobin

気軽にモーフィングを試したいときのためのメモ

気軽にいろいろ遊んでみたので、とくに面白かったものを記録しておく。

2人の顔写真の中間画像をうみだす

  • SqirlzMorph

中間画像を作っていくソフト。 www.xiberpix.net

モーフィングそのものの実現は難しくなさそう。 www.learnopencv.com

  • IMAGE MORPH JS

JavaScript向けのモーフィングライブラリも存在する。 matching pointを手動指定した場合のデモがあっていろいろ試せる。

Poisson image Editing

切り抜き位置の調整をした上で、部位位置指定でなめらなかなブレンディングをすると、目元はAさんで口元はBさんみたいな画像ができるはず… ということでなめらかなブレンディングを手軽に利用できそうな実装を探す。

Poisson Blending | OpenCV.jp

JavaScriptでPoisson Image Editingによる滑らかな画像合成 « Rest Term

ためしてみた。位置合わせとマスク画像を手作業で作ってブレンディング。顔の形のちがいからバランスが微妙になってしまった。特徴点をとって、きちんと元の位置にパーツを配置しないとだめだなあ。

f:id:FledglingRobin:20170319231029p:plainf:id:FledglingRobin:20170319231034p:plain

f:id:FledglingRobin:20170319231040p:plainf:id:FledglingRobin:20170319231044p:plain

続きはまた今度。

Node.jsとasync.jsとcallstack

Node.js の call stack の性質を知る必要がでてきた。ためしてみた結果を記録。

Node.js の stack trace の出し方を知る

Errors | Node.js v7.6.0 Documentation を参考に実行してみる。

var stack = new Error().stack
console.log( stack )

実行結果:

$ node app.js 
Error
    at Object.<anonymous> (/[directory_path]/app.js:1:75)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

変数 stack には起動から new Error までのcall stack が格納されたようだ。

同じく Console | Node.js v7.6.0 Documentation を参考に実行してみる。

console.trace();

実行結果:

$ node app.js 
Trace
    at Object.<anonymous> (/[directory_path]/app.js:1:71)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

console.traceは、 

message and stack trace

を印字するそうだ。

基本的な callstack の挙動を見てみる

関数実行後に別途関数を呼び出すのか、関数内で関数を呼び出すのかで call stack が消化されるのか、積まれるのかが変わるはず。

function method() {
    return true;
}

method();
console.trace();

実行結果:

$ node app.js 
Trace
    at Object.<anonymous> (/[directory_path]/app.js:6:9)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3
function method(callback) {
    callback(true);
}

method(function(bool){
    console.trace();
});

実行結果:

$ node app.js 
Trace
    at /[directory_path]/app.js:6:10
    at method (/[directory_path]/app.js:2:2)
    at Object.<anonymous> (/[directory_path]/app.js:5:1)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

両者は method が callstack に残されているかに違いが出る。

call stack overflow を起こす

一度、call stack overflow を経験しておくことにする。真っ先に思いつくのは再帰呼出し。

function method(n) {
    console.log(n);
    method(n + 1);
}

method(0);

実行結果の一部:

17938
17939
17940
util.js:158
function stylizeNoColor(str, styleType) {
                       ^

RangeError: Maximum call stack size exceeded
    at Object.stylizeNoColor [as stylize] (util.js:158:24)
    at formatPrimitive (util.js:439:16)
    at formatValue (util.js:227:19)
    at inspect (util.js:109:10)
    at exports.format (util.js:17:20)
    at Console.log (console.js:39:34)
    at method (/[directory_path]/app.js:2:10)
    at method (/[directory_path]/app.js:3:2)
    at method (/[directory_path]/app.js:3:2)
    at method (/[directory_path]/app.js:3:2)

なお、Errors | Node.js v7.6.0 Documentation を見ると RangeError は、

a provided argument was not within the set or range of acceptable values for a function

とのことで、今回は後者にあたるはず。

再帰呼び出しを setTimeout でラップする

ためしに event queue に仕事をさせてみる。setTimeout は引数の function をevent queue に登録し、関数自体は即座に結果を返却するはずだ。

function method(n) {
    setTimeout(function() {
        console.log(n);
        method(n + 1);
    }, 0);
}

method(0);

こうすると、先程停止した 17940 を過ぎてもMaximum call stack size exceeded が呼ばれない。ここで、実行中の call stack を比較してみることにする。

function method(n) {
    if (n === 100) {
        console.trace();
    } else {
        console.log(n);
        method(n + 1);
    }
}

method(0);
97
98
99
Trace
    at method (/[directory_path]/app.js:5:17)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
    at method (/[directory_path]/app.js:8:9)
function method(n) {
    if (n === 17940) {
        console.trace();
    } else {
        setTimeout(function() {
            console.log(n);
            method(n + 1);
        }, 0);
    }
}

method(0);
17938
17939
Trace
    at method (/[directory_path]/app.js:5:17)
    at null._onTimeout (/[directory_path]/app.js:9:13)
    at Timer.listOnTimeout (timers.js:92:15)

後者では method が複数積まれていないことがわかる。

async.js 使用時の stack trace をみてみる

async.js では、async.js に定義された関数の処理を通しながら、function list の関数を実行していく。

async.waterfall の場合

async.waterfallは、順次処理を記述する際に便利な関数だ。async.waterfall を例に call stack を見てみる。

var async = require('async');
Error.stackTraceLimit = Infinity;

function method(cb){
    cb(null);
}

async.waterfall([method, method, method], function(err, result){
    console.trace();
});

実行結果:

Trace
    at /[directory_path]/app.js:10:10
    at /[directory_path]/node_modules/async/dist/async.js:359:16
    at nextTask (/[directory_path]/node_modules/async/dist/async.js:5057:29)
    at /[directory_path]/node_modules/async/dist/async.js:5064:13
    at apply (/[directory_path]/node_modules/async/dist/async.js:21:25)
    at /[directory_path]/node_modules/async/dist/async.js:56:12
    at /[directory_path]/node_modules/async/dist/async.js:843:16
    at method (/[directory_path]/app.js:6:2)
    at nextTask (/[directory_path]/node_modules/async/dist/async.js:5070:14)
    at /[directory_path]/node_modules/async/dist/async.js:5064:13
    at apply (/[directory_path]/node_modules/async/dist/async.js:21:25)
    at /[directory_path]/node_modules/async/dist/async.js:56:12
    at /[directory_path]/node_modules/async/dist/async.js:843:16
    at method (/[directory_path]/app.js:6:2)
    at nextTask (/[directory_path]/node_modules/async/dist/async.js:5070:14)
    at /[directory_path]/node_modules/async/dist/async.js:5064:13
    at apply (/[directory_path]/node_modules/async/dist/async.js:21:25)
    at /[directory_path]/node_modules/async/dist/async.js:56:12
    at /[directory_path]/node_modules/async/dist/async.js:843:16
    at method (/[directory_path]/app.js:6:2)
    at nextTask (/[directory_path]/node_modules/async/dist/async.js:5070:14)
    at Object.waterfall (/[directory_path]/node_modules/async/dist/async.js:5073:5)
    at Object.<anonymous> (/[directory_path]/app.js:9:7)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

制御のためにいちいちasync.jsの関数を呼び出すので、call stack はやや長くなる。

async.parallel の場合

async.js には、非同期に、並列処理かのように動作する関数が存在する。async.parallelもそのひとつだ。こちらも setTimeoutを使用したときと同様の現象がみられる。

var async = require('async');
Error.stackTraceLimit = Infinity;

function method(cb){
    cb(null);
}

async.parallel([method, method, method], function(err, result){
    console.trace();
});
Trace
    at /[directory_path]/app.js:10:10
    at /[directory_path]/node_modules/async/dist/async.js:3694:9
    at /[directory_path]/node_modules/async/dist/async.js:359:16
    at iteratorCallback (/[directory_path]/node_modules/async/dist/async.js:935:13)
    at /[directory_path]/node_modules/async/dist/async.js:843:16
    at /[directory_path]/node_modules/async/dist/async.js:3691:13
    at apply (/[directory_path]/node_modules/async/dist/async.js:21:25)
    at /[directory_path]/node_modules/async/dist/async.js:56:12
    at method (/[directory_path]/app.js:6:2)
    at /[directory_path]/node_modules/async/dist/async.js:3686:9
    at eachOfArrayLike (/[directory_path]/node_modules/async/dist/async.js:940:9)
    at eachOf (/[directory_path]/node_modules/async/dist/async.js:990:5)
    at _parallel (/[directory_path]/node_modules/async/dist/async.js:3685:5)
    at Object.parallelLimit [as parallel] (/[directory_path]/node_modules/async/dist/async.js:3765:3)
    at Object.<anonymous> (/[directory_path]/app.js:9:7)
    at Module._compile (module.js:409:26)
    at Object.Module._extensions..js (module.js:416:10)
    at Module.load (module.js:343:32)
    at Function.Module._load (module.js:300:12)
    at Function.Module.runMain (module.js:441:10)
    at startup (node.js:139:18)
    at node.js:968:3

stack trace をみると method が一度分しか記載されていないことがわかる。

async.reduce と call stack overflow

async.waterfall 利用時の call stack をみると、call stack overflow を起こし得ると想像がつく。何万もの関数をwaterfallにいれる事例は考えにくいが、引数に値のリストを取るような場合ならどうだろうか。

var async = require('async');
//Error.stackTraceLimit = Infinity;

var num = parseInt(process.argv.slice(2), 10);
var list = Array.from(Array(num).keys());

function method(memo, num, cb){
    cb(null, memo + num);
}

async.reduce(list, 0, method, function(err, result){
    console.log('done');
});
$ node app.js 1940
done
$ node app.js 1950
bootstrap_node.js:432
    const script = new ContextifyScript(code, options);
                   ^
RangeError: Maximum call stack size exceeded
    at runInThisContext (bootstrap_node.js:432:20)
    at NativeModule.compile (bootstrap_node.js:520:18)
    at NativeModule.require (bootstrap_node.js:466:18)
    at tty.js:4:13
    at NativeModule.compile (bootstrap_node.js:525:7)
    at NativeModule.require (bootstrap_node.js:466:18)
    at createWritableStdioStream (internal/process/stdio.js:142:19)
    at process.getStdout [as stdout] (internal/process/stdio.js:10:14)
    at console.js:100:37
    at NativeModule.compile (bootstrap_node.js:525:7)

2000件のデータをそのまま async.reduec にいれればこういうことになる。

async.reduceと async.waterfall の組み合わせとcall stack overflow を考える

多数のデータをもつcollection を順次処理にかけるリスクがわかった。より実際の処理で起こり得そうな状況として、async.waterfallとの組み合わせを考える。

var async = require('async');
//Error.stackTraceLimit = Infinity;

var num = parseInt(process.argv.slice(2), 10);
var list = Array.from(Array(num).keys());

function method(memo, num, cb) {
    cb(null, memo + num);
}

function reduce(cb) {
    async.reduce(list, 0, method, function(err, result){
        cb(err);
    });
}

async.waterfall([reduce, reduce, reduce, reduce, reduce], function(err, result){
    console.log('done');
})
$ node app.js 400
done
$ node app.js 500
[directory_path]/node_modules/async/dist/async.js:843
        callFn.apply(this, arguments);
               ^

RangeError: Maximum call stack size exceeded
    at [directory_path]/node_modules/async/dist/async.js:843:16
    at [directory_path]/node_modules/async/dist/async.js:2487:13
    at method ([directory_path]/app.js:8:2)
    at [directory_path]/node_modules/async/dist/async.js:2485:9
    at replenish ([directory_path]/node_modules/async/dist/async.js:881:17)
    at iterateeCallback ([directory_path]/node_modules/async/dist/async.js:866:17)
    at [directory_path]/node_modules/async/dist/async.js:843:16
    at [directory_path]/node_modules/async/dist/async.js:2487:13
    at method ([directory_path]/app.js:8:2)

async.doWhilstと無限ループを考える

記事冒頭でcall stack overflowの原因に再帰呼び出しを考えたが、無限ループの場合も考えておく。async.doWhilstは繰り返しを実現する関数だ。

var async = require('async');
Error.stackTraceLimit = Infinity;

var n = 0;
var limit =parseInt(process.argv.slice(2), 10);

async.doWhilst(
  function(cb) {
    n++;
    cb(null);  
  },
  function() {
   return (n < limit);
  },
  function(err) {
        console.trace('done');
    }
);
$ node app.js 2 
Trace: done
    at [directory_path]/app.js:16:13
    at [directory_path]/node_modules/async/dist/async.js:843:16
    at [directory_path]/node_modules/async/dist/async.js:2938:18
    at apply ([directory_path]/node_modules/async/dist/async.js:21:25)
    at [directory_path]/node_modules/async/dist/async.js:56:12
    at [directory_path]/app.js:10:5
    at [directory_path]/node_modules/async/dist/async.js:2937:44
    at apply ([directory_path]/node_modules/async/dist/async.js:21:25)
    at [directory_path]/node_modules/async/dist/async.js:56:12
    at [directory_path]/app.js:10:5
    at Object.doWhilst ([directory_path]/node_modules/async/dist/async.js:2940:5)
    at Object.<anonymous> ([directory_path]/app.js:7:7)
    at Module._compile (module.js:571:32)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:422:7)
    at startup (bootstrap_node.js:143:9)
    at bootstrap_node.js:537:3

少々わかりにくいが、app.js:10:5が2回呼ばれているのがわかる。ここで、無限ループに陥った場合を考える。

var async = require('async');

var n = 0;
var limit =parseInt(process.argv.slice(2), 10);

async.doWhilst(
  function(cb) {
    n++;
    cb(null);  
  },
  function() {
    return true;
   // return (n < limit);
  },
  function(err) {
        console.log('done');
    }
);
$ node app.js 
[directory_path]/node_modules/async/dist/async.js:76
function identity(value) {
                 ^

RangeError: Maximum call stack size exceeded
    at identity ([directory_path]/node_modules/async/dist/async.js:76:18)
    at [directory_path]/node_modules/async/dist/async.js:55:24
    at [directory_path]/app.js:9:5
    at [directory_path]/node_modules/async/dist/async.js:2937:44
    at apply ([directory_path]/node_modules/async/dist/async.js:21:25)
    at [directory_path]/node_modules/async/dist/async.js:56:12
    at [directory_path]/app.js:9:5
    at [directory_path]/node_modules/async/dist/async.js:2937:44
    at apply ([directory_path]/node_modules/async/dist/async.js:21:25)
    at [directory_path]/node_modules/async/dist/async.js:56:12

この場合もcall stack があふれる。

むすび

call stack の大きさには気をつける。

Azure App Serviceをさわってみるのに困ったこと

基本的に下記を見てうまくいった。 docs.microsoft.com

一部困ったことがあったので、解決結果を記録しておく。

テスト用に無料プランがあるはずなのに見つからない

App Serviceプランを設定する際に、価格レベルの選択でFreeプランを見つけられなくて困った。 f:id:FledglingRobin:20170114221201p:plain 画面右上の「お勧め」を「すべて表示」に変えたら見つかった。 f:id:FledglingRobin:20170114220956p:plain ちなみに、Freeプランだと、

CPU時間は1日あたり60分まで、または、5分あたり2.5分まで

の制限がかかるので用途によっては向かない。

ローカルgitをデプロイ元にする設定場所が見つからない

App Serviceの展開元にあった。 f:id:FledglingRobin:20170114221612p:plain

環境変数をどうやって設定するのか

コンソールからできなかったので困った。 アプリケーション設定のアプリ設定にあった。 f:id:FledglingRobin:20170114221724p:plain

Node.jsドキュメントからbase64エンコーディングをおさらい

Node.jsでbase64エンコーディング、と検索すると下記の方法を紹介したページがずらっと見つかる。

var buffer = new Buffer('変換前の文字列');
var string = buffer.toString('base64');
console.log(string);
$ node base64_test.js 
5aSJ5o+b5YmN44Gu5paH5a2X5YiX

これってなんだろう、ということでおさらいをしてみる。

Bufferクラスは何か

Bufferクラスってなんだろう、ということで Buffer | Node.js v7.4.0 Documentation を読み直してみた。

BufferクラスはNode.js v7.4.0現段階でStable。もともとは、任意なバイナリデータ、octet-streamを扱うために使われていた。ECMAScript 2015 (ES6)でTypedArrayが導入された今は、Uint8Array APIをNode.js向けに提供している。

TypedArrayは何か

ここで一度TypedArrayについておさらいする。Javascript typed arrays - JavaScript | MDNが読みやすかった。

TypedArrayはES6で導入された、バイナリデータを効率的に扱うための型付き配列。もともとJavaScriptで利用されているArray連想配列なのに対して、純粋な配列として機能する。

var uint8 = new Uint8Array(2);
uint8[0] = 42;

例えば下記の型が提供されている。

  • Int8Array
  • Uint8Array
  • Int16Array
  • Uint16Array
  • Float32Array

このうちUnit8Arrayは、1バイト8 ビット長符号なし整数値を扱う。詳しくはECMAScript 2015 Language Specification – ECMA-262 6th Editionをみればいい。

結局Node.jsのBufferクラスは何をしているの

そのTypedArrayがES6に導入された今、Bufferクラスは何をしているのだろう。

BufferクラスはNode.jsが提供するクラスで、ES6で導入されたTypedArrayのUnit8Arrayを扱うクラスとして提供されている。Bufferクラスは固定長のバッファをメモリに確保する。

文字列については、Node.js v7.4.0 では下記8つのエンコード・デコードができる。

  • ascii
  • utf8
  • utf16le
  • ucs2
  • base64
  • latin1
  • binary
  • hex

例えば、10バイト確保して0埋めする。

const buf = Buffer.alloc(10);

例えば、文字列をASCIIのバイト列にしたときの長さ分確保して、バイト列を格納する。testのASCIIコードは[0x74, 0x65, 0x73, 0x74]になる。

const buf = Buffer.from('test');

例えば、同様のことをUTF-8で行う。

const buf = Buffer.from('tést', 'utf8');

fromを使わないコンストラクタの使用はv6.0.0で非推奨

冒頭で示したコードではnew Buffer(string[, encoding])を使っていた。

var buffer = new Buffer('変換前の文字列');
var string = buffer.toString('base64');
console.log(string);

どうやら、Buffer | Node.js v7.4.0 Documentationによると、

Stability: 0 - Deprecated: Use Buffer.from(string[, encoding]) instead.

とのことで、v6.0.0以降のNode.jsを使う場合は上記のBuffer.fromを使うのがよいらしい。

// base64 エンコード
const buf = Buffer.from('変換前の文字列');
const base64 = buf.toString('base64');
console.log(base64);    

// base64 デコード
const string = Buffer.from(base64, 'base64');
console.log(string.toString());
$ node base64_test.js 
5aSJ5o+b5YmN44Gu5paH5a2X5YiX
変換前の文字列

むすび

下記のコードは、バイナリデータを扱うためのクラスを利用してbase64エンコードを実施している。なお、Node.js v6.0.0以降はBuffer([string])が非推奨でBuffer.from([string])の使用が推奨されている。

var buffer = new Buffer('変換前の文字列');
var string = buffer.toString('base64');
console.log(string);

log4js-nodeのおさらい

なんとなくで使い始めたlog4js、改めておさらいをしておく。

github.com

log4js-nodeってなんだろう

Node.jsでlogをとるときに便利なフレームワークJavaで使われていたLog4J、それをJavaScript用に実現したLog4jsをNode.js向けにしたものがlog4js-node。npm installする際にはnpm install log4jsと書き、-nodeの部分はつけなくていい。

log4js-nodeでできること

たとえば、下記のようなことができる。

  • consoleに色付きでログを出力させる
  • Node.jsのconsole.logをおきかえる
  • ファイルサイズや日付でログをわける

使ってみる

log4js.getLoggerでloggerを用意して、logger.debug関数でログを記述していく。

var log4js = require('log4js');
var logger = log4js.getLogger();
logger.debug("Some debug messages");

日付とDEBUGdefaultの部分に色がついてconsoleに表示される。

$ node log4js_test.js 
[2017-01-06 17:29:46.544] [DEBUG] [default] - Some debug messages

ファイルに記録させる

console出力だけじゃなく、logファイルに書き出ささせることができる。

var log4js = require('log4js');
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file('logs/console.log'));

var logger = log4js.getLogger();
logger.debug("Some debug messages");
$ node log4js_test.js 
[2017-01-06 17:37:15.824] [DEBUG] [default] - Some debug messages
$ ls logs/
console.log
$ cat logs/console.log 
[2017-01-06 17:37:15.824] [DEBUG] [default] - Some debug messages

ログレベルをわけることができる

debugログ以外のログレベルも利用できる。

  • trace
  • debug
  • info
  • warn
  • error
  • fatal
var log4js = require('log4js');
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file('logs/console.log'));
var logger = log4js.getLogger();

// 実際にログを出力してみる
logger.trace('traceログです');
logger.debug('デバッグログです');
logger.info('infoログです');
logger.warn('警告です');
logger.error('エラーがでました');
logger.fatal('致命的な問題です');

出力がカラフルでとてもかわいい。

$ node log4js_test.js 
[2017-01-06 17:41:48.087] [TRACE] [default] - traceログです
[2017-01-06 17:41:48.093] [DEBUG] [default] - デバッグログです
[2017-01-06 17:41:48.094] [INFO] [default] - infoログです
[2017-01-06 17:41:48.094] [WARN] [default] - 警告です
[2017-01-06 17:41:48.094] [ERROR] [default] - エラーがでました
[2017-01-06 17:41:48.094] [FATAL] [default] - 致命的な問題です

低いレベルのログを無視する

setLevelを使うと、どのレベル以上のログを表示するか設定できる。

var log4js = require('log4js');
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file('logs/console.log'));

// loggerを用意する
var logger = log4js.getLogger();
logger.setLevel('ERROR');

// 実際にログを出力してみる
logger.trace('traceログです');
logger.debug('デバッグログです');
logger.info('infoログです');
logger.warn('警告です');
logger.error('エラーがでました');
logger.fatal('致命的な問題です');

この場合はlogger.setLevel('ERROR');で、errorfatalのほかが無視されている。

$ node log4js_test.js 
[2017-01-06 17:45:06.062] [ERROR] [default] - エラーがでました
[2017-01-06 17:45:06.069] [FATAL] [default] - 致命的な問題です
$ cat logs/console.log 
[2017-01-06 17:45:06.062] [ERROR] [default] - エラーがでました
[2017-01-06 17:45:06.069] [FATAL] [default] - 致命的な問題です

設定をまとめて書く

var log4js = require('log4js');
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file('logs/console.log'));

この部分をまとめて記述することができる。{type: 'console'}を外すとconsole出力がなくなる。{type: 'file', filename: 'logs/console.log'}を外すとファイル出力がなくなる。

var log4js = require('log4js');
log4js.configure({
  appenders: [
    {type: 'console'},
    {type: 'file', filename: 'logs/console.log'}
  ]
});

この設定は外部ファイルに記述することもできる。これが使いやすそう。

var log4js = require('log4js');
log4js.configure('config/log4js.json');

// loggerを用意する
var logger = log4js.getLogger();
logger.setLevel('ERROR');

// 実際にログを出力してみる
logger.trace('traceログです');
logger.debug('デバッグログです');
logger.info('infoログです');
logger.warn('警告です');
logger.error('エラーがでました');
logger.fatal('致命的な問題です');
{
    "appenders": [{
            "type": "console"
        },
        {
            "type": "file",
            "filename": "logs/console.log"
        }
    ]
}

Nodeのconsole.log上書き

標準だとconsole.logは上書きされない。

var log4js = require('log4js');
log4js.configure('config/log4js.json');

// loggerを用意する
var logger = log4js.getLogger();
logger.setLevel('ERROR');

// 実際にログを出力してみる
console.error('Node.jsのconsole.errorです');
$ node log4js_test.js 
Node.jsのconsole.errorです

Nodeのconsole.logを上書き利用する場合は、replaceConsoleの設定をする。

{
    "appenders": [{
            "type": "console"
        },
        {
            "type": "file",
            "filename": "logs/console.log"
        }
    ],
    "replaceConsole": true
}

これで、Node.jsのconsoleで書かれたlogをlog4jsで扱えるようになる。

$ node log4js_test.js 
[2017-01-06 18:33:15.227] [ERROR] console - Node.jsのconsole.errorです

logのカテゴリーをわける

たとえば送信時と受信時のlogをわけて保存したい場合。 まず、カテゴリーごとに保存先を指定しておく。

{
    "appenders": [{
            "type": "console"
        },
        {
            "type": "file",
            "filename": "logs/console_send.log",
            "category": "send"
        },
        {
            "type": "file",
            "filename": "logs/console_receive.log",
            "category": "receive"
        }
    ],
    "replaceConsole": true
}

カテゴリーの指定を変えてloggerを用意しておく。

var log4js = require('log4js');
log4js.configure('config/log4js.json');

// loggerを用意する
var loggerSend = log4js.getLogger('send');
var loggerReceive = log4js.getLogger('receive');

// 実際にログを出力してみる
loggerSend.info('サーバーがメッセージを送信しました');
loggerReceive.info('サーバーがメッセージを受信しました');

この結果、loggerSendloggerReceiveで保存先が変わる。

$ node log4js_test.js 
[2017-01-06 19:25:33.099] [INFO] send - サーバーがメッセージを送信しました
[2017-01-06 19:25:33.108] [INFO] receive - サーバーがメッセージを受信しました
$ cat logs/console_send.log 
[2017-01-06 19:25:33.099] [INFO] send - サーバーがメッセージを送信しました
$ cat logs/console_receive.log 
[2017-01-06 19:25:33.108] [INFO] receive - サーバーがメッセージを受信しました

日付ごとにファイルを変える

例えばpatternとして-yyyy-MM-ddを指定をすると、日付が変わる際にログファイルの末尾に-yyyy-MM-ddが付与される。

{
    "appenders": [{
            "type": "console"
        },
        {
            "type": "dateFile",
            "filename": "logs/console.log",
            "pattern": "-yyyy-MM-dd" 
        }
    ],
    "replaceConsole": true
}

expressと使う場合にはexpressのlogもlog4jsで扱うことができる

Connect Logger · nomiddlename/log4js-node Wiki · GitHub

最後に

log4js-nodeについてざっと振り返りをした。他にも設定できる項目があるので、使いながらくわしい動きを調べていきたい。

広告を非表示にする

kuromoji.jsで形態素解析をする

slack botをもう少し賢くしたい。といかけに含まれる名詞に反応できるようにしたい。そこで、kuromoji.jsによる形態素解析をためしてみる。 github.com

$ node kuromoji_test.js 
文をどうぞ 明日は関ヶ原で憎きあの男を倒すの
[ '明日', '関ヶ原', '男' ]
[ '倒す' ]
[ '憎い' ]

さっそく使ってみた

var readline = require('readline');
var kuromoji = require("kuromoji");

kuromoji.builder({
    dicPath: "node_modules/kuromoji/dict"
}).build(function(err, tokenizer) {
    if (err) {
        throw err;
    }

    // 標準入力をうけつけるインターフェース
    var rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    // 入力をうけつけて解析する
    rl.question("文をどうぞ ", function(answer) {
        var result = tokenizer.tokenize(answer);
        console.log(result);
        rl.close();
    });
});

実際に動かしてみた。

$ node kuromoji_test.js 
文をどうぞ 今日はがんばったんだよ
[ { word_id: 126270,
    word_type: 'KNOWN',
    word_position: 1,
    surface_form: '今日',
    pos: '名詞',
    pos_detail_1: '副詞可能',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '今日',
    reading: 'キョウ',
    pronunciation: 'キョー' },
  { word_id: 93010,
    word_type: 'KNOWN',
    word_position: 3,
    surface_form: 'は',
    pos: '助詞',
    pos_detail_1: '係助詞',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'は',
    reading: 'ハ',
    pronunciation: 'ワ' },
  { word_id: 3153770,
    word_type: 'KNOWN',
    word_position: 4,
    surface_form: 'がんばっ',
    pos: '動詞',
    pos_detail_1: '自立',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '五段・ラ行',
    conjugated_form: '連用タ接続',
    basic_form: 'がんばる',
    reading: 'ガンバッ',
    pronunciation: 'ガンバッ' },
  { word_id: 23430,
    word_type: 'KNOWN',
    word_position: 8,
    surface_form: 'た',
    pos: '助動詞',
    pos_detail_1: '*',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '特殊・タ',
    conjugated_form: '基本形',
    basic_form: 'た',
    reading: 'タ',
    pronunciation: 'タ' },
  { word_id: 63530,
    word_type: 'KNOWN',
    word_position: 9,
    surface_form: 'ん',
    pos: '名詞',
    pos_detail_1: '非自立',
    pos_detail_2: '一般',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'ん',
    reading: 'ン',
    pronunciation: 'ン' },
  { word_id: 23680,
    word_type: 'KNOWN',
    word_position: 10,
    surface_form: 'だ',
    pos: '助動詞',
    pos_detail_1: '*',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '特殊・ダ',
    conjugated_form: '基本形',
    basic_form: 'だ',
    reading: 'ダ',
    pronunciation: 'ダ' },
  { word_id: 92300,
    word_type: 'KNOWN',
    word_position: 11,
    surface_form: 'よ',
    pos: '助詞',
    pos_detail_1: '終助詞',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'よ',
    reading: 'ヨ',
    pronunciation: 'ヨ' } ]

品詞と標準型をとりだしてみる

var readline = require('readline');
var kuromoji = require("kuromoji");

kuromoji.builder({
    dicPath: "node_modules/kuromoji/dict"
}).build(function(err, tokenizer) {
    if (err) {
        throw err;
    }

    // 標準入力をうけつけるインターフェース
    var rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    // 入力をうけつけて解析する
    rl.question("文をどうぞ ", function(answer) {
        var result = tokenizer.tokenize(answer);

        // ひとつずつ確認する
        result.forEach(function(word) {
            console.log(word.pos + ' ' + word.basic_form);
        });


        rl.close();
    });
})

標準形にできると形容詞の使い勝手があがりそう。

$ node kuromoji_test.js 
文をどうぞ 明日はマラソン大会で今からすごい憂鬱なの
名詞 明日
助詞 は
名詞 マラソン
名詞 大会
助詞 で
名詞 今
助詞 から
形容詞 すごい
名詞 憂鬱
助動詞 だ
助詞 の

名詞・動詞・形容詞ごとにとりだせるようにしておく

var readline = require('readline');
var kuromoji = require("kuromoji");

kuromoji.builder({
    dicPath: "node_modules/kuromoji/dict"
}).build(function(err, tokenizer) {
    if (err) {
        throw err;
    }

    // 標準入力をうけつけるインターフェース
    var rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    // 入力をうけつけて解析する
    rl.question("文をどうぞ ", function(answer) {
        var result = tokenizer.tokenize(answer);

        var noun = [],
            verb = [],
            adjective = [];

        // ひとつずつ確認する
        result.forEach(function(word) {
            switch (word.pos) {
                case '名詞':
                    noun.push(word.basic_form);
                    break;
                case '動詞':
                    verb.push(word.basic_form);
                    break;
                case '形容詞':
                    adjective.push(word.basic_form);
                    break;
            }
        });

        // 一覧を確認する
        console.log(noun);
        console.log(verb);
        console.log(adjective);

        rl.close();
    });

結果はこんな感じ。使いやすそうなので、実際に組み込んでみる。

$ node kuromoji_test.js 
文をどうぞ 明日は関ヶ原で憎きあの男を倒すの
[ '明日', '関ヶ原', '男' ]
[ '倒す' ]
[ '憎い' ]

Botkitでできること

Botkitでslcak botをつくることになった。かんたんにbotが作れると評判のslack bot、標準でどんなことができるのかREADMEをしらべてみる。

github.com

f:id:FledglingRobin:20170102220928p:plain:w300

ユーザーからのメッセージに反応する

message_receivedイベントの購読で、なにかしらの書き込みに反応できる。ほかにdirect_mentiondirect_messageイベントでそれぞれの書き込みに反応できる。メッセージを返すのはbot.reply関数を使ってできる。

controller.on('message_received', function(bot, message) {
    bot.reply(message, 'I heard... something!');
});

hears関数で特定のワードに反応できる。ワードはリストで指定できる。

controller.hears(['keyword1', 'keyword2'], ['message_received'], function(bot, message) {
   bot.reply(message, 'You used a keyword!');
});

hears関数ではパターンマッチングがつかえる。マッチした結果はmessage.matchフィールドに格納される。このフィールドにはJavaScriptstring.matchと同じ形式で情報が格納されている。

controller.hears('open the (.*) doors', ['message_received'], function(bot, message) {
   var doorType = message.match[1];
   if (doorType === 'pod bay') {
      return bot.reply(message, 'I\'m sorry, Dave. I\'m afraid I can\'t do that.');
   }
      return bot.reply(message, 'Okay');
});

botからメッセージを送る

ユーザからのメッセージにひとつずつreplayするだけでなく、bot.say関数でbotからメッセージを発信することもできる。

controller.hears(['hello world'], 'message_received', function(bot, message) {
   bot.startConversation(message, function(err, convo) {
      convo.say('Hello!');
      convo.say('Have a nice day!');
   });
});

ユーザーにといかけて返答を待つこともできる。ask関数を利用する。ユーザから返答がきたあとの処理をcallbackに記述しておけばいい。

controller.hears(['question me'], 'message_received', function(bot, message) {
   bot.startConversation(message, function(err, convo) {
      convo.ask('How are you?', function(response, convo) {
         convo.say('Cool, you said: ' + response.text);
         convo.next();
    });
  })
});

会話が複雑になってくるとcallbackの管理がつらくなってくる。そんなときのためにThered機能がある。

bot.createConversation(message, function(err, convo) {

    // create a path for when a user says YES
    convo.addMessage({
            text: 'You said yes! How wonderful.',
    },'yes_thread');

    // create a path for when a user says NO
    convo.addMessage({
        text: 'You said no, that is too bad.',
    },'no_thread');

    // create a path where neither option was matched
    // this message has an action field, which directs botkit to go back to the `default` thread after sending this message.
    convo.addMessage({
        text: 'Sorry I did not understand.',
        action: 'default',
    },'bad_response');

    // Create a yes/no question in the default thread...
    convo.ask('Do you like cheese?', [
        {
            pattern: 'yes',
            callback: function(response, convo) {
                convo.changeTopic('yes_thread');
            },
        },
        {
            pattern: 'no',
            callback: function(response, convo) {
                convo.changeTopic('no_thread');
            },
        },
        {
            default: true,
            callback: function(response, convo) {
                convo.changeTopic('bad_response');
            },
        }
    ]);

    convo.activate();
});

いくつかの質問を続けるような場合は、ほかの関数を呼び出すような形で書くこともできる。

controller.hears(['pizzatime'], 'message_received', function(bot,message) {
     var askFlavor = function(err, convo) {
        convo.ask('What flavor of pizza do you want?', function(response, convo) {
           convo.say('Awesome.');
           askSize(response, convo);
           convo.next();
        });
     };
     var askSize = function(response, convo) {
        convo.ask('What size do you want?', function(response, convo) {
           convo.say('Ok.')
           askWhereDeliver(response, convo);
           convo.next();
        });
     };
     var askWhereDeliver = function(response, convo) {
        convo.ask('So where do you want it delivered?', function(response, convo) {
          convo.say('Ok! Good bye.');
          convo.next();
        });
     };

     bot.startConversation(message, askFlavor);
});

ユーザの「はい」「いいえ」を藩閥するのに便利な関数も提供されている。bot.utterances.yesyes, yeah, yup, ok and sureにヒットする。 bot.utterances.nono, nah, nopeにヒットする。 日本語には対応していないみたいなので拡張をしながら使う。

情報の保存

デフォルトだとBotkitはJSON filesに書き込みをする形で情報を保存する。保存先を指定することができる。

var controller = Botkit.slackbot({
   json_file_store: 'path_to_json_database'
});

保存する際にキーを設定する。キーにはユーザーやチャンネル、チームを利用するといい。

controller.storage.users.save({id: message.user, foo:'bar'}, function(err) { ... });
controller.storage.users.get(id, function(err, user_data) {...});
controller.storage.users.delete(id, function(err) {...});
controller.storage.users.all(function(err, all_user_data) {...});

JSON files以外にもDBなどを利用することもできる。

var controller = Botkit.slackbot({
  storage: my_storage_provider
})
広告を非表示にする