Thinking Skeever

Skyrim/The Witcher 3 Modについてのあれこれ。FoModの作り方、Mod導入時のトラブル事例などのニッチな話を書いていきます。a.k.a. BowmoreLover@nexusmods

.NET C#でMigemoってみる(その1)

Migemoってご存知でしょうか。ローマ字入力で漢字を検索しようというライブラリで、EmacsVim、SAKURAといったテキストエディタでよく使われています。
自作のテキスト検索系ツールにこの機能を組み込んでみようと思い、.NET C#Migemoが使えないかどうか試してみました。
結論から言うと上手く行きました。少し引っかかるところもあったので、忘備録を兼ねてまとめておきます。
※自作のツールへの組み込み作業で新しいことが分かったら随時追記します。

C/Migemoの入手

今回はVimでお世話になっているKaoriyaさんのC/Migemoを使うことにしました。
C言語による実装で、Vim用のアドオンと共に、単独で利用できるDLLも用意されています。

バイナリのダウンロード: C/Migemo - Kaoriya
ソースコードhttps://github.com/koron/cmigemo

テスト用プログラムの作成

今回はMicrosoft Visual Studio Community 2015のC#を使いました。
作成手順は次の通りです。

(1)プロジェクトの作成

Visual Studio C#を起動して、新規プロジェクト(Windowsフォームアプリケーション)を作成する。

(2)ライブラリと辞書の追加

プロジェクトフォルダに次のファイルをコピーする。

コピー元 コピーするファイル
cmigemo-default-win32-20110227.zip migemo.dll
cmigemo-default-win32-20110227.zip dictフォルダ
ソースコード(GitHubからダウンロード) tools/Migemo.cs

ソリューションエクスプローラーに次のファイルを追加する。

migemo.dll
migemo.cs
dict/cp932フォルダと中のファイル全部(フォルダごと)

ソリューションエクスプローラーでdict/cp932下のファイルとmigemo.dllを選択し、[出力ディレクトリにコピー]を"新しい場合はコピーする"に変更する。
f:id:thinkingskeever:20180311001331p:plain

(3)フォームの作成

Form1.csのデザインを開いてTextBox、Button、RichTextBoxを配置した後、Form1のLoadイベントとbutton1のClickイベントを追加する。
f:id:thinkingskeever:20180311001344p:plain

Form1.csのコードを開き、次のコードを追加する。

using System.Text.RegularExpressions;
using KaoriYa.Migemo;

//  (中略)

        // Migemoオブジェクト
        private Migemo m_migemo;

        private void Form1_Load(object sender, EventArgs e)
        {
            // Migemoオブジェクトを作る
            m_migemo = new Migemo("./dict/cp932/migemo-dict");

            // 行またがり検索をするときはこれを設定する
            m_migemo.OperatorNewLine = @"\s*";
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 反転表示のクリア
            richTextBox1.SelectAll();
            richTextBox1.SelectionBackColor = richTextBox1.BackColor;

            // 正規表現オブジェクトの生成
            Regex regex = m_migemo.GetRegex(textBox1.Text);
            Debug.WriteLine(regex.ToString());  //生成された正規表現をデバッグコンソールに出力

            // テキストの検索と反転表示
            MatchCollection matches = regex.Matches(richTextBox1.Text, 0);

            foreach (Match match in matches)
            {
                richTextBox1.Select(match.Index, 0);
                richTextBox1.SelectionLength = match.Length;
                richTextBox1.SelectionBackColor = Color.Yellow;
            }
            richTextBox1.Select(0, 0);
        }

Migemo.csのコードを開き、DllImportの引数CallingConvention.CdeclをCallingConvention.StdCallに変更する。こうしないとMigemoAPI呼び出しでPInvokeStackImbalance例外が発生する。
migemo.dllのAPIエントリは__stdcallで定義されているのでStdCallが正しいと思うのですが、私の試した範囲では、組み込み先のプロジェクトによっては変更しなくても動作したりします。何故だろう… パスの通った別フォルダにすごく古いバージョン(2003年!)のmigemo.dllがあるためでした。最新版のmigemo.dllでは必ずこの修正が必要です。

    [DllImport("migemo.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
    private static extern IntPtr migemo_open(string dict);
    [DllImport("migemo.dll", CallingConvention = CallingConvention.StdCall)]
    private static extern void migemo_close(IntPtr obj);
    [DllImport("migemo.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
    private static extern IntPtr migemo_query(IntPtr obj, string query);
    [DllImport("migemo.dll", CallingConvention = CallingConvention.StdCall)]
    private static extern void migemo_release(IntPtr obj, IntPtr result);

    [DllImport("migemo.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
    private static extern int migemo_set_operator(IntPtr obj,
        OperatorIndex index, string op);
    [DllImport("migemo.dll", CallingConvention = CallingConvention.StdCall)]
    private static extern IntPtr migemo_get_operator(IntPtr obj,
        OperatorIndex index);

    [DllImport("migemo.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
    private static extern DictionaryId migemo_load(IntPtr obj,
        DictionaryId id, string file);
    [DllImport("migemo.dll", CallingConvention = CallingConvention.StdCall)]
    private static extern int migemo_is_enable(IntPtr obj);

これで完成です。ビルドすればEXEが作成されるはず。

テストしてみる

テキストボックスにローマ字を入力して検索ボタンを押します。
 
まずは単語検索。
f:id:thinkingskeever:20180311001402p:plain

今度は少し長い文章を。語の切れ目を大文字にしてあげる必要があります。
f:id:thinkingskeever:20180311001406p:plain

改行またがり検索。
f:id:thinkingskeever:20180311001411p:plain
 
なかなか良い感じです。

その他気づいた点

辞書の読み込みエラーの検出

Migemoオブジェクトのコンストラクタ引数で辞書を指定する場合、読み込めなくてもエラーは発生しません。
C/MigemoのREADMEには、コンストラクタの引数は省略しておき、別途Migemo.LoadDictionaryメソッドで辞書をロードするとよいとありました。
 
そこで以下のコードを試してみましたが、regexには正しく正規表現展開されていない "sakura" または "s\s*a\s*k\s*u\s*r\s*a" が戻ってくるだけでした。

    Migemo migemo = new Migemo();
    migemo.LoadDictionary(Migemo.DictionaryId.Migemo, "./dict/cp932/migemo-dict");
    Regex regex = migemo.GetRegex("sakura");

試しに他の辞書も全て読み込んでみました。Migemo.csのenum DictionaryIdにはmigemo.hのMIGEMO_DICTID_ZEN2HANに相当する項目が定義されていませんので、ZenToHan = 5 を追加し、以下のコードを試してみましたところ、正しい正規表現が得られました。

    Migemo migemo = new Migemo();
    if (!migemo.LoadDictionary(Migemo.DictionaryId.Migemo, "./dict/cp932/migemo-dict"))
        return -1;
    if (!migemo.LoadDictionary(Migemo.DictionaryId.HanToZen, "./dict/cp932/han2zen.dat"))
        return -1;
    if (!migemo.LoadDictionary(Migemo.DictionaryId.HiraToKata, "./dict/cp932/hira2kata.dat"))
        return -1;
    if (!migemo.LoadDictionary(Migemo.DictionaryId.RomaToHira, "./dict/cp932/roma2hira.dat"))
        return -1;
    if (!migemo.LoadDictionary(Migemo.DictionaryId.ZenToHan, "./dict/cp932/zen2han.dat"))
        return -1;
    Regex regex = migemo.GetRegex("sakura");

コンストラクタでmigemoのを指定すれば関連する辞書すべて読み込まれるようなので、コンストラクタの直後にIsEnableで判定する手もあります。

    Migemo migemo = new Migemo();
    migemo.LoadDictionary(Migemo.DictionaryId.Migemo, "./dict/cp932/migemo-dict");
    if (!migemo.IsEnable())
        return -1;
    Regex regex = migemo.GetRegex("sakura");

 

利用する辞書のコードセットについて

.NETの内部コードはUnicode(UTF8?)なのでutf-8の辞書を使ってしまいそうですが、utf-8の辞書では正規表現が文字化けしてしまうようです。cp932(shift-jis)の辞書を使います。
 

改行またがりの検索をするには

デフォルトでは改行またがりの検索が無効となっています。
Migemo.OperatorNewLineプロパティに@"\s*"を指定することで、改行またがり検索が有効になります。
 

GitHubから入手したソースをVisual Studioでリビルドする

こういう作業には慣れていないのですが、恐る恐るcmigemo-master\compile\vs2003\CMigemo.slnを開いてソリューションを変換してビルドすればDLLが作成されました。セキュリティ系の警告(_s関数を使え)が沢山出ましたが、上記サンプルの範囲ではうまく動きました。
 

ライセンスについて

C/Migemo本体はMITライセンスと独自ライセンスのデュアルライセンスとなっていて、どちらも商用利用も可能な自由度のあるライセンスのようです。
ですがC/MigemoのREADMEにもあるように、辞書はGPLライセンスSKK辞書を使って作られています。
商用ソフトなど、非GPLのソフトウェアで利用する場合には注意が必要です。
※別途KaoriyaさんのC/Migemoをダウンロードして辞書ファイルをコピーすることを前提とした商用ソフトがいくつか存在するようですが、こういった方法許容されるのかどうか自信がありません。

改行またがり検索の問題について

改行位置によっては改行またがり検索がうまくヒットしない場合がありました。
 
こういうテキストがあったとします。
f:id:thinkingskeever:20180311001622p:plain
 
単語の間で改行されている場合はうまくヒットします。
f:id:thinkingskeever:20180311001630p:plain

単語の切れ目で改行されているとうまくヒットしません。
f:id:thinkingskeever:20180311001634p:plain

C/Migemoで生成された正規表現は次のとおりでした。

(?:シ\s*ュ\s*ト\s*ク|シ\s*ュ\s*ト\s*ク|取\s*得|し\s*ゅ\s*と\s*く|s\s*y\s*u\s*t\s*o\s*k\s*u|s\s*y\s*u\s*t\s*o\s*k\s*u)(?:[鋭鯣駿]|ス\s*ル|ス\s*ル|す\s*る|S\s*u\s*r\s*u|S\s*u\s*r\s*u)

 
要約すると、 "(?:取得)(?:する)" のように2つのグループが定義されていて、グループ間に0個以上の空白または改行を示す "\s*" が生成されていません。
試しに正規表現を "(?:取得)\s*(?:する)" のように訂正するとヒットしました。
これがC/Migemoの問題なのか仕様なのか、今のところ分かりません。
 
 

最後に

ちょっとびっくりするような正規表現が生成されますが、今のマシンパワーですと全然気にならないですね。幸い、自分のツールでは文章の途中に改行の入らないテキストを扱うので、行またがり検索の問題の影響はありません。早速自作ツールに組み込もうと思います。

追記:組み込みました。おかげさまで自作のしょぼいツールが少しだけパワーアップ。ありがたや。
f:id:thinkingskeever:20180311172024p:plain

最後に、素晴らしいライブラリを公開してくださったC/Migemoの作者の村岡さんに感謝します。

以上


改訂履歴
2018/03/11 00:30 - 初回公開
2018/03/11 13:00 - DllImportの引数変更に関する追記
2018/03/11 17:00 - 辞書の読み込みエラーの検出に関する追記、DllImportの引数変更に関する追記変更、組み込み結果を追加

Copyright (C) 2015-2020 ThinkingSkeever, All Rights Reserved.
ブログの記事内に記載されているメーカー名、製品名称等は、日本及びその他の国における各企業の商標または登録商標です。
リンクはご自由に。記事の転載はご遠慮ください。記事を引用する場合はトラックバックするか元のURLを明記してください。