golang で neologd の正規化処理を書いてみた(けどダメだった)

TL;DR

neologd を使う前にしておいた方がいい入力の前処理があります.

今回はこれを golang でささっと書いてみようと思ってはまって投げ出した話です.

neologd の wiki を参考に必要とされている正規化処理を順に見ていきます.

Regexp.ja · neologd/mecab-ipadic-neologd Wiki · GitHub

基本的には,英数記号は半角にするようになってます.

失敗したコードはこちらにあります.

https://github.com/ikawaha/chits/blob/master/neologd/neologd.go

1. 全角英数字は半角に置換

これは golang の strings.Replacer を定義するだけでささっと書けます. Replacer のいいところは文字列から文字列の変換をただただ書いていくだけでよくて, 濁点とかついた半角カタカナ(複数文字)を全角カタカナ1文字に置き換えるとか自然に書けます. しかも,内部でよしなに trie を作ってくれるみたいなので,たいていの場合はオレオレ実装しなくても効率よくやってくれそうです.

var neologdReplacer = strings.NewReplacer(
    "0", "0", 
    "1", "1", 
    "2", "2", 
    "3", "3", 
    "4", "4",
    ...
}

こんな感じで置換前(全角),置換後(半角)とぐだぐだ並べてくだけで ok です.

置換するときは,neologdReplacer.Replace("hogehoge012") とかして呼び出します.

2. 半角カタカナは全角に変換

これも上と同様に Replacer で.

3. ハイフンマイナスっぽい文字を置換

やっぱり Replacer で.

4. 長音記号っぽい文字を置換

Replacer でしょ.

5. 1回以上連続する長音記号は1回に置換

これはメソッド作って対応.単純に続いてるのがあれば1個に置き換えるだけ.

func (n NeologdNormalizer) ShurinkProlongedSoundMark(s string) string {
    var b bytes.Buffer
    for p := 0; p < len(s); {
        c, w := utf8.DecodeRuneInString(s[p:])
        p += w
        b.WriteRune(c)
        if c != ProlongedSoundMark {
            continue
        }
        for p < len(s) {
            c0, w0 := utf8.DecodeRuneInString(s[p:])
            p += w0
            if c0 != ProlongedSoundMark {
                b.WriteRune(c0)
                break
            }
        }

    }
    return b.String()
}

6.チルダっぽい文字は削除

Replacer は空文字にも置き換えられるのでこれで対応.

7.一部の全角記号を半角に置換

!”#$%&’()*+,−./:;<>?@[¥]^_`{|}

これは,半角に置き換えるように Replacer に追加.

8.一部の半角記号を全角記号に置換

句読点,中黒,カギ括弧は全角にする.もちろん Replacer で出来ます.

9. 空白

ここはちょっと難しいです.(というか出来なかった)

  • 全角スペースは半角スペースに
    • Replacer(ry
  • 解析対象テキストの先頭と末尾の半角スペースは削除
    • strings.Trim() でいけます
  • 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」間に含まれる半角スペースは削除
    • ぉ,おう
  • 「ひらがな・全角カタカナ・半角カタカナ・漢字・全角記号」と「半角英数字」の間に含まれる半角スペースは削除
    • ぉ,おう

空白の削除がむずい

空白除去のサンプルが載っているのでこれをみると,英アルファベットのときは空白取らないで,それ以外で取ればよさそう!

とおもったら,こんなテストケースがサンプルプログラムに付属している!

正規化前 正規化後
南アルプスの 天然水- Sparking* Lemon+ レモン一絞り 南アルプスの天然水- Sparking*Lemon+レモン一絞り

(天然水)- のあとの S(parking) の間の空白は削除しないで,(Sparking)*L(emon) の間の空白は除外するだと!

たしかに用意されてる python のサンプルスクリプトではそうなるんだが,どうやってこれを実現したものか?

サンプルの python スクリプトを参考にさせてもらう

サンプルスクリプトでは unicode カテゴリを定義して処理している.ふむー.

def remove_extra_spaces(s):
    s = re.sub('[  ]+', ' ', s)
    blocks = ''.join(('\u4E00-\u9FFF',  # CJK UNIFIED IDEOGRAPHS
                      '\u3040-\u309F',  # HIRAGANA
                      '\u30A0-\u30FF',  # KATAKANA
                      '\u3000-\u303F',  # CJK SYMBOLS AND PUNCTUATION
                      '\uFF00-\uFFEF'   # HALFWIDTH AND FULLWIDTH FORMS
                      ))
    basic_latin = '\u0000-\u007F'

    def remove_space_between(cls1, cls2, s):
        p = re.compile('([{}]) ([{}])'.format(cls1, cls2))
        while p.search(s):
            s = p.sub(r'\1\2', s)
        return s

    s = remove_space_between(blocks, blocks, s)
    s = remove_space_between(blocks, basic_latin, s)
    s = remove_space_between(basic_latin, blocks, s)
    return s

文字クラス blocks と basic_latin というのが定義されてる. なるほど.これを golangunicode.RangeTable で定義して unicode.In() に指定すればよさそうだ・・・ ・・・って,-* も同じ basic_latin に属するんじゃなかろ(ここで途切れている

どなたかうまく実現できましたら教えてください.

ところで半角に倒すのでいいのかな

neologd は英数アルファベットは半角に倒してるんだけど,mecab はいわゆる全角に倒してるんだよな. 形態素解析に影響しないんだろうか?英数アルファベットが入るような形態素は neologd が網羅しちゃうからいらなくなっちゃうのかな? それか辞書作るときに mecab のリソースの方も半角に変換してるんか?もうちょっと調べてみないとだ・・・

/以上