Xamarin.Forms + C# でAndroidアプリに形態素解析エンジンMeCabを導入する

N.Mです。

Xamarin.FormsとC#で作っているAndroidアプリに形態素解析エンジンMeCabを導入できるか試しました。少し調整が必要であるものの使用できることが分かったので、記事にしました。

# 形態素解析エンジンMeCab
MeCabは日本語の文章を名詞や動詞などの品詞に分解する、形態素解析という処理を行うオープンソースエンジンです。
[MeCab公式サイト](https://taku910.github.io/mecab/)

元のソースコードはC++ですが、Pythonなど別言語のラッパーライブラリがあり、C#も.Net用のライブラリがあります。
[MeCab.DotNet](https://github.com/kekyo/MeCab.DotNet)

nuget経由でダウンロードでき、[この記事](https://yotiky.hatenablog.com/entry/csharp_morphologicalanalysis)でもMeCabをC#で使用したWPFアプリケーションを作成しているようです。今回Xamarin.Formsでも、このライブラリを使用してMeCabを導入できるのかという試みになります。

# 導入を試みたきっかけ
Xamarin.Forms + C#ではC#のライブラリであるCoreTweetからTwitterのAPIを叩いて、ツイートを取得できます。
[Xamarin.Formsで作っているAndroid用Twitterアプリの雛型](https://github.com/HexagramNM/TLExtension)

今回のMeCabと組み合せれば、取得してきたツイート内の名詞などから、現在のトレンドを探るなんてこともできそうかなと思い、挑戦することにしました。

# 必要だった調整
MeCabといった形態素解析エンジンでは辞書ファイルを通常使用します。MeCab.DotNetにはデフォルトの辞書ファイルも入っているのですが、Androidの場合、普通にビルドしてもその辞書ファイルがアプリケーションに含まれませんでした。

Visual Studio側では"@(Content) build action is not supported"という警告が出ていました。しかし、辞書ファイルはMeCab.DotNetのnugetパッケージ内にあり、ビルドアクションの設定を変更することはできません。

そのため、Androidに辞書ファイルを入れ込むように調整する必要があります。

## 環境
開発環境にVisual Studio 2017を使用しており、Xamarin.Formsはバージョン3.0.0.561731を使用しております。（少しバージョンは古いかもしれません。）

## 調整方法
###手順1. 辞書ファイルをAndroid側の`Assets`フォルダに追加

MeCab.DotNetをnuget経由でダウンロードした場合、辞書ファイルは`C:\Users\(user_name)\.nuget\packages\mecab.dotnet\0.0.40\content\dic`のフォルダに含まれています。Visual StudioでAndroid用プロジェクトを作成すると、Androidプロジェクト内に`Assets`フォルダができます。この`Assets`フォルダの中にVisual Studioで`mecabDict`フォルダを作り、その中にMeCab.DotNetの辞書ファイル全てをコピーします。Visual Studioの「既存の項目を追加」で追加したファイルをプロジェクトに追加し、認識させます。

以下の画像のようになれば大丈夫です。
![projectStructure.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152791/dee34e7c-7dad-2981-9bcc-90ef03c36847.png)


###手順2. Android側での辞書ファイルのコピー

Androidプロジェクト内の`Assets`フォルダ内のファイルはアプリケーションに組み込まれ、`AssetManager`を経由してファイルストリーム等を取得することはできます。しかし、ファイルがアプリケーションに組み込まれている状態であり、パスというものが存在しません。一方、MeCab側では辞書ファイルのあるフォルダのパスを指定する必要があります。

そのため、`Assets`内にある辞書ファイルを、アプリケーション外にコピーする必要があります。コピー先の場所としては`System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) + "/mecabDic"`を指定しています。

最初、コピーの処理を`System.IO`の`StreamReader`/`StreamWriter`でやっていたのですが、バイナリ形式のデータコピーが上手くできていないのか、「ファイルサイズが不正である」というようなエラーがMeCab側で発生しました。そのため、`BinaryReader`/`BinaryWriter`を使用して、バイナリコピーをしています。`BinaryReader`には`StreamReader`にあった`ReadToEnd`メソッドがないため、ファイルの最後までバイナリを読み込み、コピー先に書き込む処理は自前で書く必要があります。

###手順3. MeCabへの辞書ファイルパス設定
辞書ファイルをコピーしてきたフォルダのパスをMeCabに設定する必要がありますが、これは`MeCabParam`クラスの`DicDir`プロパティにパスを設定すればOKです。

ソースコードとしては以下のような感じになります。（`CustomizedContentPage`は`Xamarin.Forms.ContentPage`を継承した独自クラスです。`Xamarin.Forms.ContentPage`で問題ないです。）

```C#:TrendSearch.cs
using System;
using System.IO;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using Android.Content.Res;
using MeCab;

namespace TLExtension
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class TrendSearch : CustomizedContentPage
    {
        public TrendSearch()
        {
            InitializeComponent();

            //手順2: Assets内にあるmecabの辞書ファイルをAndroid側のフォルダ内にコピーする。
            AssetManager assets = Forms.Context.Assets;
            String mecabDictDir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) + "/mecabDic";
            if (!Directory.Exists(mecabDictDir)) {
                Directory.CreateDirectory(mecabDictDir);
                String[] dictFiles = assets.List("mecabDic");
                int chunkSize = 1024 * 32;
                foreach (String file in dictFiles)
                {
                    BinaryReader readFile = new BinaryReader(assets.Open("mecabDic/" + file));
                    BinaryWriter writeFile = new BinaryWriter(new FileStream(mecabDictDir + "/" + file, FileMode.Create));

                    while(true)
                    {
                        try
                        {
                            byte[] fileData = readFile.ReadBytes(chunkSize);
                            writeFile.Write(fileData);
                            if (fileData.Length < chunkSize)
                            {
                                break;
                            }
                        }
                        catch (IOException)
                        {
                            break;
                        }
                    }
                    writeFile.Close();
                    readFile.Close();
                }
            }
            //手順2ここまで

            //手順3: コピーしたmecab辞書が入っているフォルダのパスをmecabに通す。
            MeCabParam parameter = new MeCabParam();
            parameter.DicDir = mecabDictDir;
            MeCabTagger tagger = MeCabTagger.Create(parameter);
            //手順3ここまで

            //形態素解析の結果をLabelに出力
            String testStr = "Mecabを用いてAndroidアプリで形態素解析";
            String result = tagger.Parse(testStr);

            StackLayout layout = new StackLayout();
            layout.Orientation = StackOrientation.Vertical;
            Label updateDateLabel = new Label();
            updateDateLabel.Text = result;
            layout.Children.Add(updateDateLabel);
            Content = layout;
        }
    }
}
```

# 結果、まとめ
<img width=50% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152791/d1bde04c-976f-a35d-6958-1e373a77fa81.png">

上の画像のように、MeCabで形態素解析した結果をAndroidアプリケーション上でも表示することができました。`Assets`フォルダに辞書を配置したので、辞書を編集、更新した場合も簡単に反映できそうです。（辞書作成はMeCab側のexeファイルをたたく必要がありそうなので、Windowsでやる必要がありそうですが）