Sunday, January 7, 2018 / data-mining, dtp

大量にある日本語ファイル名をスクリプトで処理するのに都合がよいファイル名に変換

大量のDTPデータの支給を受けたときにファイル名がスクリプトで扱いづらい日本語などの文字列になっている場合が まま ある。

これを 元の意味がわかるように日本語を英語に直すなどの手間をかけるのは面倒、 かといって、一括で大量にあるファイル名を連番などに書きかえてしまうとそれはそれでうれしくない。 そもそもファイル名がファイルの内容を示しているのはよいことだし、もし再度更新データの支給があったときには大変なことになる。

ChaSen(茶筌)などの、形態素解析ツールを使えば、日本語を よみ に変換できるのでこれを ICU4J で よみ→ローマ字に変換すればいいのは わかっていたが、普段 Groovy で仕事をしているので、 ChaSen などの 非Java言語で実装されたものは諸事情により遠慮したい。 そう思っていたら、最近 Kuromoji というツールを知りました。

Javaで実装されていて、辞書付きで maven central repository から取得できるので、Groovy や Gradle プロジェクトで使うにはとても便利。 あるプロジェクトで、実際に使ってみたらとても便利だったので、このエントリーでそのあたりの経験をシェアします。

基本的な使い方

たとえば、 「やせ蛙負けるな一茶ここにあり」という文字列を「 yasekaerumakerunaissakokoniari 」に 変換してみます。

@Grab(group='com.atilika.kuromoji', module='kuromoji-ipadic', version='0.9.0')
@Grab(group='com.ibm.icu', module='icu4j', version='60.2')

import com.atilika.kuromoji.ipadic.Token
import com.atilika.kuromoji.ipadic.Tokenizer
import com.ibm.icu.text.Transliterator

def tokenizer = new Tokenizer()
def transliterator = Transliterator.getInstance("Any-Latin")

def text = 'やせ蛙負けるな一茶ここにあり'

def sb = ''<<''
tokenizer.tokenize(text).each { token->
    def reading = transliterator.transliterate(token.reading)
    sb << ( ( reading!='*' ) ? reading : token.surface )
}

def textLatin = sb.toString()
assert textLatin == 'yasekaerumakerunaissakokoniari'

println "$text -> $textLatin"

このファイルを test.groovy などのファイルに保存して groovy test.groovy すれば以下のように出力されます。

やせ蛙負けるな一茶ここにあり -> yasekaerumakerunaissakokoniari

なお、ICU4J の Transliterator を getInstance するときに指定する文字列 Any-Latin を使うと Latin 文字に変換してくれますが、 これだけでは不十分なこともあるので、さらに Latin-ASCII を使うことで 文字列を ASCII 文字の範囲に収めることができます。

つまり...

def transliterator1 = Transliterator.getInstance("Any-Latin")
def transliterator2 = Transliterator.getInstance("Latin-ASCII")

の2つの Transliterator インスタンスを用意しておき、以下のように二段階に適用します。

def reading = transliterator2.transliterate(( transliterator1.transliterate(token.reading) )

実務上でのポイント

処理対象の日本語文字列が企業名などの固有名詞の場合、対象となる日本語に対する よみ が辞書に存在しないため、変換できない、ということが結構あります。 とりあえず、わたしが直面したケースでは、その部分は無視して処理しても問題にならなかったのですが、 そうでない場合はその部分に関しては自前で対処する必要があります。

辞書を自前で準備、という手も場合によってはありかと。

おまけ 銀魂のタイトルを変換してみた

処理結果から先に…

# ジャンプは時々土曜に出るから気を付けろ
  ->janpuhatokidokidoyouniderukarakiwotsukero
# ジジイになっても名前で呼び合える友達を作れ
  ->ジジイnina~tsutemonamaedeyobiaerutomodachiwotsukure
# 一度した約束は死んでも守れ
  ->ichidoshitayakusokuhashindemomamore
# ペットは飼い主が責任を持って最後まで面倒みましょう
  ->pettohakainushigasekininwomo~tsutesaigomademendoumimashou
# 粘り強さとしつこさは紙一重
  ->nebaridzuyosatoshitsukosahakamihitoe
# 喧嘩はグーでやるべし
  ->kenkahaグーdeyarubeshi
# 疲れた時は酸っぱいものを
  ->tsukaretatokihasuppaimonowo
# べちゃべちゃした団子なんてなぁ団子じゃねぇバカヤロー
  ->bechabechashitadangonantena~adangojane~ebakayarō
# 第一印象がいい奴にロクな奴はいない
  ->daiichiinshougaiiyakkonirokunayatsuhainai
# コスプレするなら心まで飾れ
  ->kosupuresurunarakokoromadekazare
# サンタなんていねーんだよって言い張る奴こそホントはいるって信じたいんだよ
  ->santananteinēndayotteiiharuyatsukosohontohairutteshinjitaindayo

辞書にないため変換できなかったところは元の文字列をそのまま入れています。

コードはこちら

def titleList = '''\
    |ジャンプは時々土曜に出るから気を付けろ
    |ジジイになっても名前で呼び合える友達を作れ
    |一度した約束は死んでも守れ
    |ペットは飼い主が責任を持って最後まで面倒みましょう
    |粘り強さとしつこさは紙一重
    |喧嘩はグーでやるべし
    |疲れた時は酸っぱいものを
    |べちゃべちゃした団子なんてなぁ団子じゃねぇバカヤロー
    |第一印象がいい奴にロクな奴はいない
    |コスプレするなら心まで飾れ
    |サンタなんていねーんだよって言い張る奴こそホントはいるって信じたいんだよ'''.stripMargin('|').split(System.getProperty('line.separator'))


@Grab(group='com.atilika.kuromoji', module='kuromoji-ipadic', version='0.9.0')
@Grab(group='com.ibm.icu', module='icu4j', version='60.2')

import com.atilika.kuromoji.ipadic.Token
import com.atilika.kuromoji.ipadic.Tokenizer
import com.ibm.icu.text.Transliterator

def tokenizer = new Tokenizer()
def transliterator1 = Transliterator.getInstance("Any-Latin")

titleList.each { title->
    def sb = ''<<''
    tokenizer.tokenize(title).each { token->
        def reading = transliterator1.transliterate(token.reading)
        sb << ( ( reading!='*' ) ? reading : token.surface )
    }

    def titleLatin = sb.toString()

    println "# ${title}"
    println "  ->${titleLatin}"
}

やはり銀魂のタイトルは奥が深い。