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

投稿者: | 2018年6月20日

はじめに

前回では大本となるコードの紹介をした。今回からは大本のコードを修正しながらPythonとMecabとの組み合わせで実用できるHTML自動リビふりの実現を目標に「漢字交じり」の単語・熟語に適切な「ふりがな」が振られるようにロジックを考えていきたい。

では、前回紹介したコードを元に進めていこう。ちなみにPythonは初心者です。

またPythonはPython3系を想定している。

ひらがな変換にjaconvを

コード中にMeCabから拾ってきた読み仮名をカタカナからひらがなに変換する関数があるが、これをjaconvのモジュールに置き換える。置き換える理由としては処理スピードはこちらの方がはい早そうだから。実際に実測速度を計測したが、やはりわずかながらjaconvの方が速かった。

インストールして方は下記のコマンドで。環境によっては「pip3」ではなく「pip」の場合も。

pip3 install jaconv

ひらがな変換をjaconvに置き換える

まずはモジュールを追加させる。

import jaconv

6行目の「def henkan(text) 」部分をごっそり下記のように書き換えた。

def henkan(text) :
    kana=jaconv.kata2hira(text)
    return kana

先頭がひらがなでもルビを振る

現状だと文字の先頭が漢字かどうかで判別されているので、これを感じが含まれているかどうかというように処理させたい。

51行目にある

matchOB = re.match(pattern , origin)

matchOB = re.search(pattern , origin)

に変更する。これでルビが振られなかった「お正月」も

お正月おしょうがつ

とルビが振られるようになった。

送り仮名のコードを修正

次に送り仮名の処理を改善しようと思う。現状だと2文字分の送り仮名にしか対応できていないので下記のようになってしまう。

見つみつかっていない

MeCab的には「見つかっていない」は「見つかっ | て | い | ない」と解析されているので「見つかっ」では後ろ2文字の「かっ」のみが送り仮名として処理されている。そこで、今回は送り仮名が何文字であっても適切に処理されるように正規表現で「漢字 | 送り仮名」に分割し、後方一致で「読み仮名」から「送り仮名」を削除し漢字の読み仮名部分を抜き出したいと思う。

正規表現で「漢字+仮名」かどうかチェック

下記の処理で「漢字(複数文字想定)」(^) + 「仮名(複数文字想定)」([\u3041 -\u3093])を完全一致でマッチさせる。
完全一致でマッチさせたいので「fullmatch」を使う。

# 正規表現で「漢字+仮名」かどうかチェック
matchOB_kanji_kana = re.fullmatch(r'(^[一-龥]+)([\u3041 -\u3093]+)', origin)

「漢字+仮名」の場合に処理を進め漢字を基準にして分割する

次に上記の条件で完全に一致した場合、漢字を基準に文字を分割する。

# 正規表現で「漢字+仮名」の場合
if origin != "" and matchOB_kanji_kana:
    # 漢字を基準に分割
    origin_split = re.split(u'([一-龥]+)', origin)

すると [”, ‘見’, ‘つかっ’] のリストが作成される。
空白部分が作成されるので削除する。

# 不要な空白を削除
    origin_split = [x.strip() for x in origin_split if x.strip()]

空白部分が消え、[‘見’, ‘つかっ’]というリストができる。

「送り仮名」を含んだ「読み仮名」から「送り仮名」を後方一致で削除する

次に「送り仮名」を含んだ「読み仮名」から「送り仮名」を後方一致で削除する。
削除対象は「kana」の変数で、先ほど作成したリスト「origin_split」に2番目のリスト「origin_split[1]」を使って後方一致で削除する。

# 「送り仮名」を含んだ「読み仮名」から「送り仮名」を後方一致で削除する
kana = kana.rstrip(origin_split[1])

それぞれ分割したものをHTMLのタグに挿入する

「漢字」、「読み仮名」、「送り仮名」に分割できたのでこれらをHTMLのタグに挿入する。
この場合、「漢字」は「origin_split[0]」、「読み仮名」は「kana」、「送り仮名」は「origin_split[1]」となる。

# それぞれ分割したものをHTMLのタグに挿入する
print("<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin_split[0], kana), end="")
print(origin_split[1], end="")

そうすると


スポンサーリンク
<ruby><rb>見</rb><rt>み</rt></ruby>つかっていない

というHTMLが作成され、表示させたのが下記となる

つかっていない

これらをまとめると下記のコードとなる。

# 正規表現で「漢字+仮名」かどうかチェック
matchOB_kanji_kana = re.fullmatch(r'(^[一-龥]+)([\u3041 -\u3093]+)', origin)
# 正規表現で「漢字+仮名」の場合
if origin != "" and matchOB_kanji_kana:
    # 漢字を基準に分割
    origin_split = re.split(u'([一-龥]+)', 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="")

頭文字がひらがなでも適切な読み仮名が振られるようにする

次に「お正月」のように「仮名+漢字」の適切な読み仮名が振られるようにする。手順としては先ほどのコードの逆となる。

処理が前後逆になるだけなので詳細は省くが、まとめるとコードは下記のようになる。先ほどの「if」から継続した条件になるので「elif」となる

# 正規表現で「仮名+漢字」かどうかチェック
matchOB_kana_kanji = re.fullmatch(
    r'(^[\u3041 -\u3093]+)([一-龥]+)', origin)
(中略)
# 正規表現で「仮名+漢字」の場合
elif origin != "" and matchOB_kana_kanji:
    # 漢字を基準に分割
    origin_split = re.split(u'([一-龥]+)', 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="")

まとめ

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

改修前

お正月にかれ大切たいせつにしていた財布さいふ見つみつかった。

改修後

正月しょうがつかれ大切たいせつにしていた財布さいふつかった

これで「漢字+仮名」、「仮名+漢字」にも適切にルビが振られるようになったが、全てのルビを正確に振るようようにするには、まだまだ解決しなければならないことがたくさんあるので、継続して改修していきたい。

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

#!/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("お正月に彼の大切にしていた財布が見つかった。")
# node = mecab.parseToNode("彼の財布はまだ見つかっていない")
while node:
    origin = node.surface  # もとの単語を代入
    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)
        # 正規表現で「漢字+仮名」の場合
        if origin != "" and matchOB_kanji_kana:
            # 漢字を基準に分割
            origin_split = re.split(u'([一-龥]+)', 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(u'([一-龥]+)', 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="")
        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

ではでは。