Windows10 IoT CoreでもSlackのOutgoingWebhooksを実装してみる

Windows10 IoT CoreでもSlackのOutgoingWebhooksを実装してみる

以前のエントリーでまどべんよっかいちにてRaspberryPi(Raspbian)でSlackのIntegrationであるOutgoingWebhooksの連携を行いましたが、少しもやもやしていたのでWindows10 IoT Coreでも実装してみました。

uepon.hatenadiary.com

www.slideshare.net

OutgoingWebhooksの設定に関してはSlideShareの資料を見てもらえれば良いかと思います。

今回もヨロイ元帥botを使っていきます。

ヨロイ元帥Botとは

Slackの書き込み中に「仮面ライダー」というキーワードあったら、

【投稿したUser_name】+“裏切者は殺す。デストロンを代表して。ヨロイ元帥”

と書き込むBotになります。

先日のアメトーーク!の放送で「仮面ライダー芸人」のOAがあったのでヨロイ元帥もかなり認知度が上がっているのではないでしょうか。

Windows10 IoT Core特有の問題

今回はUWPとC#という組み合わせだったので少してこずりました。というのも、Webサーバ的な機能がデフォルトでUWPでは持たせることができないところに理由があります。 もう少しダイレクトにいうならば、System.Net.HttpListenerが使えないのでHTTPの待ち受けをしてくれるような便利クラスがないということになります。 そのため、

  • TcpListenerクラス
  • StreamSocketListenerクラス

のいずれかを使用することになるのかなと思います。

試してはいませんがIoT向けのIotWeb HTTP Serverというライブラリもあるようなのでそっちを使ったほうが楽なんだと思います。

IotWeb HTTP Server

今回はStreamSocketListenerクラスをつかってWebサーバを作成してOutgoingWebhooksに対応することにします。

実はStreamSocketListenerを使ったサンプルがGithubにあったはずなのですがなくなってしまったようです。残念。

App2App WebServer https://github.com/ms-iot/samples/tree/develop/App2App%20WebServer

現在は404になっています。

WebServerのクラス

基本的には素のソケット通信でWebServerを作成することになります。 ただ、StreamSocketListenerはもう少し便利になっていて、 通信を検知するとイベント(ConnectionReceived)を検知してくれます。 そのため、このイベントに対処するイベントハンドラを作成すればデータの受信時の動きを簡単に書くことができます。

Listener_ConnectionReceived()メソッドがコネクションが張られたときに動作するイベントに該当します。 逆にデータの送信に関してはSendResponse()メソッドが該当しています。 送信に関してはOutgoingWebhooksではJSONを使用しているため、nugetでNewtonsoft.Jsonを行う必要があります。

SlackのOutgoingWebhooksのデータ送信形式に関してはSlideShareの資料を参考にしてください。

nugetするパッケージ

f:id:ueponx:20160806161529j:plain

今回はtokenデータによる簡単な認証を行っています。

using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using Windows.Networking.Sockets;
using Windows.Storage.Streams;

public class SimpleOutgoingWebhooksWebServer
{
    StreamSocketListener listener;
    StreamSocket socket;
    public event Action<string> OnReceived;
    private string tokenBlock = "token=";

    public int PORT { get; set; }
    public string TOKEN { get; set; }

    public SimpleOutgoingWebhooksWebServer(int port, string token)
    {
        this.TOKEN = token;
        tokenBlock += this.TOKEN;
        this.PORT = port;
    }

    public async void Start()
    {
        listener = new StreamSocketListener();
        listener.ConnectionReceived += Listener_ConnectionReceived;
        await listener.BindServiceNameAsync(PORT.ToString());
    }

    private async void Listener_ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args)
    {
        socket = args.Socket;
        var dr = new DataReader(socket.InputStream);

        /// 受信データの取り出し
        StringBuilder request = new StringBuilder();
        uint BufferSize = 2048;
        using (IInputStream input = socket.InputStream)
        {
            byte[] data = new byte[BufferSize];
            IBuffer buffer = data.AsBuffer();
            uint dataRead = BufferSize;
            while (dataRead == BufferSize)
            {
                await input.ReadAsync(buffer, BufferSize, InputStreamOptions.Partial);
                request.Append(Encoding.UTF8.GetString(data, 0, data.Length));
                dataRead = buffer.Length;
            }
        }

        // メソッド部分の処理
        var receiveString = request.ToString();
        var source = receiveString.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
        var content = source.Last().Split(new[] { '&' });

        var requestMethod = source[0];
        string[] requestParts = requestMethod.Split(' ');
        var text = requestParts[1];

        if (content.Contains(tokenBlock) == false)
        {
            return;//token認証NG
        }

        var x = content.Where(e => e.Contains("user_name=")).FirstOrDefault();
        var userName = "";
        if (x != null)
        {
            userName = x.Substring("user_name=".Length);
        }

        if (text.Contains("favicon.ico") == false) //faviconのリクエストは無視
        {
            /// 受信イベント
            if (this.OnReceived != null)
            {
                OnReceived(userName);
            }
        }
    }

    public async void SendResponse(string text)
    {
        if (socket == null) return;

        var data = new SendJSONData
        {
            text = text
        };
        var json = JsonConvert.SerializeObject(data);
        byte[] bodyArray = Encoding.UTF8.GetBytes(json);
        MemoryStream stream = new MemoryStream(bodyArray);
        string header = String.Format(
                            "HTTP/1.0 200 OK\r\n" +
              "Content-Type: text/javascript; charset=utf-8\r\n" +
                            "Content-Length: {0}\r\n" +
                            "Connection: close\r\n\r\n",
                            stream.Length); //ヘッダ部分
        var dw = new DataWriter(socket.OutputStream);
        dw.WriteString(header);
        dw.WriteString(json);
        await dw.StoreAsync();
    }

    private class SendJSONData
    {
        public string text { get; set; }
    }
}

このクラスをもとにメイン処理を記述していきます。

MainPage.xaml.cs

メインの処理は先ほどのSimpleOutgoingWebhooksWebServerクラスを使うことで比較的短く書くことができます。 コンストラクタの第1引数の部分が受け待ちになるport番号です。 また、コンストラクタの第2引数の部分が設定時に指定されるtokenになりますので適宜変更してください。

server_OnReceived()はデータの受信後に発生するイベント(.OnReceivedイベント)のハンドラになります。

今回はLEDなどを点灯していませんが、ここでLEDの処理を入れればいいかなと思います。

using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Core;

namespace IoTCoreWebServer
{
    public sealed partial class MainPage : Page
    {
        SimpleOutgoingWebhooksWebServer server;

        public MainPage()
        {
            this.InitializeComponent();

            server = new SimpleOutgoingWebhooksWebServer(8888, "xxxxxxxxxxxxxxxxx");
            server.OnReceived += server_OnReceived;
            server.Start();
        }
        private async void server_OnReceived(string data)
        {
            // Request受信
            await Dispatcher.RunAsync(
                CoreDispatcherPriority.Normal,
                () => { textReceive.Text = data; }); // 受信データをテキストボックスに表示


            // 応答を送信
            server.SendResponse(data + "裏切り者は殺す。デストロンを代表して。ヨロイ元帥");

        }
    }
}

一応これでコーディングは終了です。

Package.appxmanifestの変更

ただし、これだけでは動作しません。ソリューションマネージャにあるPackage.appxmanifestファイルを開き機能の追加を行います。

  • インターネット(クライアント)
  • インターネット(クライアントとサーバー)

Package.appxmanifestをクリックして

f:id:ueponx:20160806161536j:plain

機能タブを選択すると表示されます。

f:id:ueponx:20160806161542j:plain

この2つにチェックをいれて保存します。これが設定されていないと動作しません。注意が必要です。

動作

何とか動いてくれました。

f:id:ueponx:20160806161554j:plain

終わりに

Raspbianに比べると特殊事情がおおいWindows10 IoT Coreですが、VisualStudioのデバック機能が優秀なため、引っかかっても悩むことは少ないと感じました。 また、UWPアプリであるところが癖でもありますが、セキュリティーの面はそれなりにいいかもなあという印象もあります。

まだまだ接続機器に不安が残りますが、どちらも使っていこうと思います。

RaspberryPiのWindows 10 IoT CoreでGPIOを触ってみる

RaspberryPiのWindows 10 IoT CoreでGPIOを触ってみる

仕事が忙しがしく、ほとんど休みも微妙だったのですがやっと時間が取れるようになってきたので、久々にWindows10IotCoreを触ってみました。

今後はRaspianだけでなくWindows 10 IoT Coreも触っていくことにしていきます。(そんなにPythonやnode.jsがとくいというわけでもないので)

インストールに関しては、いろいろなブログもあるけど公式の情報だけでもそこそこわかりやすいと思います。 デフォルトのパスワードが引っかかるところなので、そこだけp@ssw0rdと覚えておけばいいかと。

ということで、インストールの手順は省きます。

なにはなくともLチカだろ

避けて通ることのできないLチカです。実際のコードとなるとVisual Studioの一択だと思いますので、これもインストールは省きます。個人的には全部入れるようにしています。

プロジェクトの初期設定

まずはVisual Studioを起動して新しいプロジェクトを作成します。以下のような画面になるので

f:id:ueponx:20160731113720j:plain

テンプレートの中から【C#】->【ユニバーサル】から「空白のアプリ(ユニバーサルWindows)」を選択します。 プロジェクト名は任意のものをつけて【OK】を押します。

f:id:ueponx:20160731113725j:plain

最小ターゲットの確認ダイアログが表示されますがそのまま【OK】を押して問題ないです。 すると以下のようにプロジェクトが作成されます。

f:id:ueponx:20160731113730j:plain

通常のUWPの開発であればそのままでいいのですが、IoTCoreの場合には一手間が必要になります。

f:id:ueponx:20160731113737j:plain

メニューから【参照マネージャー】を選択して左の【Universal Windows】->【拡張】を選択し右上にある検索ボックスに"iot"を入力します。すると、【Windows IoT Extensions for the UWP】が表示されるので、少し前に表示されたターゲットバージョンに合うものを選択します(チェックを入れる)。(今回は10.0 10586を選択します)

f:id:ueponx:20160731113741j:plain

確認できたら【OK】を押します。OKを押すとソリューションエクスプローラでプロジェクトが以下のようになっているでしょうか。(参照の中にWindows IoT Extensions...が入っていれば問題ありません)

f:id:ueponx:20160731113756j:plain

実際にコーディングするのはこの中のMainPage.xaml.csになります。がその前にターゲットをRaspberryPiに変更しておきます。画面の上部のメニューバーに以下のような部分があると思います。このままではWindows10のPC用のデスクトップをターゲットにしていることになっていますので

f:id:ueponx:20160731113759j:plain

x86】から【ARM】に変更します。続いてデバイスと表示されている部分のプルダウンを選択し、

【変更前】 f:id:ueponx:20160731113803j:plain

【変更後】 f:id:ueponx:20160731113806j:plain

リモートコンピュータを選択します。

f:id:ueponx:20160731113810j:plain

すると以下のようなダイアログが表示されます。

f:id:ueponx:20160731113831j:plain

既にRaspberryPiが起動している場合には【自動検出】でWindows 10 IoT Coreがインストールされたデバイスが見えているかと思います。 IPアドレスを確認して問題がない場合にはその部分をクリックします。

画面の表示が詳細表示になります。

f:id:ueponx:20160731113852j:plain

問題がなければ【選択】をクリックすると事前の設定は完了となります。

Lチカのためのコーディング

プロジェクトの設定が終わったので以降はコーディングとなります。MainPage.xaml.csを以下のコードに書き換えます。(不要なusingを削除しているので結構小さくなります。)

using Windows.UI.Xaml.Controls;

using Windows.Devices.Gpio;
using System.Threading.Tasks;

namespace App
{
    public sealed partial class MainPage : Page
    {
        private const int LED_PIN = 4;
        private GpioPin pin;

        public MainPage()
        {
            this.InitializeComponent();

            setupGPIO();
            loop();
        }

        private void setupGPIO()
        {
            var gpio = GpioController.GetDefault();

            if(gpio == null)
            {
                pin = null;
                return;
            }
            pin = gpio.OpenPin(LED_PIN);
            pin.Write(GpioPinValue.High);
            pin.SetDriveMode(GpioPinDriveMode.Output);
        }

        private async void loop()
        {
            while (true)
            {
                pin.Write(GpioPinValue.Low);
                await Task.Delay(1000);
                pin.Write(GpioPinValue.High);
                await Task.Delay(1000);
            }
        }
    }
}

一応これでGPIO4Pinに接続したLEDの点滅を行うことができます。

部分的に説明をすると、

using Windows.Devices.Gpio;
using System.Threading.Tasks;

Windows.Devices.GpioをusingすることでGPIO関係のクラスの名前空間を使用できるようになります。 System.Threading.Tasksは中にある処理でスリープ(Delay)を行うために入れていますが、この部分はかなり自信がないので、もっとほかの処理に変えたほうがいいのかもしれません。(UWPではDelay処理を推奨していないので)

        private void setupGPIO()
        {
            var gpio = GpioController.GetDefault();

            if(gpio == null)
            {
                pin = null;
                return;
            }
            pin = gpio.OpenPin(LED_PIN);
            pin.Write(GpioPinValue.High);
            pin.SetDriveMode(GpioPinDriveMode.Output);
        }

上記がGPIOの初期設定を行う部分になります。 GPIOのLED_PINで指定されるPinを初期値をHI(ON)にしてアウトプットモード(出力設定)にしています。

        private async void loop()
        {
            while (true)
            {
                pin.Write(GpioPinValue.Low);
                await Task.Delay(1000);
                pin.Write(GpioPinValue.High);
                await Task.Delay(1000);
            }
        }

上記の部分が定期的に点滅させる処理になります。今回はArduinoを少し意識してループを作成したため、Task.Delayで間隔設定してみましたが、本来であればタイマー(DispatcherTimer)を使うべきだと思います。

これでLチカができました。


Windows10 IoT CoreでLチカ

まとめ

初期の設定はありますが、比較的カンタンにLチカまでは行けたかなと思います。

LチカするだけではRaspbianでもいいのですが、Visual Studioによる便利な機能(インテリセンスやデバッガ機能)、UIなどを作成することもできるのでそういう部分ではWindows 10 IoT Coreはおすすめできるのかもしれません。 ただ、まだドライバー関連では今ひとつの部分もあるので新しいデバイスを使おうと考えるなら、時期尚早なのかもしれません。

第15回まどべんよっかいちにいってきました。

7/23に行われた第15回まどべんよっかいちに参加してきました。

「まどべんよっかいち」とは?

三重県四日市市で開催しているIT系勉強会です。参加者が開発・研究・調査している分野の報告・紹介・情報交換の場を目指しています。 はじまりは.NET FrameworkWindowsなどMicrosoft社の話題が多かったのですが、最近ではいろいろなIT系の各ジャンルを扱っています。

今回自分のショートセッションではSlackが提供しているサービス連携(Integrationと呼ぶ)を使ってRaspberryPiと連携することを発表してきました。(Windowsの要素はほとんどないw)

www.slideshare.net

IFTTTやMicrosoftFlowなどのWeb連携サービスでは実現できないこともOutgoingWebhooksで実現できるのはいい発見になりました。応用すればSlackと家電との連携なんかもできるかなと思います。

自分の都合で午後からの参加だったので、他のセッションはあまり聞けてないのですが…Cookie Clickerは息の長いゲームだとわかりました。

最近、newcomerも増えてきているのでネタの仕入れが難しいですね。

Raspberry Pi で形態素解析をやってみる(後編)

Raspberry Pi形態素解析をやってみる(後編)

前回のエントリーでは形態素解析解析ライブラリであるMeCabMeCab: Yet Another Part-of-Speech and Morphological Analyzer )をインストールしてpythonから使用を行ってみました。

uepon.hatenadiary.com

今回はMeCabを使って語尾の変換をするプログラムを作ってみます。


基本的な処理の流れ

Mecabを使用してアプリケーションの基本的な流れとしては、

  1. MeCabTaggerインスタンスを生成
  2. parseメソッドを使用し、解析結果が文字列として返される
  3. 戻り値を使用して更に変換判断などの処理

という感じになります。

parse()メソッド

まずはparse()メソッドを使ってみます。

#!/usr/bin/env python
# coding: utf-8

import MeCab
import sys

m = MeCab.Tagger ("-Ochasen")
print m.parse("今日はとても良い天気ですね。")

これを実行すると以下のようになります。

$ python mecab_sample.py
今日    キョウ  今日    名詞-副詞可能
は      ハ      は      助詞-係助詞
とても  トテモ  とても  副詞-助詞類接続
良い    ヨイ    良い    形容詞-自立     形容詞・アウオ段        基本形
天気    テンキ  天気    名詞-一般
です    デス    です    助動詞  特殊・デス      基本形
ね      ネ      ね      助詞-終助詞
。      。      。      記号-句点
EOS

先ほどのような短い文章ではよくわからないので次の文書を使ってみます。

我輩は猫である。名前はまだ無い。どこで生れたか頓と見當がつかぬ。何で も薄暗いじめじめした所でニヤーニヤー泣いて居た事丈は記憶して居る。(「吾輩は猫である」より)

#!/usr/bin/env python
# coding: utf-8

import MeCab
import sys

m = MeCab.Tagger ("-Ochasen")
print m.parse("我輩は猫である。名前はまだ無い。どこで生れたか頓と見當がつかぬ。何でも薄暗いじめじめした所でニヤーニヤー泣いて居た事丈は記憶して居る。")

実行結果は次のようになります。

$ python mecab_sample.py
我輩    ワガハイ        我輩    名詞-一般
は      ハ      は      助詞-係助詞
猫      ネコ    猫      名詞-一般
で      デ      だ      助動詞  特殊・ダ        連用形
ある    アル    ある    助動詞  五段・ラ行アル  基本形
。      。      。      記号-句点
名前    ナマエ  名前    名詞-一般
は      ハ      は      助詞-係助詞
まだ    マダ    まだ    副詞-助詞類接続
無い    ナイ    無い    形容詞-自立     形容詞・アウオ段        基本形
。      。      。      記号-句点
どこ    ドコ    どこ    名詞-代名詞-一般
で      デ      で      助詞-格助詞-一般
生れ    ウマレ  生れる  動詞-自立       一段    連用形
た      タ      た      助動詞  特殊・タ        基本形
か      カ      か      助詞-副助詞/並立助詞/終助詞
頓      トミ    頓      名詞-一般
と      ト      と      助詞-格助詞-引用
見      ミ      見る    動詞-自立       一段    連用形
當      當      當      名詞-一般
が      ガ      が      助詞-格助詞-一般
つか    ツカ    つく    動詞-自立       五段・カ行イ音便        未然形
ぬ      ヌ      ぬ      助動詞  特殊・ヌ        基本形
。      。      。      記号-句点
何      ナニ    何      名詞-代名詞-一般
でも    デモ    でも    助詞-副助詞
薄暗い  ウスグライ      薄暗い  形容詞-自立     形容詞・アウオ段        基本形
じめじめ        ジメジメ        じめじめ        副詞-一般
し      シ      する    動詞-自立       サ変・スル      連用形
た      タ      た      助動詞  特殊・タ        基本形
所      トコロ  所      名詞-非自立-副詞可能
で      デ      で      助詞-格助詞-一般
ニヤーニヤー    ニヤーニヤー    ニヤーニヤー    名詞-一般
泣い    ナイ    泣く    動詞-自立       五段・カ行イ音便        連用タ接続
て      テ      て      助詞-接続助詞
居      イ      居る    動詞-自立       一段    連用形
た      タ      た      助動詞  特殊・タ        基本形
事      コト    事      名詞-非自立-一般
丈      タケ    丈      名詞-一般
は      ハ      は      助詞-係助詞
記憶    キオク  記憶    名詞-サ変接続
し      シ      する    動詞-自立       サ変・スル      連用形
て      テ      て      助詞-接続助詞
居る    イル    居る    動詞-自立       一段    基本形
。      。      。      記号-句点
EOS

これまで国語を真面目にやってなかったのでコメントできません。すみません。

parseToNode()メソッド

MeCabでは各形態素の詳細情報を取得するためにparseToNode()メソッドが準備されています。これは「文頭」という特別な要素でMeCabのNodeクラスのインスタンスとして取得できます。

以下のようなソースになるかと思います。戻り値はnodeクラスでその中にsurfaceとfeatureというメンバがあるのでなwhileを使って処理しています。

#!/usr/bin/env python
# coding:utf-8

import MeCab
tagger = MeCab.Tagger("-Ochasen")
node = tagger.parseToNode("輩は猫である。名前はまだ無い。どこで生れたか頓と見當がつかぬ。何で
も薄暗いじめじめした所でニヤーニヤー泣いて居た事丈は記憶して居る。")
while node:
    print "%s > %s" % (node.surface, node.feature)
    node = node.next

先ほども書きましたが、parseToNodeメソッドで得られるnodeクラスには

  • surface … 表層
  • feature … 現在の品詞の分析結果

となります。

簡単に言うと、surfaceはオリジナルの語句、featureは分析結果(必ずしもオリジナルの語句が含まれているとは限りません。)となります。

実行結果は次のようになります。

$ python mecab_sample2.py
 :> BOS/EOS,*,*,*,*,*,*,*,*
我輩 :> 名詞,一般,*,*,*,*,我輩,ワガハイ,ワガハイ
は :> 助詞,係助詞,*,*,*,*,は,ハ,ワ
猫 :> 名詞,一般,*,*,*,*,猫,ネコ,ネコ
で :> 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
ある :> 助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
。 :> 記号,句点,*,*,*,*,。,。,。
名前 :> 名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
は :> 助詞,係助詞,*,*,*,*,は,ハ,ワ
まだ :> 副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
無い :> 形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
。 :> 記号,句点,*,*,*,*,。,。,。
どこ :> 名詞,代名詞,一般,*,*,*,どこ,ドコ,ドコ
で :> 助詞,格助詞,一般,*,*,*,で,デ,デ
生れ :> 動詞,自立,*,*,一段,連用形,生れる,ウマレ,ウマレ
た :> 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
か :> 助詞,副助詞/並立助詞/終助詞,*,*,*,*,か,カ,カ
頓 :> 名詞,一般,*,*,*,*,頓,トミ,トミ
と :> 助詞,格助詞,引用,*,*,*,と,ト,ト
見 :> 動詞,自立,*,*,一段,連用形,見る,ミ,ミ
當 :> 名詞,一般,*,*,*,*,*
が :> 助詞,格助詞,一般,*,*,*,が,ガ,ガ
つか :> 動詞,自立,*,*,五段・カ行イ音便,未然形,つく,ツカ,ツカ
ぬ :> 助動詞,*,*,*,特殊・ヌ,基本形,ぬ,ヌ,ヌ
。 :> 記号,句点,*,*,*,*,。,。,。
何 :> 名詞,代名詞,一般,*,*,*,何,ナニ,ナニ
でも :> 助詞,副助詞,*,*,*,*,でも,デモ,デモ
薄暗い :> 形容詞,自立,*,*,形容詞・アウオ段,基本形,薄暗い,ウスグライ,ウスグライ
じめじめ :> 副詞,一般,*,*,*,*,じめじめ,ジメジメ,ジメジメ
し :> 動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
た :> 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
所 :> 名詞,非自立,副詞可能,*,*,*,所,トコロ,トコロ
で :> 助詞,格助詞,一般,*,*,*,で,デ,デ
ニヤーニヤー :> 名詞,一般,*,*,*,*,*
泣い :> 動詞,自立,*,*,五段・カ行イ音便,連用タ接続,泣く,ナイ,ナイ
て :> 助詞,接続助詞,*,*,*,*,て,テ,テ
居 :> 動詞,自立,*,*,一段,連用形,居る,イ,イ
た :> 助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
事 :> 名詞,非自立,一般,*,*,*,事,コト,コト
丈 :> 名詞,一般,*,*,*,*,丈,タケ,タケ
は :> 助詞,係助詞,*,*,*,*,は,ハ,ワ
記憶 :> 名詞,サ変接続,*,*,*,*,記憶,キオク,キオク
し :> 動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
て :> 助詞,接続助詞,*,*,*,*,て,テ,テ
居る :> 動詞,自立,*,*,一段,基本形,居る,イル,イル
。 :> 記号,句点,*,*,*,*,。,。,。
 :> BOS/EOS,*,*,*,*,*,*,*,*


分析してみる

この解析サンプルの傾向から推測するに

ある :> 助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
【変換後】ありますぞえ
。 :> 記号,句点,*,*,*,*,。,。,。

無い :> 形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
【変換後】無い+ですぞえ
。 :> 記号,句点,*,*,*,*,。,。,。

ぬ :> 助動詞,*,*,*,特殊・ヌ,基本形,ぬ,ヌ,ヌ
【変換後】ぬ+ぞえ
。 :> 記号,句点,*,*,*,*,。,。,。

居る :> 動詞,自立,*,*,一段,基本形,居る,イル,イル
【変換後】居る+ぞえ
。 :> 記号,句点,*,*,*,*,。,。,。

なのかなと考えました。(多分全部は網羅できていないが…)

  • 助動詞(ある)+記号 → ありますぞえ+記号
  • 助動詞(ある以外)+記号 → 助動詞+ぞえ+記号
  • 形容詞+記号 → 形容詞+ぞえ+記号
  • 動詞+記号 → 動詞+ぞえ+記号

多分、名詞が語尾にくるパターンも考えられるので

  • 名詞+記号 → 名詞+でありますぞえ+記号

また、句点をつけないで終わるような行儀の悪い文も考慮して

  • 任意の品詞+BOS/EOS → 任意の品詞+でありますぞえ。

さらに感動詞(「えーと」など)も考慮して

  • 感動詞+記号 → 任意の品詞+でありますぞえ。

も追加しておきます。

先ほどのnodeクラスは双方向リストのクラスになっているのでnextにアクセスすることで次の文節を取得できるようになっています。 これはnextで参照が可能です。

つまり、

取得したnodeの品詞と次の文節の品詞の関係性を取得するには

  • node.feature
  • node.next.feature

に含まれる、品詞を取得して比較をすることになります。先ほど言ったようにnode(この場合は変数n)は双方向リストなので

  • n.feature.split(',')[0]
  • n.next.feature.split(',')[0]

この2つを比較すればよいことになります。

ちなみにfeatureの格納の形式は以下のようになっています。

品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音


コーディング

ある程度これでロジックができましたので、語尾変換をクラスとしてコーディングしてみます。

サンプル:zoe.py

#!/usr/bin/env python
# coding: utf-8

import MeCab
import sys

converter = {
        '東京' : 'とうきょう'
}

class zoeTranslator:
        def __init__(self):
                self.mecab = MeCab.Tagger("-Ochasen")

        def _get_ending(self, n):
                if n.next:
                        f = n.feature.split(',')
                        f_next = n.next.feature.split(',')

                        # print f[0] + ':'+ n.surface
                        # print f_next[0] + ':' + n.next.surface

                        if n.next.surface == '、':
                                return None
                        if n.surface in ['だ']:
                                return 'ぞえ'
                        if f[0] in ['形容詞', '動詞', '名詞', '感動詞']:
                                if f_next[0] == 'BOS/EOS':
                                        return n.surface + 'でありますぞえ'
                                if f_next[0] == '記号':
                                        return n.surface + 'ぞえ'
                        elif f[0] in ['助動詞']:
                                if f_next[0] == 'BOS/EOS' or f_next[0] == '記号':
                                        if f[6] in ['ある']:
                                                return 'ありますぞえ'
                                        else:
                                                return n.surface + 'ぞえ'
                return None

        def translate(self, src):
                n = self.mecab.parseToNode(src)
                text = ''
                while n:
                        if n.surface in converter:
                                text += converter[n.surface]
                        else:
                                ending = self._get_ending(n)
                                if ending is not None:
                                        text += ending # + "@"
                                else:
                                        text += n.surface
                        n = n.next
                return text

語句の変換を行うことも考えて専用のコンバータを入れてあります。

このクラスを使うには以下のようにします。

サンプル:zoe_main.py

#!/usr/bin/env python
# coding: utf-8
from zoe import zoeTranslator

if __name__ == "__main__":
        t = zoeTranslator()
        print t.translate('我輩は猫である。名前はまだ無い。どこで生れたか頓と見當がつかぬ。何
でも薄暗いじめじめした所でニヤーニヤー泣いて居た事丈は記憶して居る。')

実行すると以下のように表示されます。

$ python zoe_main.py
我輩は猫でありますぞえ。名前はまだ無いぞえ。どこで生れたか頓と見當がつかぬぞえ。何でも薄暗い じめじめした所でニヤーニヤー泣いて居た事丈は記憶して居るぞえ。

なんとか動くようになりました。

最後に

あと少しで完成です。しかし、単なる語尾変換などと安易に考えていたのですがこんなに頭をつかうことになるとは思いませんでした。 こんなことなら、ただのテキスト変換にすればよかったと後悔しています。

しかし、日本語は奥深いのだなと思えたことも良かったですし、勉強になったような気がします。

Raspberry Pi で形態素解析をやってみる(前編)

Raspberry Pi形態素解析をやってみる(前編)

前回のエントリーで野望に一歩近づきました。

uepon.hatenadiary.com

しかし、完成はしていません。完成させなければ意味がない! 前回ではテキストデータを与えて音声として読み上げることはできているので、 あとはテキストから特定の語尾をみつけて、変換する処理が必要になります。

一般にいう「ですます調」の文章を「である調」に変換するようなものです。 ググってみればわかりますが、スギちゃん語コンバータやみさくら語コンバータなどもあるようです。

matome.naver.jp

これと同じ処理をすればいいのですが、手っ取り早くやるなら変換パターンを見つけてstringのReplace処理すればいいのかなとも思えます。

しかし、パターンが多くなると処理が泥臭くなるので、これは避けて通りたいところです。また、途中に同じかなを使用すると誤変換(置換のほうが正しい?)が多くなってしまうので、これを予防するのも面倒に感じます。

ということで、単純な処理ではなく形態素解析を使用して文章の品詞を解析し、その結果をつかって語尾変換をする方法をとることにしました。

形態素解析とは?

形態素解析Wikipediaさんの説明では

形態素解析(けいたいそかいせき、Morphological Analysis)とは、文法的な情報の注記の無い自然言語のテキストデータ(文)から、対象言語の文法や、辞書と呼ばれる単語の品詞等の情報にもとづき、形態素(Morpheme, おおまかにいえば、言語で意味を持つ最小単位)の列に分割し、それぞれの形態素の品詞等を判別する作業である。 自然言語処理の分野における主要なテーマのひとつであり、機械翻訳かな漢字変換など応用も多い(もちろん、かな漢字変換の場合は入力が通常の文と異なり全てひらがなであるため、その内容も機械翻訳の場合とは異なったものになる)。

形態素解析 - Wikipedia

ということらしいです。

噛み砕くと自分のようにせっかちな人間からすると、文章に出てくる言葉を品詞に分けたり、分かち書き、ひらがな化などができる処理といってもいいのかなと思います。

自分は古い人間なのでKAKASHI(KAKASI - Kanji Kana Simple Inverter)ぐらいしか知らなかった…

英語などと違って語句間にスペースが含まれない日本語ではこの解析はかなり大変に思えますが、最近は高速に行える形態素解析エンジンもフリーで存在しています。

その中に

MeCab(和布蕪)があります。

MeCab: Yet Another Part-of-Speech and Morphological Analyzer

以降はこのMeCabを使用してテキストの形態素解析を行ってみます。

MeCabのインストール

RaspberryPiでは以下のようにコマンドを打つと無事にインストールが完了します。

~ $ sudo apt-get install mecab libmecab-dev mecab-ipadic-utf8 python-mecab

公式ページにはtarの展開+configure+make+make installの記載がありますが、apt-getでうまくインストールできました。(Windowsには辞書データ付きのバイナリがあるようです。)

ちなみにライセンスは

MeCabフリーソフトウェアです.GPL(the GNU General Public License), LGPL(Lesser GNU General Public License), または BSD ライセンスに従って本ソフトウェアを使用,再配布することができます。

とのことです。

MeCabを使ってみる

せっかくインストールできたのでpythonからMeCabを使用してみます。 import MeCabでエラーがなければ問題ありません。

~ $ python
Python 2.7.9 (default, Mar  8 2015, 00:52:26)
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import MeCab
>>> import sys
>>> m = MeCab.Tagger("-Ochasen")

>>> print m.parse("今日はとても良い天気ですね。")
今日    キョウ  今日    名詞-副詞可能
は      ハ      は      助詞-係助詞
とても  トテモ  とても  副詞-助詞類接続
良い    ヨイ    良い    形容詞-自立     形容詞・アウオ段        基本形
天気    テンキ  天気    名詞-一般
です    デス    です    助動詞  特殊・デス      基本形
ね      ネ      ね      助詞-終助詞
。      。      。      記号-句点
EOS

>>> print m.parse("今日はとても良い天気ですぞえ")
今日    キョウ  今日    名詞-副詞可能
は      ハ      は      助詞-係助詞
とても  トテモ  とても  副詞-助詞類接続
良い    ヨイ    良い    形容詞-自立     形容詞・アウオ段        基本形
天気    テンキ  天気    名詞-一般
です    デス    です    助動詞  特殊・デス      基本形
ぞ      ゾ      ぞ      助詞-終助詞
え      エ      え      フィラー
EOS

てな具合で解析をしてくれます。便利すぎんだろJK。 というか

GoogleソフトウェアエンジニアでGoogle日本語入力開発者の一人である工藤拓によって開発されている。(Wikipediaより)

すげー。ありがたい話だ。

途中にあるm = MeCab.Tagger("-Ochasen")は解析形式のオプションでChaSenという形態素解析エンジンの出力形式と互換があるものになります。

chasen-legacy.osdn.jp

他にも

があるようです。

ちなみに、pythonから使用しなくてもコマンドを打つことで連続して形態素解析をやってくれるのでそれだけでも楽しいです。

~ $ mecab
すもももももももものうち
すもも  名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
も      助詞,係助詞,*,*,*,*,も,モ,モ
もも    名詞,一般,*,*,*,*,もも,モモ,モモ
の      助詞,連体化,*,*,*,*,の,ノ,ノ
うち    名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

この先生きのこる
この    連体詞,*,*,*,*,*,この,コノ,コノ
先      名詞,一般,*,*,*,*,先,サキ,サキ
生き    動詞,自立,*,*,一段,連用形,生きる,イキ,イキ
のこる  動詞,自立,*,*,五段・ラ行,基本形,のこる,ノコル,ノコル
EOS

ホント素晴らしい!

語尾を変換する

といきたいのですが、長くなりそうなので後編に続きます。

/* -----codeの行番号----- */