NMeCabを使ったパーフェクトVSTO湯婆婆

#はじめに
　映画「千と千尋の神隠し」の登場人物である湯婆婆と、主人公の千尋とのやりとりを実装したものが、@Nemesisさんの[Javaで湯婆婆を実装してみる](https://qiita.com/Nemesis/items/c7192a7c510788d2cba2)の記事です。これに触発されて、[WORD VBAで湯婆婆](https://qiita.com/kenichiro_ayaki/items/b2796aba5dd99bd140a0)や、[Autohotkeyでトースト湯婆婆](https://qiita.com/kenichiro_ayaki/items/efbf33fed123e106c2be)の記事を投稿いたしました。
　そして、[「NameDividerを触媒にパーフェクト湯婆婆を創造する」](https://qiita.com/cold-wisteria/items/cab1548d71bec5d8ec50)の記事を見て思いました。まだ自分のコーディングしたものはパーフェクトではなかった、と・・・。なお、パーフェクト湯婆婆の動作仕様は以下です。

>千尋は契約書にフルネームを書き、湯婆婆は下の名前だけを口にしたうえで、下の名前から1文字を残し、名前の他の部分を奪った、のです。

　しかし、パーフェクト湯婆婆の作成には、日本語の形態素解析エンジンを組み込むことが必要であり、AutohotkeyやVBAでは対応できません。
　そこで作成したのが、NMeCabを使ったパーフェクトVSTO湯婆婆です。なお、この記事は、VSTO湯婆婆の紹介と共に、単一バイナリのNMeCabのdllの紹介も兼ねております。
　パーフェクトVSTO湯婆婆は、GitHubにソースコードとインストーラ込みで公開しています。
[https://github.com/k-ayaki/yubaba](https://github.com/k-ayaki/yubaba)

#MeCabとは
　[MeCab](https://taku910.github.io/mecab/)とは、MeCabは 京都大学情報学研究科−日本電信電話株式会社コミュニケーション科学基礎研究所 共同研究ユニットプロジェクトを通じて開発されたオープンソース 形態素解析エンジンです。開発者は工藤卓氏であり、C++で記述されています。

#NMeCabとは、
　[NMeCab](https://github.com/komutan/NMeCab)とは、日本語形態素解析エンジンであるMeCabをC#に移植したものです。MeCabの様々な移植のうち .Net Framework環境ではもっとも安定して動作します。

　余談ですが、筆者はOSDNにアップロードされているNMeCabをベースに作業していました。Githubに新しい版がアップロードされてることに今更ながら気づいた次第です。

#NMeCab単一バイナリ版とは
　NMeCab単一バイナリ版とは、筆者がNMeCabをdll化して、かつ、辞書をリソースとして含んだものです。これにより、インストール時や動作時における辞書パスの設定に悩まされずにすみます。今回は、このNMeCabの単一バイナリ版を用いて、パーフェクト湯婆婆を作成します。なお、単一バイナリのNMeCabのDLLは、以下に公開しています。
　[https://github.com/k-ayaki/NMeCabSb](https://github.com/k-ayaki/NMeCabSb)

#VSTOとは
　VSTO とは、Visual Studio Tools for Officeの略称であります。簡単にいうと、VBAと同様なWORD（Office）アドインを作成するためのフレームワークです。Visual Studioにソリューションのスケルトンが実装されています。VSTOを使うと、Microsoft Word上のテキストを直接に操作するアドインをC#で作成可能です。VBAの独特のクセが気になる方にはお勧めです。

　詳しくは、[Office ソリューションの開発の概要 (VSTO)](https://docs.microsoft.com/ja-jp/visualstudio/vsto/office-solutions-development-overview-vsto?view=vs-2019)や、[VSTOって何よってお話](http://artfulplace.hatenablog.com/entry/2015/04/15/192337)をご覧ください。なお、日本語の書籍としては、１０年以上もまえに出版された[VSTOとSharePoint Server 2007による開発技術~Visual Studio 2008で構築するOBAソリューション](https://www.amazon.co.jp/VSTO%E3%81%A8SharePoint-2007%E3%81%AB%E3%82%88%E3%82%8B%E9%96%8B%E7%99%BA%E6%8A%80%E8%A1%93-Visual-2008%E3%81%A7%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8BOBA%E3%82%BD%E3%83%AA%E3%83%A5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3-Programmer%E2%80%99s-SELECTION/dp/4798118419/ref=sr_1_3?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=vsto&qid=1607752488&sr=8-3)があるだけのようです。VSTOについては、VBAと比べて技術情報が少なく、やや苦労します。

#VSTOのプロジェクトの設定
Visual Studio 2019にて「新しいプロジェクトの作成」を選択して、Word VSTO アドイン(C#)を選択してください。
![yubaba00.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/591d5cf7-b4bb-424c-4987-703ba4ee2666.png)

そして、プロジェクトの作成先フォルダを選択します。これにより、ブロジェクトの雛形が作成されます。
![yubaba01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/54b009d5-a863-5e97-d764-d31ea452929f.png)

次に、yubabaソリューション上で右クリックし、「新しい項目」を選択します。ここで、リボン（ビジュアルなデザイナー）を選択してください。
![yubaba02.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/8a00fd3d-01e1-7986-3fc5-d2310733a84d.png)

これにより、yubabaRibbon.csが追加されます。
![yubaba03.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/75c07284-b2bf-7491-46b6-c767aa82ecd6.png)

ツールボックスにより、yubabaRibbon.cs にボタンを追加します。
![yubaba04.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/d86a1c96-551e-b9ff-545b-e212f5e874e4.png)

そして、湯婆婆ボタンにアイコンを追加します。

![yubaba64.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/7ff99891-854b-c12f-8ec8-389cd6f883b1.png)

　湯婆婆ボタンのColtrolSizeは、RibbonControlSizeLargeを選択してください。湯婆婆の顔をはっきりと見せるためです。そして、グループとタブのラベルを適宜設定してください。ここではグループのラベルに "Qiita"を設定し、タブのラベルに "AppLint" を設定しています。
![yubaba08.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/66d81836-0761-f86c-cdd2-f819ad0fb4c4.png)

そして、契約書フォームを追加します。フォームについては、普通のC#アプリケーションと同様です。
![yubaba09.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/062c9508-54b2-576f-8e73-3519b2a11694.png)

#NMeCabの組み込み
yubabaプロジェクトの「参照」を選択して右クリックし、コンテキストメニューの「参照の追加」を選択してください。そして、LibNMeCab.dll （単一バイナリのNMeCabのDLL）を選択してください。

![yubaba10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/40e8506d-02ec-e4b9-8a1e-31a94b450928.png)

#動作説明
　WordのAppLintタブに、湯婆婆アイコンが表示されています。
![yubaba_word11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/33bed272-44e9-4509-c890-97433346de65.png)

　湯婆婆アイコンをクリックすると、「契約書だよ。そこ（乙）に名前を書きな。」をWord画面に追加して、以下の契約書フォームを表示します。乙に人名（ここでは荻原千尋）を入力してOKボタンをクリックします。
![yubaba_word12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/e6a926b6-a39f-d885-899c-2711b2efff22.png)

湯婆婆は下の名前だけを口にして、この下の名前から1文字を残して名前の他の部分を奪います。

　![yubaba_word13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/1a9f8f23-e4cb-9415-0e71-7be6b81b6877.png)

#ソースコード説明

　ワード（アプリケーション）は、Globals.ThisAddIn.Applicationオブジェクトです。このうち、Selection（選択範囲）オブジェクトを操作することで、ワード画面に段落や文字を入力できます。なお、Selectionオブジェクトとは、現在の選択範囲に係るオブジェクトのことです。
　app.Selection.TypeParagraph(); は、ワードのパラグラフの入力です。
　app.Selection.TypeText(); は、ワードへのテキスト入力です。ここでは、「契約書だよ。そこ（乙）に名前を書きな。」を画面に入力したのち、契約書フォームを呼び出して名前を入力させています。
　cf.yourName は、ユーザが入力した名前です。
　入力した名前をYNameクラスに設定し、人名の下の名前 (yn.firstName) が取得できたならば、１文字だけ残した新たな名前 (yn.newName) を表示しています。
　なお、形態素解析(NMeCab)によって、下の名前が検知できなかった場合には、「フン、" + yn.strangeName + "というのかい、おかしな名だねえ」と表示させています。このとき、yn.strangeNameには、入力した全ての文字列が格納されます。

```C#:yubabaRibbon.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Office.Tools.Ribbon;
using System.Windows.Forms;
using yubaba.Name;
using Microsoft.Office.Interop.Word;

namespace yubaba
{
    public partial class yubabaRibbon
    {
        private void yubabaRibbon_Load(object sender, RibbonUIEventArgs e)
        {

        }

        private void button1_Click(object sender, RibbonControlEventArgs e)
        {
            Microsoft.Office.Interop.Word.Application app = Globals.ThisAddIn.Application;
            app.Selection.TypeParagraph();
            app.Selection.TypeText("湯婆婆「契約書だよ。そこ（乙）に名前を書きな。」");
            app.Selection.TypeParagraph();

            contractForm cf = new contractForm();
            cf.ShowDialog();
            YName yn = new YName(cf.yourName);

            if(yn.fStrange == false)
            {
                app.Selection.TypeText("湯婆婆「フン、" + yn.firstName + "というのかい、贅沢な名だねえ」");
                app.Selection.TypeParagraph();
                app.Selection.TypeText("湯婆婆「今日からお前の名前は" + yn.newName + "だ、いいかい、" + yn.newName + "だよ。分かったら返事をするんだ、" + yn.newName + "」");
            }
            else
            {
                app.Selection.TypeText("湯婆婆「フン、" + yn.strangeName + "というのかい、おかしな名だねえ」");
                app.Selection.TypeParagraph();
                app.Selection.TypeText("湯婆婆「今日からお前の名前は" + yn.newName + "だ、いいかい、" + yn.newName + "だよ。分かったら返事をするんだ、" + yn.newName + "」");
            }
        }
    }
}
```

 契約書フォームは、OKボタンをクリックしたときに当該フォームを閉じることと、textBox1に入力されたテキストをyourNameとして返すことが記載されています。

```CSharp:contractForm.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace yubaba
{
    public partial class contractForm : Form
    {
        public contractForm()
        {
            InitializeComponent();
        }
        public string yourName
        {
            get
            {
                return textBox1.Text;
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.OK;
            this.Close();
        }
    }
}
```
YNameクラスは、NMeCabを呼び出して入力文字列 inString を形態素解析し、名詞・固有名詞・人名・名のチャンクがあれば下の名前として、その中から１文字を取り出して新しい名としています。名詞・固有名詞・人名・名のチャンクがなければ、inString を strangeName に代入して、その中から１文字を取り出して新しい名としています。

```Csharp:YName.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NMeCab;

namespace yubaba.Name
{
    class YName
    {
        public string firstName { get; }
        public string strangeName { get; }
        public string newName { get; }
        public bool fStrange { get; }
        public YName(string inString)
        {
            this.firstName = "";
            this.strangeName = "";
            this.newName = "";
            this.fStrange = true;
            var mecab = MeCabTagger.Create();
            MeCabNode node = mecab.ParseToNode(inString);
            node = node.Next;
            while (node != null)
            {
                if (node.Feature != "BOS/EOS,*,*,*,*,*,*,*,*")
                {
                    Chunk chunk = new Chunk(node);
                    if (chunk.isPartOfSpeech("名詞","固有名詞","人名","名"))
                    {
                        this.firstName = node.Surface;
                        fStrange = false;
                    }
                }
                node = node.Next;
            }
            mecab.Dispose();

            var random = new Random();
            if (this.firstName.Length > 0)
            {
                newName = firstName.Substring((int)(random.Next(0, firstName.Length)), 1);
            } else
            {
                strangeName = inString;
                newName = strangeName.Substring((int)(random.Next(0, strangeName.Length)), 1);
            }
        }
    }
}
```
Chunkクラスは、形態素解析後の各ノードを記憶するものです。
なお、NMeCabが、「荻原千尋」を形態素解析すると以下となります。

Surface="荻原", Feature="名詞,固有名詞,人名,姓,*,*,荻原,オギワラ,オギワラ"
Surface="千尋", Feature="名詞,固有名詞,人名,名,*,*,千尋,チヒロ,チヒロ"

Surface を表層形のメンバー変数に、Feature をカンマで区切って各メンバー変数に設定しています。これにより、C#でアクセスしやすくなります。

```Csharp:Chunk.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NMeCab;

namespace yubaba.Name
{
    class Chunk
    {
        public string 表層形 { get; set; }
        public string 品詞 { get; set; }
        public string 品詞細分類1 { get; set; }
        public string 品詞細分類2 { get; set; }
        public string 品詞細分類3 { get; set; }
        public string 活用形 { get; set; }
        public string 活用型 { get; set; }
        public string 原形 { get; set; }
        public string 読み { get; set; }
        public string 発音 { get; set; }
        public string padding { get; set; }
        public string feature { get; set; }
        public MeCabNodeStat stat { get; set; }
        public Chunk(MeCabNode node)
        {
            表層形 = node.Surface;
            stat = node.Stat;

            feature = node.Feature;
            string[] features = node.Feature.Split(',');
            品詞 = "未定義";
            品詞細分類1 = "";
            品詞細分類2 = "";
            品詞細分類3 = "";
            活用形 = "";
            活用型 = "";
            原形 = "";
            読み = "";
            発音 = "";
            if (1 <= features.Length) 品詞 = features[0];
            if (2 <= features.Length) 品詞細分類1 = features[1];
            if (3 <= features.Length) 品詞細分類2 = features[2];
            if (4 <= features.Length) 品詞細分類3 = features[3];
            if (5 <= features.Length) 活用形 = features[4];
            if (6 <= features.Length) 活用型 = features[5];
            if (7 <= features.Length) 原形 = features[6];
            if (8 <= features.Length) 読み = features[7];
            if (9 <= features.Length) 発音 = features[8];
        }
        // 品詞とチャンクとの照合
        public bool isPartOfSpeech(string szPartOfSpeech, string szPartOfSpeechSC1 = null, string szPartOfSpeechSC2 = null, string szPartOfSpeechSC3 = null)
        {
            if (品詞 != szPartOfSpeech)
            {
                return false;
            }
            if (szPartOfSpeechSC1 == null)
            {
                return true;
            }
            if (品詞細分類1 != szPartOfSpeechSC1)
            {
                return false;
            }
            if (szPartOfSpeechSC2 == null)
            {
                return true;
            }
            if (品詞細分類2 != szPartOfSpeechSC2)
            {
                return false;
            }
            if (szPartOfSpeechSC3 == null)
            {
                return true;
            }
            if (品詞細分類3 != szPartOfSpeechSC3)
            {
                return false;
            }
            return true;
        }
    }
}
```
#インストーラ

　VSTOで最もありがたいのは、インストーラ作成がVisual Studio 2019上で簡単にできることだとおもいます。

　最初にビルドメニューから、「yubabaの発行」をクリックすると、以下の公開ウィザードが表示されますので、ディスクパスを入力します。なお、共有サーバやFTPサーバに発行することも可能なようですが、試していません。
![yubabaSetup01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/e3ea8e21-a24f-59d9-1673-72980f9672a7.png)

そして、規定のインストールパスをCD-ROMまたはDVD-ROMとします。

![yubabaSetup02.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/c8fcded2-512c-19a8-a52c-4cd2499eae13.png)

これによりSETUP.EXE が作成されました。

![yubabaSetup03.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/9f47dbad-1e2f-9355-9d6b-e17247e2225d.png)

SETUP.EXEの実行により、湯婆婆がインストールされます。

![yubabaSetup04.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/180e7d2b-92a9-165f-d88e-4e96fc0f020a.png)


#余談
　現総理の「菅義偉」を入力すると、下の名前は「義」と認識されます。前総理の「安倍晋三」を入力すると、下の名前は「晋」と認識されます。NMeCabの固有名詞辞書にない名前はやや難しいようです。
![yubaba_word14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/b0339ca6-621f-eb12-f849-5b29f3a0462f.png)

![yubaba_word15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266273/1b92cde0-a1d5-a9ed-7dfd-2da5b5df635d.png)