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

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

はじめに

前回では「男の子」や「お正月」のように「漢字+仮名+漢字」と「仮名+漢字」の組み合わせの場合に正しいルビが振られるように改修した。今回はPythonとMecabとの組み合わせで実用できるHTML自動リビふりの実現させるために、「埋め立て」と「に関して、に対して」、「漢字+仮名+漢字+仮名」と「仮名+漢字+仮名」のパターンのルビ振りについて改修していきたい。

MeCabの辞書をデフォルトではなく「mecab-ipadic-NEologd」辞書に変更

色々なテキストでふりがなを確認しているとデフォルトの辞書では読み仮名の品質に問題があると判断したのでMeCabの辞書を「mecab-ipadic-NEologd」に変更することにした。例えば「翁長雄志知事」の場合、デフォルトの辞書だと

おう長雄ながおこころざし知事ちじ

というとんでもな読み仮名になり時代に追いつけていない。なのでMeCabでの拡張辞書では有名な「mecab-ipadic-NEologd」を使用することにした。これで

翁長雄志知事おながたけしちじ

という風に正しくふりがなが振られた。

しかし、辞書の置き換えは、これはこれでまた問題がある。今までは別々に解析されていたものが一つの名詞として解されることがあること。例えば「月は東に日は西に」。なんかアニメのタイトルみたいなんだけど、置き換えた辞書では一つの名詞になるためデフォルトの辞書ではちゃんと

つきひがし西にし

となるが、拡張した辞書では

月は東に日は西つきはひがしにひはにし

となるのだ。うーん、実に面倒だ。この問題の解決は、また今度にしておく。
ということで辞書の指定を変更したので、「mecab-ipadic-neologd」をインストールし、コードを一部書き換えた。

mecab = MeCab.Tagger("-Ochasen")

を下記のように。

mecab = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd -Ochasen")

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

例えば「埋め立て」のような「漢字」+「仮名」+「漢字」+「仮名」の場合に適切なふりがなが自動でルビ振りされるようにする。ロジック的に前回のものと同様なのでざっとだけコードを載せておく。また、前回までのも含めて関数化しておいた。

def kanji_kana_kanji_kana(origin,kana):
    # 仮名を基準に分割
    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:]
    # 行頭から1文字削除したものの中に該当する仮名がいくつ存在するか確認し(2文字以上存在する場合、この処理の正確性が欠けるため)条件を満たす場合のみ分割しルビを振る
    if kana_delete.count(origin_split[1]) == 1 and kana_delete.count(origin_split[3]) == 1:
        kana_split = re.split(u'(' + origin_split[1] + '|' + origin_split[3] + ')', kana)
        kana_split = [x.strip() for x in kana_split if x.strip()]
        # print(kana_split)
        # print(origin_split)
        print(
            "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>{2}<ruby><rb>{3}</rb><rt>{4}</rt></ruby>{5}".format(origin_split[0], kana_split[0], origin_split[1], origin_split[2], kana_split[2], origin_split[3]), end="")
    # 条件を満たさない場合は分割せずにルビを振る
    else:
        print(
            "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin, kana), end="")

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

こちらは「に関して」や「に対して」という場合に適切なふりがなが自動でルビ振りされるようにする。これもロジック的に以前のものと同じなのでまとめた関数を載せておく。

def kana_kanji_kana(origin, kana):
    # 仮名を基準に分割
    origin_split = re.split(r'([\u3041 -\u3093]+)', origin)
    # 不要な空白を削除
    origin_split = [x.strip() for x in origin_split if x.strip()]
    print(origin_split)
    # 読み仮名の行頭から始めの仮名を削除
    kana = kana.lstrip(origin_split[0])
    # 読み仮名の行末から最後の仮名を削除
    kana = kana.rstrip(origin_split[2])
    print(
        "{0}<ruby><rb>{1}</rb><rt>{2}</rt></ruby>{3}".format(origin_split[0], origin_split[1], kana, origin_split[2]), end="")

今回の改修の成果

改修前

埋め立うめたに関にかんして会議かいぎおこな

改修後

てにかんして会議かいぎおこな

まとめ

PythonとMeCabでの自動ルビふりを実現するのにまたゴールに一歩近ずいたように思えたが、また新しい問題があった。今までのコードだとカタカナの処理が想定されていないので、場合によってはカタカナ部分にもよくルビが振られてしまっている。次回はそこらへんを改修していきたい。

現状でのコードは下記の通り。関数にまとめて整理しているので前回のコードから大きく変わっているのでご注意を。

#!/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
def kanji_kana(origin, 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="")
def kana_kanji(origin, kana):
    # 仮名を基準に分割
    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="")
def kanji_kana_kanji(origin, kana):
    # 漢字を基準に分割
    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="")
def kanji_kana_kanji_kana(origin, kana):
    # 仮名を基準に分割
    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:]
    # 行頭から1文字削除したものの中に該当する仮名がいくつ存在するか確認し(2文字以上存在する場合、この処理の正確性が欠けるため)条件を満たす場合のみ分割しルビを振る
    if kana_delete.count(origin_split[1]) == 1 and kana_delete.count(origin_split[3]) == 1:
        kana_split = re.split(
            u'(' + origin_split[1] + '|' + origin_split[3] + ')', kana)
        kana_split = [x.strip() for x in kana_split if x.strip()]
        # print(kana_split)
        # print(origin_split)
        print(
            "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>{2}<ruby><rb>{3}</rb><rt>{4}</rt></ruby>{5}".format(origin_split[0], kana_split[0], origin_split[1], origin_split[2], kana_split[2], origin_split[3]), end="")
    # 条件を満たさない場合は分割せずにルビを振る
    else:
        print(
            "<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin, kana), end="")
def kana_kanji_kana(origin, kana):
    # 仮名を基準に分割
    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])
    # 読み仮名の行末から最後の仮名を削除
    kana = kana.rstrip(origin_split[2])
    print(
        "{0}<ruby><rb>{1}</rb><rt>{2}</rt></ruby>{3}".format(origin_split[0], origin_split[1], kana, origin_split[2]), end="")
# mecab = MeCab.Tagger("-Ochasen")
mecab = MeCab.Tagger(
    "-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd -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)
        # 正規表現で「漢字+仮名+漢字+仮名」かどうかチェック
        matchOB_kanji_kana_kanji_kana = re.fullmatch(
            r'(^[一-龥]+)([\u3041 -\u3093]+)([一-龥]+)([\u3041 -\u3093]+)', origin)
        # 正規表現で「仮名+漢字+仮名」かどうかチェック
        matchOB_kana_kanji_kana = re.fullmatch(
            r'(^[\u3041 -\u3093]+)([一-龥]+)([\u3041 -\u3093]+)', origin)
        if origin != "" and matchOB_kanji_kana:
            kanji_kana(origin, kana)
        # 正規表現で「仮名+漢字」の場合
        elif origin != "" and matchOB_kana_kanji:
            kana_kanji(origin, kana)
        elif origin != "" and matchOB_kanji_kana_kanji:
            kanji_kana_kanji(origin, kana)
        elif origin != "" and matchOB_kanji_kana_kanji_kana:
            kanji_kana_kanji_kana(origin, kana)
        elif origin != "" and matchOB_kana_kanji_kana:
            kana_kanji_kana(origin, kana)
        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