yotiky Tech Blog

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

Azure Functions で Startup クラスを定義して DI を利用する

Azure Functions は、v2 で DI を正式にサポート。

Startup クラスを(自分で)定義し、 DI を設定することで Azure Functions にインジェクションすることができるようになる。

目次

検証環境

  • Azure Functions v3
  • Microsoft.Azure.Functions.Extensions v1.1.0
  • Microsoft.Extensions.Http v3.1.11
    • v5.0.0 だとMicrosoft .Extentions.DependencyInjection のライブラリがバージョン不一致を起こす
  • Azure.Extensions.AspNetCore.Configuration.Secrets v1.0.2
  • Microsoft.Extensions.Configuration.AzureKeyVault v3.1.11
  • Microsoft.Azure.Services.AppAuthentication v1.6.0
  • Microsoft.Azure.KeyVault v3.0.5
  • Microsoft.Extensions.Azure v1.0.0

インストール

Nuget で Microsoft.Azure.Functions.Extensions をインストールする。

使い方

基本的な使い方

今回は DI するために適当なクラスを用意する。

public interface IMyService { }
public class MyServiceA : IMyService { }
public class MyServiceB : IMyService { }
public class MyServiceC : IMyService { }

まず FunctionsStartup を継承した Statup クラスを新規作成する。

using Microsoft.Azure.Functions.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(FunctionApp1.Startup))]

namespace FunctionApp1
{
    class Startup : FunctionsStartup
    {
        public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
        {
            base.ConfigureAppConfiguration(builder);
        }
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddTransient<MyServiceA>();
            builder.Services.AddTransient<IMyService, MyServiceA>();

            builder.Services.AddScoped<MyServiceB>();
            builder.Services.AddScoped<IMyService, MyServiceB>();

            builder.Services.AddSingleton<MyServiceC>();
            builder.Services.AddSingleton<IMyService, MyServiceC>();
        }
    }
}

アセンブリ属性FunctionsStartupAttributeStartup クラスを指定する。

IFunctionsHostBuilder を使ってサービスを登録する。登録メソッドは登録するサービスの有効期間毎に別れており、各インスタンスの有効期間は以下の3通りになる。

  • 一時的(Transient)
  • スコープ(Scoped)
    • Function(関数)実行ごとに1回生成される
  • シングルトン(Singleton)
    • ホストの有効期間と一致する
    • DocumentClientHttpClient など接続やクライアントに推奨されている

なお IFunctionsConfigurationBuilderFunctionsHostBuilderContext を取得すると以下の3つの値が取得できる。

    var context = builder.GetContext();
    var rootPath = context.ApplicationRootPath;
    var configuration = context.Configuration;
    var environment = context.EnvironmentName;      


関数側は、 Azure Functions のクラスとメソッドの static を外し、コンストラクタを実装してインジェクションするための引数を追加する。

public class Function1
{
    private readonly MyServiceA _serviceA;
    private readonly MyServiceB _serviceB;
    private readonly MyServiceC _serviceC;
    private readonly IMyService _service;

    public Function1(
        MyServiceA serviceA,
        MyServiceB serviceB,
        MyServiceC serviceC,
        IMyService service)
    {
        _serviceA = serviceA;
        _serviceB = serviceB;
        _serviceC = serviceC;
        // 3つの有効期間で同じインターフェイスを登録してると Scoped のインスタンスが入ってくるらしい
        _service = service;
    }

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        return new OkObjectResult("This HTTP triggered function executed successfully.");
    }
}

参考:

環境変数を設定する

IConfigurationStartup クラスの中で何もしなくてもインジェクションされる。ローカル実行時も ConfigurationBuilder と違い明示的に local.settings.json を追加しなくて良い。

    public class Function1
    {
        private readonly IConfiguration _configuration;
        public Function1(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [FunctionName("Function1")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation(_configuration["AzureWebJobsStorage"]);
            log.LogInformation(_configuration["APPLICTION_SETTINGS_VALUE"]);
            log.LogInformation(_configuration.GetConnectionString("DB_CONNECTION_STRING"));

            return new OkObjectResult("This HTTP triggered function executed successfully.");
        }
    }

f:id:yotiky:20210128210604p:plain

f:id:yotiky:20210129211630p:plain

HttpClient を利用する

Nuget で Microsoft.Extensions.Http をインストールする。
v5.0.0 だとMicrosoft .Extentions.DependencyInjection のライブラリがバージョン不一致を起こすので注意。

サービスに HttpClient を登録する。(この1行なくてもインジェクションされるけどどうなんだろうか)

    class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddHttpClient();
        }
    }

Functions 側はコンストラクタで受け取る。

    private readonly HttpClient _httpClient;

    public Function1(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

ロガーを利用する

host.jsonlogLevel を追加する。 FunctionApp1の部分は出力対象とする名前空間やクラス名を指定する。

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingExcludedTypes": "Request",
      "samplingSettings": {
        "isEnabled": true
      }
    },
    "logLevel": {
      "FunctionApp1": "Information"
    }
  }
}

Functions で利用する場合は、コンストラクタで受け取るだけ。 デフォルトで付いてくるメソッド引数の log は削除して問題ない。

    private readonly ILogger<Function1> _logger;

    public Function1(ILogger<Function1> logger)
    {
        _logger = logger;
    }

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
    {
        _logger.LogInformation("ILogger<Functions1> _logger");

        return new OkObjectResult("This HTTP triggered function executed successfully.");
    }

Functions 以外のクラスで利用する場合は、対象のクラスに ILogger をインジェクションしてもらうために、Startup クラスの Configure メソッドでサービスに登録する。 こうすることでロガーをクラス間でたらい回しにしなくて済む。

public class LogWriter
{
    private readonly ILogger<LogWriter> _logger;
    public LogWriter(ILogger<LogWriter> logger)
    {
        _logger = logger;
    }

    public void Write()
    {
        _logger.LogInformation("ILogger<LogWriter> _logger");
    }
}
class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddTransient<LogWriter>();
    }
}

Functions 側では目的のクラスをインジェクションしてもらいそれを利用する。

    private readonly ILogger<Function2> _logger;
    private readonly LogWriter _logWriter;

    public Function2(
        ILogger<Function2> logger,
        LogWriter logWriter)
    {
        _logger = logger;
        _logWriter = logWriter;
    }

    [FunctionName("Function2")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
    {
        _logger.LogInformation("ILogger<Functions2> _logger");
        _logWriter.Write();

        return new OkObjectResult("This HTTP triggered function executed successfully.");
    }

f:id:yotiky:20210129234436p:plain

参考:Azure Functions におけるロガーの扱いとフィルタの注意点 - しばやん雑記

appsettings.json を読み込む

ASP.NET Core でよく使われる appsettings.json を読み込む。local.settings.json では配列や特殊なオブジェクトなどを定義できない。

プロジェクトに appsettings.json を追加して、出力ディレクトリに「常にコピーする」ように設定する。実行環境毎の設定は appsettings.Development.json などを追加する。

f:id:yotiky:20210130011947p:plain:w300

{
  "SampleAppSettings": {
    "Name": "SampleAppSettings",
    "Items": [
      {
        "Key": "Key1",
        "Message": "Message1"
      },
      {
        "Key": "Key2",
        "Message": "Message2"
      }
    ]
  }
}

設定を受け取るクラスを定義する。

public class SampleAppSettings
{
    public string Name { get; set; }
    public Item[] Items { get; set; }
}

public class Item
{
    public string Key { get; set; }
    public string Message { get; set; }
}

Startup クラスで appsettings.json を読み込んで、設定を受け取るクラスを登録する。

public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
    base.ConfigureAppConfiguration(builder);

    builder.ConfigurationBuilder
        .AddJsonFile(Path.Combine(context.ApplicationRootPath, "appsettings.json"), optional: true, reloadOnChange: false)
        .AddJsonFile(Path.Combine(context.ApplicationRootPath, $"appsettings.{context.EnvironmentName}.json"), optional: true, reloadOnChange: false)
        .AddEnvironmentVariables();
}
public override void Configure(IFunctionsHostBuilder builder)
{
    builder.Services.Configure<SampleAppSettings>(context.Configuration.GetSection("SampleAppSettings"));
}

参考:構成ソースのカスタマイズ - .NET Azure Functions で依存関係の挿入を使用する

Functions 側は IOption<SampleAppSettings> で受け取る。

public Function1(IOptions<SampleAppSettings> sampleAppSettings)
{
}

参考:Azure Functions V2 の Startup.cs で appsettings.json を読み込む(2019年5月バージョン) - BEACHSIDE BLOG


設定にオブジェクト単位ではなく、個別のパラメータを定義してバインドすることもできる。 local.settings.jsonMyOptions:Value1MyOptions:Value2 を追加する。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "MyOptionsSettings:Value1": "local.settings.json-Value1",
    "MyOptionsSettings:Value2": "local.settings.json-Value2"
  }
}

設定を受け取るクラスを定義する。

public class MyOptions
{
    public string Value1 { get; set; }
    public string Value2 { get; set; }
}

Startup クラスで設定を読み込んで、MyOptions クラスを MyOptionSettings セクションにバインドする。

public override void Configure(IFunctionsHostBuilder builder)
{
    builder.Services.AddOptions<MyOptions>()
        .Configure<IConfiguration>((target, configuration) =>
        {
            configuration.GetSection("MyOptionsSettings").Bind(target);
            // もしくは
            //configuration.Bind("MyOptionsSettings", target);
        });
}

Functions 側は IOptions<MyOptions> で受け取る。

public Function1(IOptions<MyOptions> options)
{
}

f:id:yotiky:20210130204011p:plain

参考:Azure Functions で設定情報を使いたい - Qiita

オプション

環境変数を含むアプリ設定に定義されている値は IConfiguration で読み取ることできる。 さらに、ASP.NET Core ではオプションパターンを使って IConfiguration から任意のクラスに値を抽出することができる。 このパターンで関連する設定をグループ化する。

docs.microsoft.com

参考:

Key Valut を読み込む

Azure Key Vault のアクセスポリシーは「取得」と「一覧」を設定する。

f:id:yotiky:20210131004000p:plain:w200

Key Vault のエンドポイントをアプリケーション設定などに保存する。local.settings.json の例。

f:id:yotiky:20210131004546p:plain

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "KeyVaultEndpoint": "https://<KeyVaultName>.vault.azure.net/"
  }
}

Nuget で以下のパッケージをインストールする。

  • Azure.Extensions.AspNetCore.Configuration.Secrets

Startup で、Key Vault のエンドポイントを使って Configuration に追加する。

class Startup : FunctionsStartup
{
    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {
            var builtConfig = builder.ConfigurationBuilder.Build();

            builder.ConfigurationBuilder.AddAzureKeyVault(new Uri(builtConfig["KeyVaultEndpoint"]), new DefaultAzureCredential());
    }
}

※Options を使う場合は、「appsettings.json を読み込む」の項目を参照。

Functions 側は インジェクションした IConfiguration からシークレットの名前で取得できる。

    public class Function1
    {
        private readonly IConfiguration _configuration;
        public Function1(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [FunctionName("Function1")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation(_configuration["keys-blobconnectionstring"]);

            return new OkObjectResult("This HTTP triggered function executed successfully.");
        }
    }

以下は、既に非推奨となっているライブラリであるが、実装例として残しておく。

Nuget で以下のパッケージをインストールする。

Startup で、Key Vault を参照して Configuration に追加する。

class Startup : FunctionsStartup
{
    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {
        var builtConfig = builder.ConfigurationBuilder.Build();
        var tokenProvider = new AzureServiceTokenProvider();
        var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));
    
        builder.ConfigurationBuilder.AddAzureKeyVault(builtConfig["KeyVaultEndpoint"], keyVaultClient, new DefaultKeyVaultSecretManager());
    }
}

参考:

Blob Storage の Client を利用する

Nuget で以下のパッケージをインストールする。

  • Microsoft.Extensions.Azure
  • Azure.Storage.Blobs

Startup で、Blob Service Client を追加する。接続先が複数ある場合は名前を付けておく。

class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var context = builder.GetContext();
        builder.Services.AddAzureClients(x =>
        {
            var connectionString = context.Configuration["AzureWebJobsStorage"];
            x.AddBlobServiceClient(connectionString);
            //x.AddBlobServiceClient(connectionString).WithName("BlobClientA");
        });
    }
}

Functions 側では、BlobServiceClient を受け取る。

public Function2(BlobServiceClient blobClient)
{
}

名前付きのクライアントを取得するには、IAzureClientFactory<BlobServiceClient> をインジェクションしてもらう。

public Function2(IAzureClientFactory<BlobServiceClient> clientFactory)
{
    var namedClient = clientFactory.CreateClient("BlobClientA");
}

参考:【.NET】BlobServiceClientFactoryはあったんだ! – 10bace LOG

Table Storage の Client を利用する

Nuget で以下のパッケージをインストールする。

Startup で、CloudTableClient を追加する。

class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var context = builder.GetContext();
        builder.Services.AddSingleton(_ =>
        {
            var connectionsString = context.Configuration["AzureWebJobsStorage"];
            var storageAccount = CloudStorageAccount.Parse(connectionsString);
            return storageAccount.CreateCloudTableClient();
        });
    }
}

Functions 側では、CloudTableClient を受け取る。

public Function2(CloudTableClient tableClient)
{
}

昔の書き込みはスレッドセーフじゃないってあるが、最近はライフサイクルは Singleton で良さそう。

Queue Storage の Client を利用する

QueueClientインスタンスqueuName に結びつくため、キュー毎にラップしたクラスなどを用意して別々に生成する必要がある。

yotiky.hatenablog.com

App Configuration を読み込む

App Configuration のエンドポイントをアプリケーション設定などに保存する。

Nuget で以下のパッケージをインストールする。

  • Azure.Extensions.AspNetCore.Configuration.Secrets

Startup で、App Configuration のエンドポイントを使って Configuration に追加する。

class Startup : FunctionsStartup
{
    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {
            var builtConfig = builder.ConfigurationBuilder.Build();

            builder.ConfigurationBuilder.AddAzureAppConfiguration(options =>
                options.Connect(new Uri(builtConfig["AppConfigEndpoint"]), new ManagedIdentityCredential()));
    }
}

設定や接続文字列を使うなど詳細は以下を参照。

yotiky.hatenablog.com

関連記事

Azure Functions で Azure Key Valut から設定を読み込む

Azure Key Valut は、トークン、パスワード、証明書、API キー、接続文字列、その他のシークレットを安全に格納し、それらへのアクセスを厳密に制御できるサービス。(参考

目次

Azure Functions に Managed Id を設定

デプロイ済みの Azure Functions の設定から ID を選択する。

f:id:yotiky:20210130214151p:plain:w300

システム割り当て済みをオンにする。

f:id:yotiky:20210130214226p:plain:w300

Azure Key Vault を設定

Azure Key Valut を作成していなければ作成する。

f:id:yotiky:20210130214812p:plain:w400

作成した Key Valut で設定からシークレットを選択し、キーと値を登録する。

f:id:yotiky:20210130215715p:plain:w300

f:id:yotiky:20210130215846p:plain

続いて、設定からアクセスポリシーを選択し、アクセスポリシーを追加する。

f:id:yotiky:20210130215427p:plain:w300

シークレットのアクセス許可を「取得」、プリンシパルの選択で Azure Functions のオブジェクトIDを入力して、リソースを選択する。追加し終わったら一覧で保存する。

f:id:yotiky:20210130220354p:plain

アプリケーション設定に Key Vault の識別子を設定

Key Valut で作成したキーを開いて、シークレット識別子をコピーする。

f:id:yotiky:20210130220909p:plain

Azure Functions のアプリケーション設定で、シークレット識別子を以下の書式で値として設定する。

@Microsoft.KeyVault(SecretUri=<シークレット識別子>)

正常に接続できるとキーコンテナーの参照にチェックが付く。 f:id:yotiky:20210130221845p:plain

コードからアプリケーション設定を取得すると Key Vault の値が展開される。

var connectionString = Configuration["BLOB_CONNECTION_STRING_KEYVAULT"]

参考

blog.beachside.dev

関連記事

Azure Functions で Azure Storage への接続情報

目次

検証環境

  • Azure Functions v3
  • Azure Blob Storage
  • Azure.Storage.Blobs v12.8.0 (ライブラリ)

直書き

サンプルコード向けなら直書き。

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

設定情報から取得

設定

ポータルで Azure Functions の構成からアプリケーション設定と接続文字列を設定できる。

アプリケーション設定には環境変数を設定する。
接続文字列にはDBの接続文字列を設定する。一応 Custom を選択するとストレージの接続文字列も使えなくはない。

f:id:yotiky:20210128202527p:plain:w300

tech-blog.cloud-config.jp

Azure Functions をローカル実行する時は、local.settings.json が設定として使われる。

AzureWebJobsStorage は、Azure Functions 作成時にリンクしたストレージの接続文字列が設定される。local.settings.jsonUseDevelopmentStorage=true が設定されている場合は、ストレージエミュレーターに接続される。

今回は、ValuesAPPLICTION_SETTINGS_VALUEConnectionStringsDB_CONNECTION_STRING を追加した。

ポータル上だと次の通り。

f:id:yotiky:20210128212501p:plain

local.settings.json では次のようになる。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "APPLICTION_SETTINGS_VALUE": "ApplicationSettingsValue"
  },
  "ConnectionStrings": {
    "DB_CONNECTION_STRING": "DBConnectionString"
  }
}

f:id:yotiky:20210128210604p:plain

実装

.NET Framework 版である v1 では、ConfigurationManager を使用していた。
.NET Core 版である v2 以降では、ConfigurationMBuilder を使用する。

ConfigurationMBuilderlocal.settings.json を追加してから Build() する。 ローカル実行の設定が不要な場合は、Configuration = new ConfigurationBuilder().Build(); でも動く。

Values は、Configuration[<key>] で取得する。
ConnectionStrings は、Configuration.GetConnectionString(<key>) で取得する。

using Microsoft.Extensions.Configuration;

public static class Function1
{
    private static IConfigurationRoot Configuration { get; }
    static Function1()
    {
        var builder = new ConfigurationBuilder()
            .AddJsonFile("local.settings.json", true)
            .AddEnvironmentVariables();
    
        Configuration = builder.Build();
    }

    [FunctionName("Function1")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        var appSettingsValue = Configuration["APPLICTION_SETTIGS_VALUE"];
        var connectionString = Configuration.GetConnectionString("DB_CONNECTION_STRING");

f:id:yotiky:20210128213716p:plain

実際は接続に必要な情報を設定した上で利用する。

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

参考

blog.shibayan.jp

関連記事

GPL v2 / LGPL v2.1 のデュアルライセンスについて

これは、GPLv2 もしくは LGPLv2.1 のデュアルライセンス についての覚書です。 ライセンスについては素人なので間違っている可能性があります。

一般財団法人ソフトウェア情報センターの「IoT 時代における OSS の利用と法的諸問題 Q&A 集」において、「D-3-8 LGPLリバースエンジニアリングの許可」の項目が非常に分かりやすかった。

Azure Functions で Azure Blob Storage からファイルを取得する

目次

検証環境

  • Azure Functions v3
  • Azure.Storage.Blobs v12.8.0

実装

Azure Functions のプロジェクトに NuGet で「Azure.Storage.Blobs」をインストールする。

using Azure.Storage.Blobs;
private static string connectionString = "YOUR CONNECTION STRING";
private static string blobContainerName = "CONTAINER_NAME";
private static string blobName = "BLOB_NAME";

private static async Task<byte[]> Download()
{
    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);
        return stream.ToArray();
    }
}

関連記事

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

MeCab.DotNet にユーザー辞書を追加する

目次

インストール

本家 MeCab より Windows 用のバイナリパッケージをダウンロードします。

f:id:yotiky:20210126163108p:plain:w350

ダウンロードしたインストーラーを実行します。 今回は辞書の文字コードは「SHIFT-JIS」を選択。

f:id:yotiky:20210126163445p:plain:w350

辞書の作成

以下の形式の csv でデータを作成します。末尾に追加エントリもできるようです。

表層形,左文脈ID,右文脈ID,コスト,品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音

引用元


作成例1)

山田太郎,,,10,名詞,一般,,,,,やまだたろう,ヤマダタロウ,ヤマダタロウ,独自辞書

f:id:yotiky:20210126175451p:plain


作成例2)

山田花子,,,10,名詞,一般,,,,,やまだはなこ,ヤマダハナコ,ヤマダハナコ,独自辞書



管理者権限でコマンドを開いて mecab-dict-index を実行します。

cd C:\Program Files (x86)\MeCab
bin\mecab-dict-index -d "dic\ipadic" -u "dic\userdic\userdic1.dic" -f shift-jis -t utf-8 "dic\userdic\userdic1.csv"
bin\mecab-dict-index -d "dic\ipadic" -u "dic\userdic\userdic2.dic" -f shift-jis -t utf-8 "dic\userdic\userdic2.csv"

オプションは以下の通り。

mecab-dict-index [options] files
-d : システム辞書のディレクトリ
-u : ユーザー辞書の保存先
-f : CSVファイルの文字コード
-t : ユーザー辞書の文字コード

実装

前回のアプリを元に修正します。

yotiky.hatenablog.com


前項で作成した辞書をシステム辞書と同じ実行ディレクトリの dic フォルダに配置します。(bin\Debug\netcoreapp3.1\dic など)

MeCabTagger を作成する際に、ユーザー辞書を設定したパラメータを引数に指定します。

    var param = new MeCabParam();
    param.DicDir = @"dic";
    param.UserDic = new[] { "userdic1.dic", "userdic2.dic" };
    var tagger = MeCabTagger.Create(param);


ユーザー辞書を追加しなかった場合、「山田太郎」が「山田」と「太郎」で分解されます。 f:id:yotiky:20210126185034p:plain


ユーザー辞書を追加した場合、「山田太郎」が名詞として1つになっています。また、末尾に追加した「独自辞書」のエントリも問題なく動いてます。 f:id:yotiky:20210126184930p:plain

参考

辞書を作成できるサンプルアプリを公開してる方もいらっしゃいました。

resanaplaza.com

関連記事