メモ2ブログ

メモtoウェブログ。旧ブログはこちら。 http://sakebook.blogspot.jp/

AndroidでKuromojiを使う

Kuromoji

KuromojiはJavaで書かれているオープンソースの日本語形態素解析エンジンです。

Androidでオフラインでも即座に形態素解析ができるように組み込んでみました。

LuceneのJapaneseTokenizerではなく、AtilikaのKuromojiを組み込みました。

はじめに言っておくと、組み込めたものはAtilikaのKuromoji-1.0-SNAPSHOTで、ipadicの辞書を利用したものです。

開発環境はKotlinです。

Atilika

0.7.7

公式で紹介されてる 0.7.7を試してみました。

MavenCentralに上がっていないので、リポジトリを追加します。

  • build.gradle
allprojects {
    repositories {
        jcenter()
        maven {
            url "http://www.atilika.org/nexus/content/repositories/atilika"
        }
    }
}

必要なライブラリをGradleのDependenciesに追加します。

  • app/build.gradle
    compile "org.atilika.kuromoji:kuromoji:0.7.7"

必要なコードを記述してビルドします。

    val tokenizer = Tokenizer.builder().build()
    val tokens = tokenizer.tokenize("我輩は猫である。名前はまだない。");
        tokens.forEach {
            Log.d("Kuromoji", "${it.allFeatures}")
        }

実行はできたのですが、空白文字以外形態素解析が機能していませんでいた。 特定したわけではないですが、おそらく辞書の読み込みに失敗しているようです。

0.9.0

調べたら、MavenCentral0.9.0が公開されてたので、そちらを使ってみました。

必要なライブラリをGradleのDependenciesに追加します。パッケージ名が異なるので注意してください。

  • app/build.gradle
    compile "com.atilika.kuromoji:kuromoji-ipadic:0.9.0"

必要なコードを記述してビルドします。
先ほどとは異なり、Builderが大文字になっています。Tokenizerのimport文ももちろん異なります。

    val tokenizer = Tokenizer.Builder().mode(TokenizerBase.Mode.NORMAL).build()
    val tokens = tokenizer.tokenize("我輩は猫である。名前はまだない。");
        tokens.forEach {
            Log.d("Kuromoji", "${it.allFeatures}")
        }

次のエラーがでました。

Caused by: java.lang.RuntimeException: Could not load dictionaries.
      at com.atilika.kuromoji.ipadic.Tokenizer$Builder.loadDictionaries(Tokenizer.java:231)
      at com.atilika.kuromoji.TokenizerBase.configure(TokenizerBase.java:77)
      at com.atilika.kuromoji.ipadic.Tokenizer.<init>(Tokenizer.java:74)
      at com.atilika.kuromoji.ipadic.Tokenizer.<init>(Tokenizer.java:59)
      at com.atilika.kuromoji.ipadic.Tokenizer$Builder.build(Tokenizer.java:203)
Caused by: java.lang.IllegalArgumentException: capacity < 0: -4
      at java.nio.ByteBuffer.allocate(ByteBuffer.java:54)
      at com.atilika.kuromoji.io.IntegerArrayIO.readArray(IntegerArrayIO.java:38)
      at com.atilika.kuromoji.buffer.WordIdMap.<init>(WordIdMap.java:35)
      at com.atilika.kuromoji.dict.TokenInfoDictionary.setup(TokenInfoDictionary.java:168)
      at com.atilika.kuromoji.dict.TokenInfoDictionary.newInstance(TokenInfoDictionary.java:160)
      at com.atilika.kuromoji.ipadic.Tokenizer$Builder.loadDictionaries(Tokenizer.java:219)
      at com.atilika.kuromoji.TokenizerBase.configure(TokenizerBase.java:77) 
      at com.atilika.kuromoji.ipadic.Tokenizer.<init>(Tokenizer.java:74) 
      at com.atilika.kuromoji.ipadic.Tokenizer.<init>(Tokenizer.java:65)

辞書の読み込みに失敗しているようです。 実際に問題となっているのは次のメソッドのようです。

  • IntegerArrayIO#readArray
    public static int[] readArray(InputStream input) throws IOException {
        DataInputStream dataInput = new DataInputStream(input);
        int length = dataInput.readInt();

        ByteBuffer tmpBuffer = ByteBuffer.allocate(length * INT_BYTES);
        ReadableByteChannel channel = Channels.newChannel(dataInput);
        channel.read(tmpBuffer);

        tmpBuffer.rewind();
        IntBuffer intBuffer = tmpBuffer.asIntBuffer();

        int[] array = new int[length];
        intBuffer.get(array);

        return array;
    }

このIssueで、フォークして解決したとあります。

しかし、別のIssueによると解決してMasterにマージされているようなので、Masterからビルドして必要なjarを生成してみます。

1.0-SNAPSHOT

試した時点でのコミットは9ce40b605990f38854a4b08c38dc08a85cc3bb8bです。

ソースを取り込んでjarを生成します。

$git clone git@github.com:atilika/kuromoji.git
$cd kuromoji
$mvn clean package

私の環境だと次のエラーがでました。

1.7 は無効な VM バージョンです。

JAVA_HOMEの設定が正しくないようだったので、設定します。

時間がかかりますが、無事生成できました。

  • kuromoji/kuromoji-core/target/kuromoji-core-1.0-SNAPSHOT.jar
  • kuromoji/kuromoji-ipadic/target/kuromoji-ipadic-1.0-SNAPSHOT.jar

を自分の形態素解析を行いたいプロジェクトのlibsに配置します。

必要なライブラリをGradleのDependenciesに追加します。

  • app/build.gradle
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile files("libs/kuromoji-core-1.0-SNAPSHOT.jar")
    compile files("libs/kuromoji-ipadic-1.0-SNAPSHOT.jar")

必要なコードを記述します

    val tokenizer = Tokenizer.Builder().mode(TokenizerBase.Mode.NORMAL).build()
    val tokens = tokenizer.tokenize("我輩は猫である。名前はまだない。");
        tokens.forEach {
            Log.d("Kuromoji", "${it.allFeatures}")
        }

無事形態素解析した結果が得られました!

D/Kuromoji: 名詞,一般,*,*,*,*,我輩,ワガハイ,ワガハイ
D/Kuromoji: 助詞,係助詞,*,*,*,*,は,ハ,ワ
D/Kuromoji: 名詞,一般,*,*,*,*,猫,ネコ,ネコ
D/Kuromoji: 助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
D/Kuromoji: 助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
D/Kuromoji: 記号,句点,*,*,*,*,。,。,。
D/Kuromoji: 名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
D/Kuromoji: 助詞,係助詞,*,*,*,*,は,ハ,ワ
D/Kuromoji: 副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
D/Kuromoji: 形容詞,自立,*,*,形容詞・アウオ段,基本形,ない,ナイ,ナイ
D/Kuromoji: 記号,句点,*,*,*,*,。,。,。

f:id:sakebook:20160513035313g:plain

まとめ

Android特有の問題もあってか、少し手間取りましたが、辞書が内包されているものは少し試すには便利です。

しかし、生成したjarが11MBもあったり、辞書展開時にOOBを引き起こしたりするので、特定の環境で専用に使う場合は良いと思いました。

それ以外の環境では、辞書のアップデートもされないし迅速なレスポンスを求めないのであればWEB API形態素解析を行うのがAndroidだと現実的だと思いました。

検証したサンプルです。

github.com

参考

kuromoji / atilika

LuceneのAnalyzer、KuromojiのModeごとの挙動を確認する / CLOVER

Kuromoji IPADIC / MVN REPOSITORY

Kuromoji on Android / GitHub

Android runtime exception when creating new Tokenizer using kuromoji-ipadic / GitHub

MacOS X でJava開発のトラブル / Qiita

Androidでラムダ式を使いたい / Qiita

忘れてしまったAndroidの署名情報は見つかるかもしれない

Androidのアプリの署名に使う情報を忘れてしまうことが、稀にあります。
やってはいけないことですが、稀にあります。

別PCで作業してていつもMaster Passwordに任せっきりだったりとか、久しぶりに更新をかけようとしたアプリで起きたりします。

同じ署名のkeystoreファイルが使えないとなると、別アプリ扱いとなるので同じアプリのストアの更新はできなくなります。なので、別アプリとしてストアに上げ直すことになります。
もちろんそれは意図した場合を除いては避けたいことなので、なんとか署名情報を思い出します。

これが助けになるかもしれないし、助けにならないかもしれません。 藁にもすがる思いでこの投稿をみつけた方には一度試してもらいたいと思います。

keystoreファイルをなくしてしまった方には今回の方法は使えません。教訓として同じことを繰り返さないようにしましょう。

署名情報

それぞれをなんと呼ぶのかわかりにくいと思ったので、この投稿では次のように扱います。

f:id:sakebook:20160503164033p:plain

画像のまんまですが、
1に入力するものを Key store password
2に入力するものを Key alias
3に入力するものを Key password

とします。

プロジェクトルートから探る

アプリのプロジェクトルートに.gradleという隠しフォルダがあると思います。 その中の、Gradleのバージョン/taskArtifacts/taskArtifacts.binというファイルを、エディターなどで無理やり開きます。

signingConfig.storePassword, signingConfig.keyAlias あたりで検索すると、 Key store passwordKey aliasが見つかります。私の環境では、Key passwordは見つかりませんでした。

Key passwordは次のツールを使えば探すことができます。

力技をサポートしてくれるツールです。

Android-keystore-password-recover by MaxCamillo

パスワードに使われていそうな文字列にあたりがある場合は見つけられる確率は少しあると思いますが、非常に時間がかかります。 上のツールを使うと、Key aliasはすぐに表示してくれます。

Android Studioのログから探る

~/Library/Logs/に、いろんなアプリケーションのログがあります。AndroidStudioのものは複数見つかると思いますが、Releaseビルドを成功させたバージョンのログをみます。idea.logというファイルをエディターで開きます。こちらもidea.log.1など複数あると思いますが、大まかに日ごとに分かれているので、作成日などをヒントにあたりをつけましょう。

android.injected.signing.storeと検索すると、近くに見つかると思います。GUI上で****と入力された履歴の下に、実際に入力されていた文字列がそのまま表示されています。

こちらは、Key store password, Key alias, Key password すべてが見つかります。

まとめ

keystoreファイルと同じく、それに使う署名情報もバックアップが取れる形で保存しておきましょう。
GUIでapkを生成するのではなく、CLIで生成できるようにしておき、人力による入力漏れを無くすのも効果的です。

Please Advice Needed, Lost KeyStore password / StackOverFlow

android KeystorePassとaliasPassを忘れた時の対処法 / Game Factory

Android Keystore Password Recovery / GitHub Pages

How to retrieve Key Alias and Key Password for signed APK in android studio(migrated from Eclipse) / StackOverFlow

jsoupのデフォルトのUser-Agentはなんなのか。

jsoup

JavaでHTMLをいい感じにParseできる便利なライブラリです。 WebページにアクセスしてHTMLを取得し、それを加工することでWebページ内のコンテンツを簡単に取得することができます。

スクレイピング先が、User-Agent(UA)によって表示を変えている場合、それに応じてjsoupのUAを変更しなければ、思っていたコンテンツが取得できない場合があります。

User-Agentの設定

Connection#userAgent(String) で設定できます。次のようにすることで設定されたUAを確認できます。

String url = "http://www.useragentstring.com/";
Connection con = Jsoup.connect(url);
con.userAgent("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0");
Doc doc = con.get();
doc.getElementById("uas_textfeld").val());

メソッドで指定した通り、

Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0

が設定されているのがわかります。

未設定時

未設定のときにどうなるのか調べてみました。 Androidエミュレータで前述のようにし、UAの設定をしないと次のようになりました。

  • Genymotion
Dalvik/2.1.0 (Linux; U; Android 5.0; Nexus 5 - 5.0.0 - API 21 - 1080x1920_1 Build/LRX21M)

どこで誰が設定しているんだろうと思い、jsoupのコードを見ていたのですが、AndroidSDKHttpURLConnectionが使われているところで詰んでしまいました。

frameworkのところまで調べていくと、 RuntimeInitというクラスで、デフォルトのUAに使われる値が生成されていました。

    /**
     * Returns an HTTP user agent of the form
     * "Dalvik/1.1.0 (Linux; U; Android Eclair Build/MASTER)".
     */
    private static String getDefaultUserAgent() {
        StringBuilder result = new StringBuilder(64);
        result.append("Dalvik/");
        result.append(System.getProperty("java.vm.version")); // such as 1.1.0
        result.append(" (Linux; U; Android ");
    
        String version = Build.VERSION.RELEASE; // "1.0" or "3.4b5"
        result.append(version.length() > 0 ? version : "1.0");
    
        // add the model for the release build
        if ("REL".equals(Build.VERSION.CODENAME)) {
            String model = Build.MODEL;
            if (model.length() > 0) {
                result.append("; ");
                result.append(model);
            }
        }
        String id = Build.ID; // "MASTER" or "M4-rc20"
        if (id.length() > 0) {
            result.append(" Build/");
            result.append(id);
        }
        result.append(")");
        return result.toString();
    }

他のやり方ではどのように表示されるのか、いろんな方法でUAが確認できるサイトにアクセスしてみました。

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36
Mozilla/5.0 (Linux; Android 6.0.1; Nexus 6 Build/MMB29V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.105 Mobile Safari/537.36
curl/7.43.0
  • httpie
HTTPie/0.8.0

httpieが一番わかりやすく、ソースを見ると、

...
DEFAULT_UA = 'HTTPie/%s' % __version__
...
def get_default_headers(args):
    default_headers = {
        'User-Agent': DEFAULT_UA
    }
    ...

となっていたので、アクセスするプログラムが決めているのだとわかりました。 curlやブラウザなどは中がわからなかったのですが、おそらく内部で同じように自分のUAを設定しているのだと思います。

間接的で、直接はわかっていないのですが、少しスッキリしました。

jsoupのデフォルトのUA

Androidから使うときは、Android SDKHttpURLConnectionのデフォルトのUAが適応され、Android端末のUAとなります。

参考

jsoup: Java HTML Parser / jsoup

JSoup UserAgent, how to set it right? / StackOverFlow

User Agent String.Com / UserAgentString.com

What's the default Jsoup User Agent string? / StackOverFlow

Cross Reference: RuntimeInit.java / OpenGrok

jkbrzt/httpie / GitHub