2019.3.25 追記
当記事で紹介させて頂いてるプログラムは記載の例文などある程度は対応できるのですが、一部単語では適切にふりがなが振られないことがわかりました。
下記サイトできめら様が当記事でのコードをより改善させたものを紹介して頂いてるので、こちらもぜひご覧ください!
https://www.karelie.net/python3-mecab-html-furigana-1/
当記事の問題点をご指摘頂きましたきめら様に感謝致します。
できるようになったこと
私は大学を辞めたい
というhtmlがある場合に自動で
<ruby><rb>私</rb><rt>わたし</rt></ruby>は
<ruby><rb>大学</rb><rt>だいがく</rt></ruby>を
<ruby><rb>辞</rb><rt>や</rt></ruby>めたい
私は大学を辞めたい
とルビがふれるようになる
必要なもの
環境はMacOS,Pythonのバージョンは3.5.1。
1.Mecab(筆者の場合デフォルトでmacに入っていたので省略します)
2.mecab-python3
3.pip(mecab-python3をインストールするのに必要)
必要なものをそろえる
Mecabというのは辞書解析ツールでこれを使うことに漢字のよみがなを取得することができます。
コマンドライン上からmecabコマンドを実行し、次の行にテキストを入力することで実行・取得できます。
$ mecab
私は大学を辞めたい
私 名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
大学 名詞,一般,*,*,*,*,大学,ダイガク,ダイガク
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
辞め 動詞,自立,*,*,一段,連用形,辞める,ヤメ,ヤメ
たい 助動詞,*,*,*,特殊・タイ,基本形,たい,タイ,タイ
EOS
このMecabをpython3で扱うためにはpython-mecab3というパッケージが必要になります。
そういったパッケージ類を管理するツールがpipというものなので
pipがインストールされていない場合はまずインストールします。
(Rubyでいうところのgemのようなもの)
pipのインストール
$ easy_install pip
ちなみにpython3.4からはデフォルトで入っているそうです。
mecab-python3 のインストール
pipがインストールできましたら、次にmecab-python3 をインストールします。
$ pip install mecab-python3
pip listコマンドで現在入っているパッケージの一覧を取得できますので、
mecab-pythonがあればインストール成功です。
$ pip list
mecab-python3 (0.7)
とりあえずpython3でMecabを扱ってみる
さて、とりあえず環境が整いましたので、pythonからmecabを使ってみてどのようにデータを取得できるのかを確かめてみようと思います。
#!/usr/local/src/pyenv/shims/python
# -*- coding: utf_8 -*-
import sys
import MeCab
mecab = MeCab.Tagger("-Ochasen")#mecabを呼び出し
text = mecab.parse('私は大学を辞めたい')#ふりがなを取得
print(text)
$ mecab.py
私 ワタシ 私 名詞-代名詞-一般
は ハ は 助詞-係助詞
大学 ダイガク 大学 名詞-一般
を ヲ を 助詞-格助詞-一般
辞め ヤメ 辞める 動詞-自立 一段 連用形
たい タイ たい 助動詞 特殊・タイ 基本形
EOS
パース(parse)というのは"分析" といった意味なようで
mecab = MeCab.Tagger("-Ochasen")
でMecabを呼び出した後に、
text = mecab.parse('私は大学を辞めたい')
parseメソッドを呼び出してその引数に分析したいテキストを指定してあげることで実行結果のような形式で得る事が出来ます。
parseToNodeを用いる
上でとりあえず取得できた訳ですが、この形式だとスペースで区切られているためプログラム的にはとても扱いづらいです。
そのため、ノード(Node)と言われる形式に変更して扱いやすくします。
import sys
import MeCab
mecab = MeCab.Tagger("-Ochasen")
mecab.parse('') # 空でパースする必要がある
node = mecab.parseToNode('私は大学を辞めたい')
while node :
print(node.surface + "\t" + node.feature)
node = node.next
$ mecab.py
BOS/EOS,*,*,*,*,*,*,*,*
私 名詞,代名詞,一般,*,*,*,私,ワタシ,ワタシ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
大学 名詞,一般,*,*,*,*,大学,ダイガク,ダイガク
を 助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
辞め 動詞,自立,*,*,一段,連用形,辞める,ヤメ,ヤメ
たい 助動詞,*,*,*,特殊・タイ,基本形,たい,タイ,タイ
BOS/EOS,*,*,*,*,*,*,*,*
これにより、「,」区切りで分析結果が得られるので扱いやすくなりました。
先ほどと同じように、mecabを呼び出したらとりあえず引数を空にしてパースする必要があるそうなのでその通りにします。
その後
node = mecab.parseToNode('私は大学を辞めたい')
といったようにparseToNodeメソッドの引数に文章を指定してあげます。
nodeは1単語づつ区切って処理していくので、while文を用いて
文章が終わる(nodeに値を入れ終わる)までループさせます。
ループの中では、
node.surfaceで元の引数のテキスト、
node.featureでカンマ区切りの分析データを取得できます。
ちなみに
node = node.next
を忘れると無限ループされるので気をつけましょう。
ここまででとりあえずpythonからmecabを使う事に成功しました。
HTML形式で実際にルビを振ってみる
ここまではインターネット上に情報がたくさんあったのですが、
ここから先はあまり情報がなく苦労した部分です。
が、基本的には上のnode.featureをいじりながらhtmlに当てはめていく作業になります。
htmlでのルビの振り方は
<ruby><rb>文字</rb><rt>もじ</rt></ruby>
文字
といった形になるので、
タグ内に元の漢字、rtタグ内にふりがなを当てはめていきましょう。
#!/usr/local/src/pyenv/shims/python
# -*- coding: utf_8 -*-
import sys
import MeCab
import re
mecab = MeCab.Tagger("-Ochasen")
mecab.parse('')#空でパースする必要がある
node = mecab.parseToNode('私は大学を辞めたい')
while node :
origin = node.surface#もとの単語を代入
kana = node.feature.split(",")[7]#読み仮名を代入
#正規表現で漢字と一致するかをチェック
pattern = "[一-龥]"
matchOB = re.match(pattern , origin)
#originが空のとき、漢字以外の時はふりがなを振る必要がないのでそのまま出力する
if origin != "" and matchOB:
print("<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin,kana),end="")
else :
print(origin)
node = node.next
$ mecab.py
<ruby><rb>私</rb><rt>ワタシ</rt></ruby>は
<ruby><rb>大学</rb><rt>ダイガク</rt></ruby>を
<ruby><rb>辞め</rb><rt>ヤメ</rt></ruby>たい
私は大学を辞めたい
だいぶそれっぽくなってきました。
まず、最初の2行で単語とよみがなをそれぞれoriginとkanaに代入します。
よみがなの方に関して、よみがなはnode.featureのカンマ区切りの7番目にあるのでsplit関数でカンマ区切りで配列にした7番目を指定しています。
origin = node.surface #もとの単語を代入
kana = node.feature.split(",")[7] #読み仮名を代入
続いて、今回はもとの単語が漢字の時のみふりがなをふりたいので
元の単語が漢字かどうかを正規表現を用いてチェックします。
正規表現を用いるためにまず冒頭でreというものをimportします。
import re
正規表現についてここでは詳しくは触れませんが、先ほどimportしたreにはいろいろなmethodがあります。
今回は単語originの先頭の1文字が漢字かどうかを調べたいとして、matchメソッドを用います。
第1引数にはパターン(今回は漢字)第2引数には単語を指定。
マッチした場合マッチオブジェクトというマッチに関する情報が格納され、マッチしない場合は「None」が格納されます。
# 正規表現で漢字と一致するかをチェック
pattern = "[一-龥]" #漢字のパターン
matchOB = re.match(pattern , origin) #漢字じゃ無い時はNone
最後にifで判定を行います。
日本語で整理すると、
・originが漢字のとき
➡htmlでルビを振って出力
・originが漢字以外(ひらがな、カタカナ、数字など)のとき
➡そのままの形で出力
となります。
#originが空のとき、漢字以外の時はふりがなを振る必要がないのでそのまま出力する
if matchOB and origin != "" :
print("<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin,kana),end="")
else :
print(origin)
originが漢字以外の時はmarchOBにNoneが代入されてるので判定にかかりelseの方に移行します。
ここまででだいぶ形にはなってきたのですが問題があります。
カタカナ問題
送り仮名はひらがなで振りたいのに、Mecabで取得できる読み仮名はカタカナなのでこれをひらがなに変換したいです。
Rubyだと関数を使えば一発で出来るのですがPythonだと調べた限りでは出来なさそうです。
ですので、ここは一工夫します。
まず、このようなひらがなとカタカナ1文字ずつの配列を用意します。
hiragana = ["あ","い","う","ん"] #hiragana[0]=="あ"
katakana = ["ア","イ","ウ","ン"] #katakana[0]=="ア"
そして例えば、「アイ」という言葉を「あい」にしたいとします。
text = list("アイ")#text[0]=="ア" text[1]=="イ"になる
kana = "" # ひらがなを入れる変数
for hoge in len(text) # 文字数だけ繰り返す(hogeに代入)
for i in list(katakana)
katakana[i] == hoge
kana += hiragana[i]
print(kana) # あい
「アイ」をアとイに分けて1字ずつ照合したいので、
「アイ」を1文字ずつ分割して配列にします。
そしてその配列の個数(=文字数)だけforでループします。
その中では、katakanaの個数分だけループし、hogeと照合します。
今回最初のループのhogeの中身は"ア"なので、iが0のときすなわちkatakana[0]と一致します。
最後にhiragana[i]、つまり今回で言うところのhiragana[0]すなわち"あ"
を変数kanaに足します。
これを文字数だけ繰り返せばkanaにはひらがなが入ります。
これをモジュール化したコードがこちらです。
#!/usr/local/src/pyenv/shims/python
# -*- coding: utf_8 -*-
import sys
import MeCab
import re
def henkan(text) :
hiragana = [chr(i) for i in range(12353, 12436)]
katakana = [chr(i) for i in range(12449, 12532)]
kana = ""
#読み仮名のカタかなをひらがなに
for text in list(text):
for i in range(83):
if text == katakana[i]:
kana += hiragana[i]
return kana
mecab = MeCab.Tagger("-Ochasen")
mecab.parse('')#空でパースする必要がある
node = mecab.parseToNode('私は大学を辞めたい')
while node :
origin = node.surface #元の単語を代入
kana = node.feature.split(",")[7] #読み仮名を代入
kana = henkan(kana) #変換関数を呼びだしカタカナをひらがなに
# 正規表現で漢字と一致するかをチェック
pattern = "[一-龥]"
matchOB = re.match(pattern , origin)
# originが空のとき、漢字以外の時はふりがなを振る必要がないのでそのまま出力する
if origin != "" and matchOB:
print("<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin,kana),end="")
else :
print(origin)
node = node.next
詳細は省きますが、
range(83)
となっているのは小さい文字も加わり50音だけでなく配列の個数が83になるためです。
送り仮名問題
もう1つの問題が送り仮名の問題です。
Mecabは単語ごとに区切っているので送り仮名も含まれてしまい純粋に漢字のみにルビを振れていないという事です。
つまり、
<ruby><rb>辞め</rb><rt>やめ</rt></ruby>
辞め
ではなく
<ruby><rb>辞</rb><rt>や</rt></ruby>め
辞め
としたいということです。
これについてみていきます。
基本的には、元の単語と送り仮名の語尾が一致するかどうかを判定します。
origin:美しい
kana:うつくしい
origin:走る
kana:はしる
これらは語尾2字または1字が一致しているので送り仮名と判断する事が出来ます。
これに関しても先ほどのカタカナ問題のときのように
originとkanaを1字ずつ分解し配列化し
語尾1文字または2文字が一致するかどうかの判定をします。
#!/usr/local/src/pyenv/shims/python
# -*- coding: utf_8 -*-
import sys
import MeCab
import re
def henkan(text) :
hiragana = [chr(i) for i in range(12353, 12436)]
katakana = [chr(i) for i in range(12449, 12532)]
kana = ""
#読み仮名のカタかなをひらがなに
for text in list(text):
for i in range(83):
if text == katakana[i]:
kana += hiragana[i]
return kana
mecab = MeCab.Tagger("-Ochasen")
mecab.parse('') # 空でパースする必要がある
node = mecab.parseToNode('私は大学を辞めたい')
while node :
origin = node.surface # 元の単語を代入
yomi = node.feature.split(",")[7] # 読み仮名を代入
kana = henkan(yomi)
# 正規表現で漢字と一致するかをチェック
pattern = "[一-龥]"
matchOB = re.match(pattern , origin)
# originが空のとき、漢字以外の時はふりがなを振る必要がないのでそのまま出力する
if origin != "" and matchOB:
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="".join(origin)
kana="".join(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 = "".join(origin)
kana = "".join(kana)
print("<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin,kana),end="")
print(okurigana)
else :
print(origin)
node = node.next
これを実行すると、
$ mecab.py
<ruby><rb>私</rb><rt>わたし</rt></ruby>
は
<ruby><rb>大学</rb><rt>だいがく</rt></ruby>
を
<ruby><rb>辞</rb><rt>や</rt></ruby>め
たい
私は大学を辞めたい
となり、見事に完成です!!!
では詳しく見ていきましょう。
origin = list(origin)
kana = list(kana)
num1 = len(origin)
num2 = len(kana)
okurigana = ""
まずは先ほどいったように1字ずつ調べたのでlist関数で配列化します。
また、語尾が一致するかを調べたいため配列の個数をlen関数で調べて
num1,num2にそれぞれ代入します。
if origin[num1-1] == kana[num2-1] and origin[num1-2] == kana[num2-2] :
okurigana = origin[num1-2]+origin[num1-1]
origin[num1-1] = ""
kana[num1-2] = ""
origin[num2-1] = ""
kana[num2-2] = ""
origin = "".join(origin)
kana = "".join(kana)
これは最後の文字と最後から2番目が一致した時の処理、つまり
「美しい」みたいな単語の時の処理です。
この場合、語尾2文字の「しい」が送り仮名になるので、
これをokurigana変数に代入します。
okurigana = origin[num1-2] + origin[num1-1]
一旦変数に入れてしまえば「しい」は不要なので削除します。
origin[num1-1] = ""
kana[num1-2] = ""
origin[num2-1] = ""
kana[num2-2] = ""
最後に、残った
origin = ["美"]
kana = ["う","つ","く"]
を変数に戻すためにjoin関数を用います。
origin = "".join(origin)
kana = "".join(kana)
残りの部分に関しては「走る」のような送り仮名1文字の時か、
「大学」のような送り仮名が無い時の処理です
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 = "".join(origin)
kana = "".join(kana)
配列化してしまっているので、何も処理が無い送り仮名なしの時にも
変数に戻す作業をします。
最後は出力。
ここまでの処理できっちり変数に代入されているので特別変更する箇所はありませんが、送り仮名がokuriganaに代入されるのでこれを忘れずに出力してあげればokです。
print("<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin,kana),end="")
print(okurigana)
あとはこのままだと少し見づらいので関数化したら完成です!
#!/usr/local/src/pyenv/shims/python
# -*- coding: utf_8 -*-
import sys
import MeCab
import re
def henkan(text) :
hiragana = [chr(i) for i in range(12353, 12436)]
katakana = [chr(i) for i in range(12449, 12532)]
kana = ""
#読み仮名のカタかなをひらがなに
for text in list(text):
for i in range(83):
if text == katakana[i]:
kana += hiragana[i]
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 #元の単語を代入
yomi = node.feature.split(",")[7] # 読み仮名を代入
kana = henkan(yomi)
#正規表現で漢字と一致するかをチェック
pattern = "[一-龥]"
matchOB = re.match(pattern , origin)
#originが空のとき、漢字以外の時はふりがなを振る必要がないのでそのまま出力する
if origin != "" and matchOB:
origin,kana,okurigana = kanadelete(origin,kana)
print("<ruby><rb>{0}</rb><rt>{1}</rt></ruby>".format(origin,kana),end="")
print(okurigana)
else :
print(origin)
node = node.next