yotiky Tech Blog

とあるエンジニアの備忘録

Azure Functions で MeCab.DotNet を使って形態素解析

目次

概要

Azure Functions で形態素解析を行うため MeCab.DotNet を使用します。こちらのライブラリはローカルディレクトリから辞書ファイルを読み込むため、Azure Blob Storage にアップロードした各種ファイルを Azure Functions の TMP ディレクトリ配下にダウンロードして利用するようにします。 容量制限やライフサイクルなどは考慮が必要です。

qiita.com


WPFで動かす記事やユーザー辞書を追加する前回の記事など書いています。

インストール

Azure Functions のプロジェクトを作成したら、NuGet で「MeCab.DotNet」と「Azure.Storage.Blobs」をインストールします。

f:id:yotiky:20210126211348p:plain

MeCab.DotNet は「MeCab」、「NMeCab」を .NET Core に移植したパッケージです。

github.com

以下パッケージサイトから抜粋。

"MeCab" は、日本語形態素解析エンジンのプロジェクトです。

"NMeCab" は、上記MeCabを、.NET Framework 2.0のマネージライブラリとして実装し直したものです。ただ、もう更新されていないようです...

"MeCab.DotNet" (このプロジェクト)は、上記NMeCabを最新の.NET Core 1/2/3と.NET Frameworkで使えるように移植し、NuGetのパッケージに固めて使いやすくしたものです。

なお、ライセンスは GPL2 または LGPL2.1 とのことで利用に際しては注意が必要です。

サンプル実装

まずは Json を受けるためのパラメータクラスです。

public class TextAnarytics
{
    public bool DeleteCacheDics { get; set; }
    public bool ForceDownload { get; set; }
    public string[] UserDics { get; set; }
    public string Text { get; set; }
}

Text が解析対象の文字列です。 ユーザー辞書はパラメータで指定するようになっています。

一度ダウンロードした辞書を刷新するために TMP ディレクトリから全辞書を削除するフラグと、ユーザー辞書などを更新するために上書きをするフラグを用意してあります。


続いて、Function です。

[FunctionName("Function1")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    var data = JsonConvert.DeserializeObject<TextAnarytics>(requestBody);

    var deleteCacheDics = data.DeleteCacheDics;
    var forceDownload = data.ForceDownload;
    var userDics = data.UserDics;
    var text = data.Text;

    if (deleteCacheDics) { DeleteDir(); }

    await Task.WhenAll(
        Download("char.bin", forceDownload),
        Download("dicrc", forceDownload),
        Download("matrix.bin", forceDownload),
        Download("sys.dic", forceDownload),
        Download("unk.dic", forceDownload));

    if(userDics?.Length > 0)
    {
        var tasks = new List<Task>();
        foreach (var dic in userDics)
            tasks.Add(Download(dic, forceDownload));

        await Task.WhenAll(tasks);
    }

    var result = "";
    if (!string.IsNullOrEmpty(text))
    {
        var dicDir = Directory.CreateDirectory(Path.Combine(Environment.GetEnvironmentVariable("TMP"), "dic"));

        var param = new MeCabParam();
        param.DicDir = dicDir.FullName;
        param.UserDic = userDics ?? param.UserDic;
        var tagger = MeCabTagger.Create(param);

        foreach (var node in tagger.ParseToNodes(text))
        {
            if (0 < node.CharType)
            {
                result += $"{node.Surface}\t{node.Feature}\r\n";
            }
        }
    }


    string responseMessage = string.IsNullOrEmpty(result)
        ? "This HTTP triggered function executed faild."
        : result;

    return new OkObjectResult(responseMessage);
}


TMP ディレクトリから dic フォルダ毎削除するコードです。

private static void DeleteDir()
{
    Directory.Delete(Path.Combine(Environment.GetEnvironmentVariable("TMP"), "dic"), true);
}


最後に辞書をダウンロードして TMP ディレクトリに保存するコードです。

private static string connectionString = "YOUR CONNECTION STRING";
private static string blobContainerName = "dic";
private static async Task Download(string blobName, bool force = false)
{
    var dicDir = Directory.CreateDirectory(Path.Combine(Environment.GetEnvironmentVariable("TMP"), "dic"));
    var dicFile = Path.Combine(dicDir.FullName, blobName);

    if(!force && File.Exists(dicFile)) { return; }

    var blobServiceClient = new BlobServiceClient(connectionString);
    var blobContainerClient = blobServiceClient.GetBlobContainerClient(blobContainerName);
    var blobClient = blobContainerClient.GetBlobClient(blobName);

    using (var stream = new MemoryStream())
    {
        var response = await blobClient.DownloadToAsync(stream).ConfigureAwait(false);

        using (var file = new FileStream(dicFile, FileMode.Create, FileAccess.Write))
        {
            stream.Position = 0;
            await stream.CopyToAsync(file);
        }
    }
}

プロジェクト設定

ユーザー辞書を使用するためには csproj を開いて、PropertyGroup に以下の1行を追加します。

<MeCabUseDefaultDictionary>false</MeCabUseDefaultDictionary>

f:id:yotiky:20210126213352p:plain

Azure Blob Storage にアップロード

MeCab.DotNet に含まれている以下のファイルを Azure Blob Storage にアップロードします。

  • char.bin
  • dicrc
  • matrix.bin
  • sys.dic
  • unk.dic

加えて、前回の記事で作成したユーザー辞書もアップロードしておきます。

  • userdic1.dic
  • userdic2.dic

f:id:yotiky:20210126213610p:plain

実行

プロジェクトをサーバーにデプロイします。

ちなみにローカル実行した場合の TMP ディレクトリは C:\Users\<YOURNAME>\AppData\Local\Temp になります。

デプロイが終わったら、Postman などを使って 以下の Json を投げてみます。

{
    "DeleteCacheDics" : false,
    "ForceDownload" : false,
    "Text" : "はじめまして。 山田太郎、山田が苗字で、太郎が名前です。 日本の大阪出身です。 私は、1973年4月14日生まれです。山田花子は姉です。",
}

システム辞書のみなので、「山田太郎」が「山田」と「太郎」で分解されます。

f:id:yotiky:20210126214720p:plain


続いて、ユーザー辞書を指定します。

{
    "DeleteCacheDics" : false,
    "ForceDownload" : false,
    "Text" : "はじめまして。 山田太郎、山田が苗字で、太郎が名前です。 日本の大阪出身です。 私は、1973年4月14日生まれです。山田花子は姉です。",
    "UserDics" : ["userdic1.dic", "userdic2.dic"]
}

ユーザー辞書で「山田太郎」を名詞として登録したので、「山田太郎」で認識されています。

f:id:yotiky:20210126214655p:plain