1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ルビ付けにおける正規表現による送り仮名の処理

Last updated at Posted at 2021-03-12

最近作っている日本語学習者向けの Telegram BOT @jadicts_bot 「辞書 BOT」一つの機能として、「かな付け」というものがあります。

「まだ六歳とはいえ、世間の目を敏感に感じ取れる年齢にはなっている。」
     (赤川次郎『幽霊愛好会』)

例えば以上の文を以下のように変換することです。

「まだ六歳(ろくさい)とはいえ、世間(せけん)の目(め)を敏感(びんかん)に感(かん)じ取(と)れる年齢(ねんれい)にはなっている。」
  (赤川(あかがわ)次郎(じろう)『幽霊(ゆうれい)愛好会(あいこうかい)』)

問題点その一

形態素解析エンジンを使用して、言葉毎に読み仮名を取得しています。ここに問題となるのは、動詞などの送り仮名、例えば「読む」の「む」の部分が、読み仮名に含まれてしまいます。だから、安直にf"{kanji}({hiragana})"のようにしてしまうと、「読む(よむ)」になってしまいます。別に括弧しか使えない環境なら、このままでも特にどうということもないですが、HTMLのルビなどの形になると<ruby>読む<rt>よむ<rt></ruby>読むよむ」のように気持ち悪くなります。

だから、何かしらの処理をしなければなりません。

甘かった処理

まず考えつきやすいのは、交じり表記とひらがな表記を、後からその最長共通部分列(LCS)を取って、三つの部分(例えば「読む」を「読」「よ」「む」)に分けて処理することです。


def get_okurigana(a: str, b: str) -> str:
    # 順を逆転
    ra, rb = a[::-1], b[::-1]
  # 最長共通部分列を取る
    def _iter():
        for a, b in zip(ra, rb):
            if a == b:
                yield a
            else:
                return
    return ''.join(reversed(list(_iter())))

def format_okurigana(orig: str, hira: str, okurigana: str) -> tuple[str, str, str]:
    # 「読む」を「読」「よ」「む」に分ける
    okurigana_length = len(okurigana)
    return [orig[:-okurigana_length], hira[:-okurigana_length], okurigana]

def process_item(item: dict) -> str:
    # 単語リストを処理する
    if item['orig'] == item['hira'] or item['orig'] == item['kana']:
        # ひらがなやカタカナのみの語をそのまま
        return item['orig']
    elif len(okurigana := get_okurigana(item['orig'], item['hira'])):
        # 送り仮名
        orig, hira, okrgn = format_okurigana(item['orig'], item['hira'], okurigana)
        return f"{ orig }{ hira }{okrgn}"
    else:
        # 漢字のみの語
        return f"{ item['orig'] }{ item['hira'] }"

この処理は、他のところにもよく見る方法だけど、実はとても甘いです。

問題点その二

「まだ六歳とはいえ、世間の目を敏感に感じ取れる年齢にはなっている。」
     (赤川次郎『幽霊愛好会』)

それは何かというと、例えば前にも出てきたこの文の「感じ取れる」という単語。これは日本語の複合動詞で、ほとんどの場合は形態素解析エンジンにとって一語です。だから、「感じ取れる」(かんじとれる)を、前のように処理した場合、「感じ取(かんじと)れる」となります。「読む(よむ)」よりも気持ち悪いですね🤢。

最終の処理

だから色々考えた結果、正規表現を使うことにしました。

目的はあくまでもすべての漢字(の連続)の後に括弧をつけてその読み方を入れるだけだから、だから、必要なのは漢字の読み仮名を取得することだけです。しかしそれを取得するには、漢字と相応の位置にある文字を抽出する必要があります。ここで思いついたのは、例えば"感じ取れる", "かんじとれる"に対して、「感じ取れる」から漢字をり貫いて、「○じ○れる」を「かんじとれる」に嵌めて、「かん」と「と」を取るというプロセスです。

具体的には、逆から行って、「かんじとれる」にはまるような正規表現が必要です。それを作るには、漢字を仮名の正規表現に変える正規表現が必要です。

それは何かというと、漢字の連続を表す\p{Han}+です。これでマッチしたもの(漢字の連続)をひらがなの連続を表す正規表現\p{Hiragana}+に変えて、それを括弧で捕まえます``(\p{Hiragana}+)。

捕まったら、もとの漢字表記の漢字の後の部分に穴を開けて、その中に捕まったものを順に入れればいいわけです。

分かりましたか?実際にコードを書いてみましょう!

※ ここでは、正規表現のユニコード属性(Unicode Property)\p{}を使いたいので、Python の組み込みライブラリーreではなく、拡張されたregexを使います(pip install regex)。


# 漢字の連続をマッチするパターン
KANJI_PATTERN = regex.compile(r"(\p{Han}+)")
def okurigana(orig, hira):
    # e.g. orig=お悔やみ申し上げる, hira=おくやみもうしあげる

  # 漢字の連続を (\p{Hiragana}+) に替える
    new_pattern = KANJI_PATTERN.sub(r"(\p{Hiragana}+)", orig)
    # お(\p{Hiragana}+)やみ(\p{Hiragana}+)し(\p{Hiragana}+)げる

    # 「おくやみもうしあげる」に型を嵌める
    mgroups = regex.match(new_pattern, hira).groups()
    # く, もう, あ

    # 「お悔やみ申し上げる」に穴をあける
    filling = KANJI_PATTERN.sub(r"\1{}", orig)
    # お悔{}やみ申{}し上{}げる

    # 開いた穴に捕まったひらがなを入れる
    return filling.format(*[f"{x}" for x in mgroups])
    # お悔(く)やみ申(もう)し上(あ)げる

これで終わります。

テスト用例

testcases = [
    ("感じ取れる", "かんじとれる"),
    ("お悔やみ", "おくやみ"),
    ("お悔やみ申し上げる", "おくやみもうしあげる"),
    ("お土産", "おみやげ"),
    ("任せる", "まかせる"),
    ("算数", "さんすう")
]
for orig, hira in testcases:
    print(okurigana(orig, hira))

結果

感(かん)じ取(と)れる
お悔(く)やみ
お悔(く)やみ申(もう)し上(あ)げる
お土産(みやげ)
任(まか)せる
算数(さんすう)

終わりに

今回結構曲がり道通ったなと思いました。大量なテストを通さなければ、問題に気づかないし、気づいても解決までの道のりは遠くて曲がっています。しかし、解決法を探す過程自体が、また一つの問題となっていて、パズルを解くための方法を探すというパズルになっていて、解けば解くほど、上手くなるし楽しくなります。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?