ビルドしたkuromojiのJARファイルへのアクセス用のコードをScalaで書いたところ、reflectionの使い方でハマった件。

By | 2020年6月27日 , Last update: 2022年1月7日

はじめに

前の記事で、kuromojiに対してipadic及び最新のNEologdを組み込んだJARファイルをビルドし、試運転した件について書きました。

kuromoji自体は複数の辞書に対応していますので、同一の文に対して異なる辞書を用いた場合の形態素解析の結果を調べることができます。そもそもNEologd自体がWeb上から得た新語を追加するカスタマイズを行ったシステム辞書ですので、NEologdが組み込まれているものとそうでないものでは解析の結果が変化する可能性がありそうです(新しくできた駅とか)。

そこで、上記のJARファイルへのアクセス用のプログラム(以下、「テスト用プログラム」と書きます。)を当初予定していたipadic及び最新のNEologdを組み込んだJARファイル専用とするのではなく、kuromojiのビルド時に作成された他の辞書を組み込んだJARファイルも適宜切り替えて読み込めるように実装することとしました。

スポンサーリンク

必要事項の整理

まず最初に、テスト用プログラムに辞書の切り替え機能を組み込むために必要な事項を整理します。

以下のような感じに整理できそうです。

  1. 上記の辞書を組み込んだJARファイルのうちのいずれか1個をテスト用プログラムから呼び出せるように実装すること。
  2. テスト用プログラムの実行時にコマンドラインオプションを用いて読み込みの対象となる辞書のJARファイルを切り替える機能を追加すること。

kuromojiは(辞書ファイルも含めて)Javaで実装されていますが、テスト用プログラム自体は本Webサイトの管理人たるpandaのモチベーションの問題等の理由によりScalaで実装しますので、上記の事項をScalaで実現するための方法についても追加で調べる必要がありそうです。

辞書のJARファイルの組み込み

kuromojiのWebサイトによると、kurumojiは以下の7個の辞書をサポートしています(個々の辞書の詳細な説明については省略します)。

  1. IPADIC (2.7.0-20070801)
  2. IPADIC NEologd (2.7.0-20070801-neologd-20171113)
  3. JUMANDIC (7.0-20130310)
  4. NAIST jdic (0.6.3b-20111013)
  5. UniDic (2.1.2)
  6. UniDic Kana Accent (2.1.2)
  7. UniDic NEologd (2.1.2-neologd-20171002)

上記の辞書のうち、前の記事のビルド作業では2番目のIPADIC NEologdに代えて、最新のNEologdを組み込んだIPADIC NEologdをビルドしています。

前の記事でkuromojiのビルドを行ったPC(Fedora32)のMavenのローカルリポジトリ($HOME/.m2/repository/)を見たところ、以下のJARファイルがcom/atilika/kuromojiの下に作成されていました。

[panda@pandanote.info kuromoji]$ find . -name \*.jar -print
./kuromoji-core/1.0-SNAPSHOT/kuromoji-core-1.0-SNAPSHOT.jar
./kuromoji-core/1.0-SNAPSHOT/kuromoji-core-1.0-SNAPSHOT-tests.jar
./kuromoji-ipadic/1.0-SNAPSHOT/kuromoji-ipadic-1.0-SNAPSHOT.jar
./kuromoji-ipadic-neologd/1.0-SNAPSHOT/kuromoji-ipadic-neologd-1.0-SNAPSHOT.jar
./kuromoji-jumandic/1.0-SNAPSHOT/kuromoji-jumandic-1.0-SNAPSHOT.jar
./kuromoji-naist-jdic/1.0-SNAPSHOT/kuromoji-naist-jdic-1.0-SNAPSHOT.jar
./kuromoji-unidic/1.0-SNAPSHOT/kuromoji-unidic-1.0-SNAPSHOT.jar
./kuromoji-unidic-kanaaccent/1.0-SNAPSHOT/kuromoji-unidic-kanaaccent-1.0-SNAPSHOT.jar
./kuromoji-unidic-neologd/1.0-SNAPSHOT/kuromoji-unidic-neologd-1.0-SNAPSHOT.jar
./kuromoji-benchmark/1.0-SNAPSHOT/kuromoji-benchmark-1.0-SNAPSHOT.jar

 

上記のJARファイルのうち、kuromoji-core及びkuromoji-benchmarkを除いたディレクトリの下にあるJARファイルがそれぞれ上記の辞書と1対1に対応していると考えて良さそうです。

なお、GitHubから取得したコードをビルドすると、バージョン番号は1.0-SNAPSHOTになります。また、これはmavenのリポジトリからは取得できないため、Scalaのプロジェクトにはアンマネージドな依存性を持つライブラリ(=mavenのリポジトリからは取得しない)として取り込むことにします。


スポンサーリンク

Scalaのプロジェクトへの取り込みは以下の手順で行いました。

  1. Scalaのプロジェクトのルートディレクトリにlibというディレクトリを作成する。
  2. 手順1で作成したディレクトリの下に、ビルドしたJARファイル(コアライブラリ: kuromoji-core-1.0-SNAPSHOT.jarを含む。)をコピーする。なお、今回はテスト用プログラムのビルド時間の節約のため、以下の5個のJARファイルのみをコピーしました。
    • kuromoji-core-1.0-SNAPSHOT.jar (コアライブラリ)
    • kuromoji-ipadic-1.0-SNAPSHOT.jar
    • kuromoji-ipadic-neologd-1.0-SNAPSHOT.jar
    • kuromoji-unidic-1.0-SNAPSHOT.jar
    • kuromoji-unidic-neologd-1.0-SNAPSHOT.jar

辞書の切り替え機能のScalaでの実装

次に、辞書の切り替え機能をScalaで実装します。

実装の方針決め

上記の辞書のJARファイルには(パッケージ名はJARファイルごとに異なりますが)、以下の2個のクラスがあります。

  1. Tokenizerクラス
  2. Tokenクラス

kuromojiのGitHubの(Javaによる)コード例を見る限り、Tokenizerクラスのインスタンスに解析の対象となる文を入力として与えると、その文に対して形態素解析を行うことによって得られた単語ごとに作られるTokenクラスのインスタンスのリストが出力として得られるようです。

また、Tokenizerクラス及びTokenクラスはコアライブラリにあるTokenizerBaseクラス及びTokenBaseクラスの子クラスになっています。したがって、以下の手順でBaseクラスのインスタンスを作成することで、もとのFQCNに対応した辞書を使用することができそうです。

  1. パッケージ名まで含めたFQCNを使ってScalaのreflectionによりFQCNに対応するインスタンスを作る。
  2. 手順1で作成したインスタンスのコンストラクタを呼び出す。
  3. 適宜Baseクラスのインスタンスへとcastする。

ところが、kuromojiの0.9以降のバージョンでは、Tokenizerクラスの内部にBuilderというクラスが存在し、このインスタンスに対してTokenizerクラスのインスタンスに渡すためのオプションをメソッドチェーンで設定し、チェーンの最後でbuildメソッドを呼ぶことでオプションが反映されたTokenizerクラスのインスタンスを得る仕組みになっています。

したがって、FQCNとして指定すべきなのは、”<パッケージ名>.Tokenizer” ではなく、”<パッケージ名>.Tokenizer.Builder” になります。

コード例


スポンサーリンク

ここまでの動作を実現するためのコード例は以下のような感じになります。なお、scala.reflect.runtimeパッケージはscala-reflectライブラリに含まれていますので、build.sbtのlibraryDependenciesにscala-reflectライブラリを追加する必要があります。

import scala.reflect.runtime.{ universe => ru }
(中略)
val runtimeMirror: ru.Mirror = ru.runtimeMirror(getClass.getClassLoader)
val staticClassSymbol = runtimeMirror.staticClass(tokenizerClassName)
val classMirror: ru.ClassMirror = runtimeMirror.reflectClass(staticClassSymbol)
val constructors = staticClassSymbol.typeSignature.members.filter(_.isConstructor).toList
val constructorMirror = classMirror.reflectConstructor(constructors.head.asMethod)
val b = constructorMirror().asInstanceOf[TokenizerBase.Builder]

 

上記のコード例の3行目のtokenizerClassNameにFQCNを与えると、8行目にある変数bにTokenizerBase.Builderクラスのコンストラクタがセットされます。

ScalaのreflectionはJavaのそれと比べて機能豊富な分、かなり難解なものがあります(正直なところ、正確に理解できているかあやしいです…)が、上記のコードにおける大まかな処理の流れは以下の通りです。

  1. クラスローダからランタイムミラーを取得する。
  2. ランタイムミラーとFQCNを使って、staticなクラスのシンボルを取得する(Builderがstaticに定義されているため)。
  3. staticなクラスのシンボルからそれに対応するクラスミラーを取得する。
  4. クラスミラーからコンストラクタメソッドのシンボルを取得する。
  5. コンストラクタメソッドのシンボルからコンストラクタミラーを取得する。コンストラクタは複数存在する可能性がありますが、TokenizerBase.Builderクラスのコンストラクタは1個しかないので、複数あるかどうかの確認及び選択のための処理は省略しています。
  6. コンストラクタミラーのapplyメソッドを実行することで得られたインスタンスをTokenizeBase.Builderクラスにcastする。

TokenizerBase.Builderクラスのコンストラクタを得ることができたら、メソッドチェーンを用いて必要なオプションをセットしてbuildメソッドを実行してから、TokenizeBaseクラスにcastすると所望の形態素解析器を得ることができます。

コード例は以下のような感じになります。

val tokenizer = b.build().asInstanceOf[TokenizerBase]

 

処理例

IPADIC Neologdを組み込んだkuromojiのJARファイルを用いた処理例は以下のような感じになります。

$ java -jar target/scala-2.13/kuromojitest-assembly-0.1.0-SNAPSHOT.jar -d ipadic.neologd -t “虎ノ門ヒルズ駅で降りると、徒歩で新虎通りに出ることができる。”
com.atilika.kuromoji.ipadic.neologd.Tokenizer
虎ノ門ヒルズ駅 名詞,固有名詞,一般,*,*,*,虎ノ門ヒルズ駅,トラノモンヒルズエキ,トラノモンヒルズエキ
で 助詞,格助詞,一般,*,*,*,で,デ,デ
降りる 動詞,自立,*,*,一段,基本形,降りる,オリル,オリル
と 助詞,接続助詞,*,*,*,*,と,ト,ト
、 記号,読点,*,*,*,*,、,、,、
徒歩 名詞,一般,*,*,*,*,徒歩,トホ,トホ
で 助詞,格助詞,一般,*,*,*,で,デ,デ
新虎通り 名詞,固有名詞,一般,*,*,*,新虎通り,シントラドオリ,シントラドーリ
に 助詞,格助詞,一般,*,*,*,に,ニ,ニ
出る 動詞,自立,*,*,一段,基本形,出る,デル,デル
こと 名詞,非自立,一般,*,*,*,こと,コト,コト
が 助詞,格助詞,一般,*,*,*,が,ガ,ガ
できる 動詞,自立,*,*,一段,基本形,できる,デキル,デキル
。 記号,句点,*,*,*,*,。,。,。

 

まとめ

reflectionの実装については、コンストラクタが複数存在した場合に必要なコンストラクタを選択するための処理を欠くなどの点で、多少の手抜き感は否めませんが、参考にしていただければ幸いです。

この記事は以上です。

References / 参考文献