テキストの音韻を調べたい
目的
テキストの音韻を気軽に見たい。
方針
かな……とは……?
文章を構成する音から、母音や子音を抜いてきたい、という場合、日本語であればとりあえず、対象の文章を「ひらがな」と「カタカナ」に分けるくらいのところからはじめることになりそうです。
が、この時点ですでに、面倒なことが多少あります。
Unicide における「平仮名」の符号位置は、U+3040..U+309F
となっていますが、この中に入ってるのはこんな感じです(ちなみにU+3040
はカラ)。
ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ゙゚゛゜ゝゞゟ
ゐゑを
あたりは子音や母音のふりようがあるとして、ゕゖ
とかどうしますかね……。ゟ
とかも。
ちなみに、「片仮名」(U+30A0..U+30FF
)ではこうなっています。
゠ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ・ーヽヾヿ
うーん?
とりあえず、トップにダブルハイフンがいて、「ヷヸヹヺ」なんかもいますね……。
カタカナにはさらに、「片仮名拡張」(U+31F0..U+31FF
)や「仮名補助」(U+1B000..U+1B0FF
)なんかもあります。後者には変体仮名を収録。
片仮名拡張はこんな感じ。
ㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ
扱う「かな」の対象を決める
今の目的は……とりあえず、ざっくり音を見たい、というところで、発音がよくわからないところは、どうせその都度対応することになります。
なので、対象とする「ひらがな」と「カタカナ」をあらかじめ決めてしまった方がよさそうです。
その際、コードポイントで指定しても構わんのですが、あとで見たときわからなくなる可能性はそこそこ高いです。
なので、このくらいならハードコードしてしまってよくないですか……。
こう、かな……。
restricted_hiragana_set = {
"あ", "い", "う", "え", "お",
"か", "き", "く", "け", "こ",
"さ", "し", "す", "せ", "そ",
"た", "ち", "つ", "て", "と",
"な", "に", "ぬ", "ね", "の",
"は", "ひ", "ふ", "へ", "ほ",
"ま", "み", "む", "め", "も",
"や", "ゆ", "よ",
"わ", "ゐ", "ゑ", "を",
"ゔ", "ん",
"が", "ぎ", "ぐ", "げ", "ご",
"ざ", "じ", "ず", "ぜ", "ぞ",
"だ", "じ", "づ", "で", "ど",
"ば", "び", "ぶ", "べ", "ぼ",
"ぱ", "ぴ", "ぷ", "ぺ", "ぽ",
"ぁ", "ぃ", "ぅ", "ぇ", "ぉ",
"ゃ", "ゅ", "ょ",
"ゎ", "ゕ", "ゖ"}
ひらがなをこう制限しておくと、対応するカタカナはコードポイントをずらすだけで得られます。
ひらがな → カタカナ、カタカナ → ひらがな用の関数を用意して、
def hiragana_to_katakana(s: str) -> str:
out = []
for ch in s:
if 0x3041 <= ord(ch) <= 0x3096:
out.append(chr(ord(ch) + 0x60))
else:
out.append(ch)
return "".join(out)
def katakana_to_hiragana(s :str) -> str:
out = []
for ch in s:
if 0x30A1 <= ord(ch) <= 0x30F6:
out.append(chr(ord(ch) - 0x60))
else:
out.append(ch)
return "".join(out)
restricted_katakana_set = {hiragana_to_katakana(ch) for ch in restricted_hiragana_set}
みたいなところですか。
あと足りないのは、音引き「ー」ですね。
restricted_kigou_set = {"ー"}
として、「かな」として扱う対象を、
restricted_kana_set = restricted_hiragana_set | restricted_katakana_set | restricted_kigou_set
と決めます。決めました。決めないと進まんからな。
モーラ
という感じで、「かな」を定めたわけですが、これで一文字ずつ子音と母音をうまいぐあいに定めることはできるでしょうか。
五十音
いわゆる五十音については、0, k, s, t, n, h, m, y, r, w
という子音に、a, i, u, e, o
という母音を組み合わせ(さらにん
を加える)ると音ができると信じられていますが、実運用上はわりと違いますね。「し」は、("s", "i")
よりも("sh", "i")
かなあと思っている人は多そうです。
……これもまあ、JSONとかにベタ打ちする方がよさそうです。
「っ」
は、さて、なんでしょうか。「たったいま」は、音節的には「たっ・た・い・ま」ですが、モーラとしては「た・っ・た・い・ま」です。俳句っぽく読むと、五文字、って感じがあるのではないでしょうか。
ということで、「っ」はまあ、母音、子音のペアとして、("っ", "っ")
とか扱っておきましょうか。
「ん」
「ん」もとりあえず、("N", "N")
で。
「ぁぃぅぇぉゃゅょ」
は、前の単位とくっついて音をなす場合と、音をなさない場合がありますし、ゆれが起こりえます。「リィ」とか、は「リ・ィ」なのか、「リィ」なのか場合による気がします。
このあたりは、発音できる母音・子音ペアをリストアップすることにします。
「ファ」は、「ファ」ですね。
「わぁ」は……「わ・ぁ」かな。
そのあたりの連続は?
「シャァ」は、「シャ・ァ」
「しゃっつ」だと、「しゃ・っ・つ」が正しそう。
「ツァッ」は「ツァ・ッ」かな……。このへんは実例をみていかないとなにがあるかよくわからない。
「ー」
音引きも、音節としてはくっつきますが、モーラとしては独立します。「スーパー」は「ス・ウ・パ・ア」。前の単位の母音を引き継ぐ、ということになりますが、ついては先頭にいるときは音価を持たないとしておきます。その場合、("ー", "ー")
としますか。
あと、今、母音のところには、"っ"
と "N"
と"ー"
も入ることにしているので、引き継ぐ母音は、aiueoだけ、としておきますか。引き継がない場合は、("ー", "ー")
対応表をつくる
といったあたりを踏まえて、
{
"だ": {"C": "d", "V": "a"},
"ぢ": {"C": "dz", "V": "i"},
"づ": {"C": "dz", "V": "u"},
"で": {"C": "d", "V": "e"},
"ど": {"C": "d", "V": "o"},
"きゃ": {"C": "ky", "V": "a"},
"きゅ": {"C": "ky", "V": "u"},
"きょ": {"C": "ky", "V": "o"},
}
こんな感じに、JSONファイルをつくることにします。
設計
- 日本語の平文が与えられる。
- それを「かな」に直す。これを
kanaized_text
と呼ぶことにする。- 漢字はひらがなに直すとして、なにかに使うような気がするので、カタカナはそのまま保持することにしておきます。
- ここには、句読点とか鍵括弧とか、「かな」に入らないものも混じっていることに注意。
- kanaized_text をモーラに分割します。「かな」に入らない文字は、その一文字で1モーラとしておきます。
関数
def plain_text_to_kanaized_text(s: str) -> str:
pass
% kanaized_text = plain_text_to_kanaized_text("「平文をここに」")
っていう感じの使い方を想定。
これはそこそこ間違えることが予想されるので、結果は人力で直す。
やってみたらいろいろ面倒くさかったので、エスケープ文字は相手にしないことにします。
クラス
ちゃんと人力でチェックした kanaized_text
をKanaText
クラスに突っ込み、
kt = KanaText(kanaized, cv_map)
print(kt.text())
print(kt.moras())
print(kt.syllables())
print(kt.vowels())
print(kt.consonants())
みたいに使うようにしますか。それぞれ配列を返すんでしょう。
cv_map
は、モーラに対する母音子音対応表。
具体的なコード
Discussion