FledglingRobin’s diary

Github: https://github.com/FledglingRobin

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
})