Pythonで
日本語処理 入門

〜フリガナプログラムを作ろう〜

Takanori Suzuki

みんなのPython勉強会#101 / 2024 Feb 22

アジェンダ 📋

  • 自然言語(日本語)処理とは

    • 形態素解析 について

  • Janome でフリガナプログラム

  • SudachiPy でフリガナプログラム

  • LLM(大規模言語モデル)については 話しません

ゴール 🥅

  • 自然言語処理がどういうものか知る

  • JanomeまたはSudachiPyを使った日本語処理ができそう

Photos 📷 Tweets 🐦 👍

#stapy / @takanory

Slides / スライド 💻

slides.takanory.net

takanory slides

Who am I? / お前 誰よ 👤

takanory profile kuro-chan and kuri-chan

PyCon JP Association 🐍

日本国内のPythonユーザのために、Pythonの普及及び開発支援を行うために、継続的にカンファレンス(PyCon)を開くことを目的とした 非営利組織

pycon jp logo

PyCon JP Associationの 主な活動

PyCon JP 2024 🇯🇵

BeProud Inc. 🏢

  • BeProud: Pythonシステム開発、コンサル

  • connpass: IT勉強会支援プラットフォーム

  • PyQ: Python独学プラットフォーム

  • TRACERY: システム開発ドキュメントサービス

BeProud logos

Pythonプロフェッショナル
プログラミング 第4版

  • 2024年2月16日発売(5年ぶりに大幅改訂)

  • 468ページ、3,300円(税込)

  • チーム開発 に必須の プロの基礎知識 !!

Pythonプロフェッショナルプログラミング 第4版

自然言語処理 とは 🗣️

自然言語処理 とは 🗣️

  • NLP(Natural Language Processing)

  • 自然言語(日本語、英語等)は厳格な構文がない

    • Pythonは言語仕様があるので機械的に処理がしやすい

  • NLP(自然言語処理)用のライブラリが必要

NLPライブラリ

日本語 の特徴

  • 単語が スペースで区切られていない

    • 「すもももももももものうち」

  • 文脈で 単語の分かれ目 が違う

    • 「東京都と神奈川の小京都」

  • 一つの漢字に 複数の読み方

    • 「一月一日は元日で昨日は大晦日」

単語に分割して情報を取得

  • 日本語を単語に分割する

    • 「すもも/も/もも/も/もも/の/うち」

    • 「東京/都/と/神奈川/の/小/京都」

  • 読みの情報を取得する

    • 「いちがつ/ついたち/は/がんじつ…」

  • 形態素解析

形態素解析 とは 💬

形態素解析 とは 💬

  • 自然言語(日本語)を 形態素 に分割

    • 形態素=単語などの要素

  • 品詞 などの情報を付加

  • 日本語の 辞書 が必要

品詞、原形、読み

  • 形態素解析が付加する主な情報

  • 「とても美味しいビールを飲みたい」

    • 品詞: とても(副詞)美味しい(形容詞)ビール(名詞)…

    • 原形: 飲み→飲む

    • 読み: 美味しい→おいしい、飲み→のみ

形態素解析の用途

  • 検索エンジンの検索インデックス

  • 文章の分類

  • 単語の数で文章の特徴を表す(Bag of Words)

  • 重要な単語に重み付けする(TF-IDF)

形態素解析を利用した プログラム

  • 文章にフリガナを振るプログラムを作る

形態素解析を利用した プログラム

  • 実行イメージ(HTMLの ruby タグを使用)

$ ./furigana.py "美味しい麦酒を飲もう" > result.html && cat result.html
<ruby><rb>美味</rb><rt>おい</rt></ruby>しい
<ruby><rb>麦酒</rb><rt>びーる</rt></ruby>を
<ruby><rb>飲</rb><rt>の</rt></ruby>もう

result

Janome で形態素解析 👀

Janome とは

  • URL: mocobeta.github.io/janome/

  • Pure Python で書かれた 辞書内包 の形態素解析器

    • OSに依存しない

    • すぐ使い始められる

Janomeをインストール

  • pip install janome でインストール

$ python3.11 -m venv env  # venvモジュールで仮想環境作成
$ . env/bin/activate
(env) $ pip install janome
...
Successfully installed janome-0.5.0

Janomeで形態素解析

  • janome コマンドで形態素解析

(env) $ echo "美味しい麦酒を飲もう" | janome
美味しい	形容詞,自立,*,*,形容詞・イ段,基本形,美味しい,オイシイ,オイシイ
麦酒	名詞,一般,*,*,*,*,麦酒,ビール,ビール
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
飲も	動詞,自立,*,*,五段・マ行,未然ウ接続,飲む,ノモ,ノモ
う	助動詞,*,*,*,不変化型,基本形,う,ウ,ウ

形態素解析の結果

  • 「表層形 品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音」の形式

美味しい	形容詞,自立,*,*,形容詞・イ段,基本形,美味しい,オイシイ,オイシイ
麦酒	名詞,一般,*,*,*,*,麦酒,ビール,ビール
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
飲も	動詞,自立,*,*,五段・マ行,未然ウ接続,飲む,ノモ,ノモ
う	助動詞,*,*,*,不変化型,基本形,う,ウ,ウ

プログラムで形態素解析

(env) $ python
>>> from janome.tokenizer import Tokenizer
>>> t = Tokenizer()  # トークナイザーを生成
>>> for token in t.tokenize("美味しい麦酒を飲もう"):
...     print(token)
... 
美味しい	形容詞,自立,*,*,形容詞・イ段,基本形,美味しい,オイシイ,オイシイ
麦酒	名詞,一般,*,*,*,*,麦酒,ビール,ビール
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
飲も	動詞,自立,*,*,五段・マ行,未然ウ接続,飲む,ノモ,ノモ
う	助動詞,*,*,*,不変化型,基本形,う,ウ,ウ

Janomeで分かち書き

  • tokenize() メソッドで分かち書きモード(wakati=True)を指定

>>> tokens = t.tokenize("美味しい麦酒を飲もう", wakati=True)
>>> tokens
<generator object Tokenizer.__tokenize_stream at 0x10055e9d0>
>>> list(tokens)
['美味しい', '麦酒', 'を', '飲も', 'う']
>>> list(t.tokenize("すもももももももものうち", wakati=True))
['すもも', 'も', 'もも', 'も', 'もも', 'の', 'うち']

読みなどの情報を取得

>>> tokens = list(t.tokenize("飲もう"))
>>> tokens[0].surface  # 表層形
'飲も'
>>> tokens[0].part_of_speech  # 品詞情報
'動詞,自立,*,*'
>>> tokens[0].base_form  # 原形
'飲む'
>>> tokens[0].reading  # 読み
'ノモ'
>>> tokens[0].phonetic  # 発音
'ノモ'
>>> tokens = list(t.tokenize("縮む"))  # 読みと発音が異なる例
>>> tokens[0].reading, tokens[0].phonetic
('チヂム', 'チジム')

Janomeで フリガナ 🖊️

Janomeで フリガナ 🖊️

  • 形態素(トークン)の 表層形読み を取得

>>> from janome.tokenizer import Tokenizer
>>> t = Tokenizer()
>>> for token in t.tokenize("美味しい麦酒を飲もう"):
...     token.surface, token.reading  # 表層形, 読み
... 
('美味しい', 'オイシイ')
('麦酒', 'ビール')
('を', 'ヲ')
('飲も', 'ノモ')
('う', 'ウ')

Janomeで フリガナ 🖊️

  • surface (表層形)と reading (読み)を使用

import sys
from janome.tokenizer import Tokenizer

def furigana(s: str) -> str:
    """文字列にフリガナを振ったHTMLを返す"""
    t = Tokenizer()
    result = ""
    for token in t.tokenize(s):
        result += (f"<ruby><rb>{token.surface}</rb>"
                   f"<rt>{token.reading}</rt></ruby>")
    return result

if __name__ == "__main__":
    print(furigana(sys.argv[1]))

Janomeで フリガナ 🖊️

  • すべての文字にフリガナが振られている

(env) $ python furigana1.py "美味しい麦酒を飲もう"
<ruby><rb>美味しい</rb><rt>オイシイ</rt></ruby><ruby><rb>麦酒</rb><rt>ビール</rt></ruby><ruby><rb>を</rb><rt>ヲ</rt></ruby><ruby><rb>飲も</rb><rt>ノモ</rt></ruby><ruby><rb>う</rb><rt>ウ</rt></ruby>

実行結果1

フリガナを ひらがな にする

  • jaconvを使用して ひらがなに変換

(env) $ pip install jaconv
import sys
from jaconv import kata2hira  # カタカナをひらがなに変換
from janome.tokenizer import Tokenizer

def furigana(s: str) -> str:
    """文字列にフリガナを振ったHTMLを返す"""
    t = Tokenizer()
    result = ""
    for token in t.tokenize(s):
        result += (f"<ruby><rb>{token.surface}</rb>"
                   f"<rt>{kata2hira(token.reading)}</rt></ruby>")
    return result

フリガナを ひらがな にする

  • フリガナが ひらがな になった

(env) $ python furigana2.py "美味しい麦酒を飲もう"
<ruby><rb>美味しい</rb><rt>おいしい</rt></ruby><ruby><rb>麦酒</rb><rt>びーる</rt></ruby><ruby><rb>を</rb><rt>を</rt></ruby><ruby><rb>飲も</rb><rt>のも</rt></ruby><ruby><rb>う</rb><rt>う</rt></ruby>

フリガナがひらがなに

漢字が含まれる場合のみを対象に

  • surface漢字が含まれる 場合のみ対象

  • 漢字を表す正規表現を定義

import re

KANJI = r"[\u3005-\u3007\u4E00-\u9FFF]"  # 漢字を表す正規表現

漢字が含まれる場合のみを対象に

  • 正規表現で漢字を含むかをチェック

def furigana(s: str) -> str:
    """文字列にフリガナを振ったHTMLを返す"""
    t = Tokenizer()
    result = ""
    for token in t.tokenize(s):
        if re.search(KANJI, token.surface):  # 漢字か?
            result += (f"<ruby><rb>{token.surface}</rb>"
                       f"<rt>{kata2hira(token.reading)}</rt></ruby>")
        else:
            result += token.surface
    return result

漢字が含まれる場合のみを対象に

  • 「を」「う」の フリガナが消えた

(env) $ python furigana3.py "美味しい麦酒を飲もう"
<ruby><rb>美味しい</rb><rt>おいしい</rt></ruby><ruby><rb>麦酒</rb><rt>びーる</rt></ruby>を<ruby><rb>飲も</rb><rt>のも</rt></ruby>う

「を」「う」のフリガナが消えた

送りがなに対応

  • 「美味しい」の「美味」のみにフリガナ

  • ruby() 関数を作成し 送りがな処理 を追加

KANA = r"[\u3041-\u309F]+$"  # 末尾のひらがなを表す正規表現

def ruby(kanji: str, kana: str) -> str:
    """1つの単語にフリガナを振る"""
    hira = kata2hira(kana)
    okuri = ""
    if m := re.search(KANA, kanji):
        okuri = m[0]
        kanji = kanji.removesuffix(okuri)  # 送り仮名を削除
        hira = hira.removesuffix(okuri)  # 送り仮名を削除
    return f"<ruby><rb>{kanji}</rb><rt>{hira}</rt></ruby>{okuri}"

送りがなに対応

  • ruby() 関数を呼び出すように変更

def furigana(s: str) -> str:
    """文字列にフリガナを振ったHTMLを返す"""
    t = Tokenizer()
    result = ""
    for token in t.tokenize(s):
        if re.search(KANJI, token.surface):  # 漢字か?
            result += ruby(token.surface, token.reading)
        else:
            result += token.surface
    return result

送りがなに対応

  • 送りがな が処理できるようになった!

(env) $ python furigana4.py "美味しい麦酒を飲もう"
<ruby><rb>美味</rb><rt>おい</rt></ruby>しい<ruby><rb>麦酒</rb><rt>びーる</rt></ruby>を<ruby><rb>飲</rb><rt>の</rt></ruby>もう

送りがなに対応

だがまだ完璧ではない

辞書 をカスタマイズ 📕

想定したフリガナにならない

  • 新出(しんしゅつ)漢字

  • 後付け(あとづけ)

(env) $ echo "新出漢字、後付け" | janome
新出	名詞,固有名詞,人名,姓,*,*,新出,ニイデ,ニーデ
漢字	名詞,一般,*,*,*,*,漢字,カンジ,カンジ
、	記号,読点,*,*,*,*,、,、,、
後	接頭詞,名詞接続,*,*,*,*,後,コウ,コー
付け	名詞,一般,*,*,*,*,付け,ツケ,ツケ

ユーザー定義辞書を使用

ユーザー定義辞書を使用

  • ユーザー定義辞書(janome_dict.csv)

新出,カスタム名詞,シンシュツ
後付け,カスタム名詞,アトヅケ
  • Tokenizer() の引数に辞書を指定

def furigana(s: str) -> str:
    """文字列にフリガナを振ったHTMLを返す"""
    t = Tokenizer("janome_dict.csv", udic_type="simpledic")
    result = ""

ユーザー定義辞書を使用

  • ユーザー定義辞書 で結果をカスタマイズ

(env) $ python furigana5.py "新出漢字を後付けする"    
<ruby><rb>新出</rb><rt>しんしゅつ</rt></ruby><ruby><rb>漢字</rb><rt>かんじ</rt></ruby>を<ruby><rb>後付</rb><rt>あとづ</rt></ruby>けする

ユーザー定義辞書を利用

Janomeの辞書の 問題点

  • mecab-ipadic-2.7.0 ベースの辞書

  • 「後付け」など登録されていない語が多い

  • 新しい単語が入っていない

    • よりよい辞書を持つライブラリへ

SudachiPy で形態素解析 🍊

SudachiPyとは

SudachiPyをインストール

  • 辞書は smallcorefull の3種類

  • ここでは core をインストール

(env) $ pip install sudachidict_core sudachipy
...
Successfully installed sudachidict_core-20240109 sudachipy-0.6.8

SudachiPyで形態素解析

  • sudachipy コマンドで形態素解析

  • 読みを確認するには -a オプション

(env) $ echo "美味しい麦酒を飲もう" | sudachipy
美味しい	形容詞,一般,*,*,形容詞,連体形-一般	美味しい
麦酒	名詞,普通名詞,一般,*,*,*	麦酒
を	助詞,格助詞,*,*,*,*	を
飲もう	動詞,一般,*,*,五段-マ行,意志推量形	飲む
EOS
(env) $ echo "美味しい麦酒を飲もう" | sudachipy -a 
美味しい	形容詞,一般,*,*,形容詞,連体形-一般	美味しい	美味しい	オイシイ	0	[6880]
麦酒	名詞,普通名詞,一般,*,*,*	麦酒	麦酒	ビール	0	[649]
を	助詞,格助詞,*,*,*,*	を	を	ヲ	0	[]
飲もう	動詞,一般,*,*,五段-マ行,意志推量形	飲む	飲む	ノモウ	0	[]
EOS

プログラムで形態素解析

(env) $  python
>>> from sudachipy import Dictionary
>>> tokenizer = Dictionary().create()
>>> for token in tokenizer.tokenize("美味しい麦酒を飲もう"):
...     print(token)
... 
美味しい
麦酒
を
飲もう

読みなどの任意の情報を取得

>>> tokens = list(tokenizer.tokenize("飲もう"))
>>> tokens[0].surface()  # 表層形
'飲もう'
>>> tokens[0].part_of_speech()  # 品詞情報
('動詞', '一般', '*', '*', '五段-マ行', '意志推量形')
>>> tokens[0].reading_form()  # 読み
'ノモウ'
>>> tokens[0].dictionary_form()  # 原形
'飲む'

SudachiPyで分かち書き

  • 表層系(surface())のリストを作成すれば分かち書きに

(env) $  python
>>> from sudachipy import Dictionary
>>> tokenizer = Dictionary().create()
>>> tokens = tokenizer.tokenize("美味しい麦酒を飲もう")
>>> [token.surface() for token in tokens]
['美味しい', '麦酒', 'を', '飲もう']

SudachiPyで分かち書き

  • 3種類の分割モード(Cがデフォルト)

>>> from sudachipy import Dictionary, SplitMode
>>> tokenizer = Dictionary().create()
>>> s = "高輪ゲートウェイ駅から国会議事堂前駅に向かう"
>>> for mode in SplitMode.A, SplitMode.B, SplitMode.C:
...     [t.surface() for t in tokenizer.tokenize(s, mode)]
... 
['高輪', 'ゲートウェイ', '駅', 'から', '国会', '議事', '堂', '前', '駅', 'に', '向かう']
['高輪', 'ゲートウェイ', '駅', 'から', '国会議事堂前', '駅', 'に', '向かう']
['高輪ゲートウェイ駅', 'から', '国会議事堂前駅', 'に', '向かう']

単語の正規化にも対応

  • normalized_form() で単語を正規化

  • 表記揺れ対策に使えるかも

>>> from sudachipy import Dictionary, SplitMode
>>> tokenizer = Dictionary().create()
>>> for word in ("Vacation", "ヴァイオリン", "亜細亜",
...              "シュミレーション", "國", "たとえば"):
...     tokenizer.tokenize(word, mode)[0].normalized_form()
...
'バケーション'
'バイオリン'
'アジア'
'シミュレーション'
'国'
'例えば'

SudachiPyで フリガナ 🖊️

SudachiPyで フリガナ 🖊️

  • JanomeからSudachiPyに書き換え

from sudachipy import dictionary

def furigana(s: str) -> str:
    """文字列にフリガナを振ったHTMLを返す"""
    t = dictionary.Dictionary().create()
    result = ""
    for token in t.tokenize(s):
        surface = token.surface()
        if re.search(KANJI, surface):  # 漢字か?
            result += ruby(surface, token.reading_form())
        else:
            result += surface
    return result

SudachiPyで フリガナ 🖊️

  • 同じ結果が得られる

  • 新出漢字、後付けも デフォルト辞書 で対応

(env) $ python furigana6.py "美味しい麦酒を飲もう。新出漢字を後付けする"

SudachiPyでフリガナ

辞書を切り替え

  • full の辞書は 雑多な固有名詞 が増えている

  • -s オプションで 辞書の切り替え が可能

(env) $ pip install sudachidict_full
(env) $ echo "僕のヒーローアカデミア" | sudachipy
僕	代名詞,*,*,*,*,*	僕
の	助詞,格助詞,*,*,*,*	の
ヒーロー	名詞,普通名詞,一般,*,*,*	ヒーロー
アカデミア	名詞,普通名詞,一般,*,*,*	アカデミア
EOS
(env) $ echo "僕のヒーローアカデミア" | sudachipy -s full
僕のヒーローアカデミア	名詞,固有名詞,一般,*,*,*	僕のヒーローアカデミア
EOS

辞書を切り替え

  • Dictionary() に引数 dict="full" を指定

def furigana(s: str) -> str:
    """文字列にフリガナを振ったHTMLを返す"""
    t = dictionary.Dictionary(dict="full").create()
    result = ""
    for token in t.tokenize(s):
        surface = token.surface()
        if re.search(KANJI, surface):  # 漢字か?
            result += ruby(surface, token.reading_form())
        else:
            result += surface
    return result

フリガナのさらなる 改善

対応できていないパターン

  • [漢字]+[ひらがな]+ のパターンのみに対応

  • 途中 にひらがながあると対応できない

    • 例: 追い出す、しみ込む、立ち入り禁止

  • カタカナ にもフリガナを振っている

    • 例: アフリカ大陸、東アジア

対応できていないパターン

  • 適切なフリガナにならない

$ python furigana7.py "追い出す、しみ込む、立ち入り禁止。アフリカ大陸と東アジア"
<ruby><rb>追い出</rb><rt>おいだ</rt></ruby>す、<ruby><rb>しみ込</rb><rt>しみこ</rt></ruby>む、<ruby><rb>立ち入り禁止</rb><rt>たちいりきんし</rt></ruby>。<ruby><rb>アフリカ大陸</rb><rt>あふりかたいりく</rt></ruby>と<ruby><rb>東アジア</rb><rt>ひがしあじあ</rt></ruby>

対応できていないパターン

doctestを追加

  • doctestでdocstringに対話的なテストを記述

def ruby(kanji: str, kana: str) -> str:
    """1つの単語にフリガナを振る
    >>> ruby("麦酒", "びーる")
    '<ruby><rb>麦酒</rb><rt>びーる</rt></ruby>'
    >>> ruby("飲もう", "のもう")
    '<ruby><rb>飲</rb><rt>の</rt></ruby>もう'
    >>> ruby("追い出す", "おいだす")
    '<ruby><rb>追</rb><rt>お</rt></ruby>い<ruby><rb>出</rb><rt>だ</rt></ruby>す'
    >>> ruby("しみ込む", "しみこむ")
    'しみ<ruby><rb>込</rb><rt>こ</rt></ruby>む'
    >>> ruby("立ち入り禁止", "たちいりきんし")
    '<ruby><rb>立</rb><rt>た</rt></ruby>ち<ruby><rb>入</rb><rt>い</rt></ruby>り<ruby><rb>禁止</rb><rt>きんし</rt></ruby>'
    >>> ruby("東アジア", "ひがしあじあ")
    '<ruby><rb>東</rb><rt>ひがし</rt></ruby>アジア'
    """

doctestを実行

  • python -m doctest でテストを実行

  • 4/6件のテストが失敗

(env) $ python -m doctest furigana8.py
**********************************************************************
File "/Users/takanori/.../furigana8.py", line 16, in furigana8.ruby
Failed example:
    ruby("追い出す", "おいだす")
Expected:
    '<ruby><rb>追</rb><rt>お</rt></ruby>い<ruby><rb>出</rb><rt>だ</rt></ruby>す'
Got:
    '<ruby><rb>追い出</rb><rt>おいだ</rt></ruby>す'
...
1 items had failures:
   4 of   6 in furigana8.ruby

フリガナ処理を改善

  • ひらがなとカタカナに対応

  • make_ruby() 関数を追加

KANA = r"[\u3041-\u309F\u30A1-\u30FF]+"  # ひらがなとカタカナを表す正規表現

def make_ruby(kanji: str, furi: str) -> str:
    """rubyタグを生成して返す"""
    return f"<ruby><rb>{kanji}</rb><rt>{furi}</rt></ruby>"

フリガナ処理を改善

  • かなの前後で分割して、フリガナ処理を改善

def ruby(kanji: str, kana: str) -> str:
    hira = kata2hira(kana)
    text = ""
    while m := re.search(KANA, kanji):  # kanjiの中のすべてのかな
        okuri = m[0]
        index = hira.find(kata2hira(okuri), m.start())  # 最初のかなの位置
        furigana = hira[:index]
        hira = hira[index + len(okuri):]  # 残りのふりがな
        f_kanji, kanji = kanji.split(okuri, 1)  # kanjiを送りがなで分割
        if furigana:
            text += make_ruby(f_kanji, furigana)
        text += okuri  # 送りがなを追加
    if kanji:  # 漢字が残っている場合
        text += make_ruby(kanji, hira)
    return text

doctestと実行結果を確認

  • 6件のテストに成功(なにも出力されない)

(env) $ python -m doctest furigana9.py
(env) $ python furigana9.py "追い出す、しみ込む、立ち入り禁止。アフリカ大陸と東アジア"
<ruby><rb>追</rb><rt>お</rt></ruby>い<ruby><rb>出</rb><rt>だ</rt></ruby>す、しみ<ruby><rb>込</rb><rt>こ</rt></ruby>む、<ruby><rb>立</rb><rt>た</rt></ruby>ち<ruby><rb>入</rb><rt>い</rt></ruby>り<ruby><rb>禁止</rb><rt>きんし</rt></ruby>。アフリカ<ruby><rb>大陸</rb><rt>たいりく</rt></ruby>と<ruby><rb>東</rb><rt>ひがし</rt></ruby>アジア

改善されたフリガナ

フリガナレベル対応 🏫

フリガナレベル対応 🏫

別表 学年別漢字配当表

学年別漢字配当表の HTMLを確認

  • <td></td> の間を抜き出せば使えそう

<tr>
<th valign="top">第一学年</th>
<td>一 右 雨 円 王 音 下 火 花...力 林 六(80字)</td>
</tr>
<tr>
<th valign="top" scope="row">第二学年</th>
<td>引 羽 雲 園 遠 何 科 夏 家...里 理 話(160字)</td>
</tr>

学年別漢字配当表を スクレイピング

kanji_grade.py
import re
import json
from urllib import request

def main():
    """学年別漢字配当表をJSON形式で保存"""
    URL = "https://www.mext.go.jp/a_menu/shotou/new-cs/youryou/syo/koku/001.htm"
    kanji_grade = []
    with request.urlopen(URL) as f:
        for line in f:
            if m := re.match(r"<td>(.*)(\d+字)</td>", line.decode("utf-8")):
                kanji_grade.append(m[1].replace(" ", ""))
    with open("kanji_grade.json", "w") as f:
        json.dump(kanji_grade, f, indent=2, ensure_ascii=False)


if __name__ == "__main__":
    main()

学年別漢字配当表を JSON で保存

  • 上から順に小学1年生〜6年生の漢字

  • kanji_grade.json

[
  "一右雨円王音下火花貝学気九休玉金空月犬見五口校左三山子四糸字耳七車手十出女小上森人水正生青夕石赤千川先早草足村大男竹中虫町天田土二日入年白八百文木本名目立力林六",
  "引羽雲園遠何科夏家歌画回会海絵外角楽活間丸岩顔汽記帰弓牛魚京強教近兄形計元言原戸古午後語工公広交光考行高黄合谷国黒今才細作算止市矢姉思紙寺自時室社弱首秋週春書少場色食心新親図数西声星晴切雪船線前組走多太体台地池知茶昼長鳥朝直通弟店点電刀冬当東答頭同道読内南肉馬売買麦半番父風分聞米歩母方北毎妹万明鳴毛門夜野友用曜来里理話",
  "悪安暗医委意育員院飲運泳駅央横屋温化荷界開階寒感漢館岸起期客究急級宮球去橋業曲局銀区苦具君係軽血決研県庫湖向幸港号根祭皿仕死使始指歯詩次事持式実写者主守取酒受州拾終習集住重宿所暑助昭消商章勝乗植申身神真深進世整昔全相送想息速族他打対待代第題炭短談着注柱丁帳調追定庭笛鉄転都度投豆島湯登等動童農波配倍箱畑発反坂板皮悲美鼻筆氷表秒病品負部服福物平返勉放味命面問役薬由油有遊予羊洋葉陽様落流旅両緑礼列練路和",
  "愛案以衣位囲胃印英栄塩億加果貨課芽改械害街各覚完官管関観願希季紀喜旗器機議求泣救給挙漁共協鏡競極訓軍郡径型景芸欠結建健験固功好候航康告差菜最材昨札刷殺察参産散残士氏史司試児治辞失借種周祝順初松笑唱焼象照賞臣信成省清静席積折節説浅戦選然争倉巣束側続卒孫帯隊達単置仲貯兆腸低底停的典伝徒努灯堂働特得毒熱念敗梅博飯飛費必票標不夫付府副粉兵別辺変便包法望牧末満未脈民無約勇要養浴利陸良料量輪類令冷例歴連老労録",
  "圧移因永営衛易益液演応往桜恩可仮価河過賀快解格確額刊幹慣眼基寄規技義逆久旧居許境均禁句群経潔件券険検限現減故個護効厚耕鉱構興講混査再災妻採際在財罪雑酸賛支志枝師資飼示似識質舎謝授修述術準序招承証条状常情織職制性政勢精製税責績接設舌絶銭祖素総造像増則測属率損退貸態団断築張提程適敵統銅導徳独任燃能破犯判版比肥非備俵評貧布婦富武復複仏編弁保墓報豊防貿暴務夢迷綿輸余預容略留領",
  "異遺域宇映延沿我灰拡革閣割株干巻看簡危机揮貴疑吸供胸郷勤筋系敬警劇激穴絹権憲源厳己呼誤后孝皇紅降鋼刻穀骨困砂座済裁策冊蚕至私姿視詞誌磁射捨尺若樹収宗就衆従縦縮熟純処署諸除将傷障城蒸針仁垂推寸盛聖誠宣専泉洗染善奏窓創装層操蔵臓存尊宅担探誕段暖値宙忠著庁頂潮賃痛展討党糖届難乳認納脳派拝背肺俳班晩否批秘腹奮並陛閉片補暮宝訪亡忘棒枚幕密盟模訳郵優幼欲翌乱卵覧裏律臨朗論"
]

漢字配当表を読み込む

  • フリガナプログラムでJSONから読み込む

import json

def get_kanji_grade_set() -> set[str]:
    """漢字配当表の全漢字のセットを返す"""
    kanji_grade = set()
    with open("kanji_grade.json") as f:
        for s in json.load(f):
            kanji_grade.update(set(s))
    return kanji_grade

漢字が範囲内かチェック

  • 形態素内の 全漢字 が漢字配当表に含まれるか

def is_ruby_required(surface: str, grade: set[str]) -> bool:
    """フリガナが必要かを判定する"""
    if re.search(KANJI, surface) is None:  # 漢字を含んでいるか
        return False
    kanji_set = set(re.findall(KANJI, surface))
    return not kanji_set <= grade  # 配当表外の漢字があるか

furigana() 関数を書き換え

def furigana(s: str) -> str:
    """文字列にフリガナを振ったHTMLを返す"""
    kanji_grade = get_kanji_grade_set()
    t = dictionary.Dictionary(dict="full").create()
    result = ""
    for token in t.tokenize(s):
        surface = token.surface()
        if is_ruby_required(surface, kanji_grade):
            result += ruby(surface, token.reading_form())
        else:
            result += surface
    return result

小学校の漢字はフリガナなしに

(env) $ python furigana10.py "祇園精舎の鐘の声、諸行無常の響あり。沙羅双樹の花の色、盛者必衰の理をあらはす。"
<ruby><rb>祇園精舎</rb><rt>ぎおんしょうじゃ</rt></ruby>の<ruby><rb>鐘</rb><rt>かね</rt></ruby>の声、諸行無常の<ruby><rb>響</rb><rt>ひびき</rt></ruby>あり。<ruby><rb>沙羅双樹</rb><rt>さらそうじゅ</rt></ruby>の花の色、<ruby><rb>盛者必衰</rb><rt>じょうしゃひっすい</rt></ruby>の理をあらはす。

中学生向けフリガナ

さらなる拡張アイデア

  • 小学校の学年をオプションで指定

  • 常用漢字に対応

  • Web API化

まとめ 📝

  • 自然言語処理形態素解析 の概要を知る

  • Janome で形態素解析、フリガナ

  • SudachiPy で形態素解析、フリガナ

  • 自然言語処理 プログラムを作る流れ を知る

Thank You 🙏

slides.takanory.net

@takanory takanory takanory takanory

takanory profile kuro-chan and kuri-chan