【第3回】Python3とMeCabを使ってHTMLで自動ルビ振りを実現させるためへの挑戦

投稿者: | 2018年6月21日

MeCabに読み仮名が存在しない場合のエラー回避

元のコードではMeCabで解析し8番目の項目にある読み仮名を変数に代入して処理をしているが、アルファベットや数字などMecabで解析した時にその項目が空の場合、代入する項目が存在しないためにエラーが出現する。

それを回避するために下記のコードで、読み仮名が存在する場合にのみ変数を代入するようにした。

# アルファベットや数字など読み仮名が存在しない場合にエラーになるので読み仮名が存在する時のみ代入させる
if node.feature.split(",")[7:]:
    # 読み仮名を代入
    yomi = node.feature.split(",")[7]
    kana = henkan(yomi)

「漢字」「仮名」「漢字」の場合に適切なふりがなが自動でルビ振りされるようにする

元のコードでは例えば「男の子」などのように「漢字」「仮名」「漢字」の組み合わせの場合に

男の子おとこのこ

とルビが振られるので「の」の部分にルビを振らないようにコードを追加していく。

「漢字」「仮名」「漢字」に完全一致するか

前回と同様にfullmatchを使って上記の条件に完全一致しているかどうか確認する。

matchOB_kanji_kana_kanji = re.fullmatch(r'(^[一-龥]+)([\u3041 -\u3093]+)([一-龥]+)', origin)

「漢字」と「仮名」に分割する

次にひらがなを基準にして分割する。前回では漢字を基準にしていたがひらがなを基準にした方が処理速度が速いんじゃないかという考えでひらがなで分割するようにした。また分割時に空白ができるので削除する。

# 仮名を基準に分割
origin_split = re.split(r'([\u3041 -\u3093]+)', origin)
# 不要な空白を削除
origin_split = [x.strip() for x in origin_split if x.strip()]

これで

[‘男’, ‘の’, ‘子’]

というリストが作成される。

「読み仮名」を「該当する仮名」を基準に分割する

次に「読み仮名」を「該当する仮名」を基準に分割する。(順序的にこのコードは中に分割基準になる仮名が1つだけ含まれるのを確認した後に挿入する)

# 該当する仮名で分割
kana_split = re.split(u'(' + origin_split[1] + ')', kana)

これで

[‘おとこ’, ‘の’, ‘こ’]

というリストが作成される。

正確にルビが振られるように条件を厳しくする

次に正確にルビが振られるように条件を厳しようと思う。

例えば「木葉の色」(このはのいろ)の場合、分割基準にしたい「の」は複数存在するために一つ目の「の」で分割するか二つ目の「の」で分割するかを正確に処理しきれないので、このような場合はとりあえず無理に分割せずにスルーさせるようにしたい。

少しでも精度を上げるために両端から1文字ずつ削除する。今回は両端に必ず漢字がくるため読み仮名の両端の1文字が分割の基準になる可能性がないため。

そして分割基準になる仮名が1回出現する時のみ振り仮名を分け、それ以外は無視してルビを振るようにする。

# 両端から1文字削除(精度を高めるため)
kana_delete = kana[1:]
kana_delete = kana_delete[:-1]
if kana_delete.count(origin_split[1]) == 1:
    print("<ruby><rb>{0}</rb><rt>{1}</rt></ruby>{2}<ruby><rb>{3}</rb><rt>{4}</rt></ruby>".format(origin_split[0], kana_split[0], origin_split[1], origin_split[2], kana_split[2]), end="")
# 条件を満たさない場合は分割せずにルビを振る
else:
    print(
        "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin, kana), end="")

今回の改修の成果は下記の通り。


スポンサーリンク

改修前

男の子おとこのこ

改修後

おとこ

まとめ

これでまた一歩前進したが、他にもまだ色々な組み合わせがあるので、問題点を紹介しながら改修していきたい。

現状でのコードは下記の通り。

#!/usr/local/bin/python3
# -*- coding: utf_8 -*-
import sys
import MeCab
import re
import jaconv
def henkan(text):
    kana = jaconv.kata2hira(text)
    return kana
def tohensu(origin, kana):
    origin = "".join(origin)
    kana = "".join(kana)
    return origin, kana
def kanadelete(origin, kana):
    origin = list(origin)
    kana = list(kana)
    num1 = len(origin)
    num2 = len(kana)
    okurigana = ""
    if origin[num1-1] == kana[num2-1] and origin[num1-2] == kana[num2-2]:
        okurigana = origin[num1-2]+origin[num1-1]
        origin[num1-1] = ""
        origin[num1-2] = ""
        kana[num2-1] = ""
        kana[num2-2] = ""
        origin, kana = tohensu(origin, kana)
    elif origin[num1-1] == kana[num2-1]:
        okurigana = origin[num1-1]
        origin[num1-1] = ""
        kana[num2-1] = ""
        origin = "".join(origin)
        kana = "".join(kana)
    else:
        origin, kana = tohensu(origin, kana)
    return origin, kana, okurigana
mecab = MeCab.Tagger("-Ochasen")
mecab.parse('')  # 空でパースする必要がある
node = mecab.parseToNode("お正月に埋め立てられた湾岸から大切にしていた男の子の財布が見つかった。")
while node:
    origin = node.surface  # もとの単語を代入
    # アルファベットや数字など読み仮名が存在しない場合にエラーになるので読み仮名が存在する時のみ代入させる
    if node.feature.split(",")[7:]:
        # 読み仮名を代入
        yomi = node.feature.split(",")[7]
        kana = henkan(yomi)
    # 正規表現で漢字と一致するかをチェック
    pattern = "[一-龥]"
    matchOB = re.search(pattern, origin)
    # originが空のとき、漢字以外の時はふりがなを振る必要がないのでそのまま出力する
    if origin != "" and matchOB:
        # 正規表現で「漢字+仮名」かどうかチェック
        matchOB_kanji_kana = re.fullmatch(r'(^[一-龥]+)([\u3041 -\u3093]+)', origin)
        # 正規表現で「仮名+漢字」かどうかチェック
        matchOB_kana_kanji = re.fullmatch(
            r'(^[\u3041 -\u3093]+)([一-龥]+)', origin)
        # 正規表現で「仮名+漢字」かどうかチェック
        matchOB_kanji_kana_kanji = re.fullmatch(
            r'(^[一-龥]+)([\u3041 -\u3093]+)([一-龥]+)', origin)
        # 正規表現で「漢字+仮名」の場合
        if origin != "" and matchOB_kanji_kana:
            # 仮名を基準に分割
            origin_split = re.split(r'([\u3041 -\u3093]+)', origin)
            # 不要な空白を削除
            origin_split = [x.strip() for x in origin_split if x.strip()]
            # 「送り仮名」を含んだ「読み仮名」から「送り仮名」を後方一致で削除する
            kana = kana.rstrip(origin_split[1])
            # それぞれ分割したものをHTMLのタグに挿入する
            print(
                "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin_split[0], kana), end="")
            print(origin_split[1], end="")
        # 正規表現で「仮名+漢字」の場合
        elif origin != "" and matchOB_kana_kanji:
            # 仮名を基準に分割
            origin_split = re.split(r'([\u3041 -\u3093]+)', origin)
            # 不要な空白を削除
            origin_split = [x.strip() for x in origin_split if x.strip()]
            # 「行頭の仮名」を含んだ「読み仮名」から「行頭の仮名」を前方一致で削除する
            kana = kana.lstrip(origin_split[0])
            # それぞれ分割したものをHTMLのタグに挿入する
            print(origin_split[0], end="")
            print(
                "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin_split[1], kana), end="")
        elif origin != "" and matchOB_kanji_kana_kanji:
            # 仮名を基準に分割
            origin_split = re.split(r'([\u3041 -\u3093]+)', origin)
            # 不要な空白を削除
            origin_split = [x.strip() for x in origin_split if x.strip()]
            # 両端から1文字削除(精度を高めるため)
            kana_delete = kana[1:]
            kana_delete = kana_delete[:-1]
            # # 両端から1文字削除したものの中に該当する仮名がいくつ存在するか確認し(2文字以上存在する場合、この処理の正確性が欠けるため)条件を満たす場合のみ分割しルビを振る
            if kana_delete.count(origin_split[1]) == 1:
                # 該当する仮名で分割
                kana_split = re.split(u'(' + origin_split[1] + ')', kana)
                print(
                    "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>{2}<ruby><rb>{3}</rb><rt>{4}</rt></ruby>".format(origin_split[0], kana_split[0], origin_split[1], origin_split[2], kana_split[2]), end="")
            # 条件を満たさない場合は分割せずにルビを振る
            else:
                print(
                    "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin, kana), end="")
        else:
            origin, kana, okurigana = kanadelete(origin, kana)
            print(
                "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin, kana), end="")
            print(okurigana, end="")
    else:
        print(origin, end="")
    node = node.next