MeCab+UniDicでルビ表示を正しく実装する方法
日本語の形態素解析システムでルビ(振り仮名)を正確に表示することは、単純に見えて実は非常に複雑な問題です。本記事では、MeCab + UniDicを使った形態素解析において、「映画」が「ええが」、「生活」が「せえかつ」と誤って表示される問題を解決する実装方法を詳しく解説します。
背景:なぜルビ表示は難しいのか
日本語の形態素解析でルビ表示を実装する際、最も大きな課題は長音記号「ー」の適切な変換です。MeCabの解析結果では、多くの単語の読みが長音記号を含んだ形で返されます:
- 「映画」→「エーガ」
- 「生活」→「セーカツ」
- 「研究」→「ケンキュー」
しかし、実際のルビとして表示すべき正しい仮名は以下のようになります:
- 「映画」→「えいが」
- 「生活」→「せいかつ」
- 「研究」→「けんきゅう」
問題の原因
複雑な長音変換ロジックの実装
// 間違いやすい実装例
if ('エケゲセゼテデネヘベペメレ'.includes(prevChar)) {
return 'イ'; // エ段の長音は「イ」に変換
}
このような単純な規則では、日本語の複雑な音韻規則に対応できません。特に:
- 拗音(ゃゅょ)の処理
- 連濁の処理
- 活用形での読みの変化
これらを全て自前で実装すると、コードが複雑になり保守が困難になります。
解決方法:UniDicのformBase
フィールドを活用
UniDicには実は正しい仮名表記を提供するフィールドが既に存在しています。
UniDicの重要フィールド比較
フィールド名 | 映画 | 生活 | 説明 |
---|---|---|---|
surface_reading | エーガ | セーカツ | 長音記号あり(発音用) |
formBase | エイガ | セイカツ | 正しい仮名表記 |
goshu | エーガ | セーカツ | 語種情報 |
実装例
改善前:複雑な長音変換ロジック
// 長音記号を変換する複雑なロジック
finalReading = token.surface_reading.replace(/ー/g, (match, offset) => {
if (offset > 0) {
const prevChar = token.surface_reading[offset - 1];
// エ段の長音は「イ」に変換
if ('エケゲセゼテデネヘベペメレ'.includes(prevChar)) {
return 'イ';
}
// オ段の長音は「ウ」に変換
if ('オコゴソゾトドノホボポモヨロヲ'.includes(prevChar)) {
return 'ウ';
}
// その他多くの条件分岐...
}
return match;
});
改善後:UniDicのformBase
を利用
// UniDicのformBaseフィールドを優先的に使用(これが正しい仮名表記)
let finalReading = '';
if (token.formBase && token.formBase !== '*' && !containsKanji(token.formBase)) {
// formBaseがカタカナで正しい読みを持っている
finalReading = token.formBase;
} else if (token.conjugated_reading && token.conjugated_reading !== '*') {
// AWS Lambdaが生成した活用形の読み
finalReading = token.conjugated_reading;
} else if (reading && !containsKanji(reading)) {
// 基本的な読み
finalReading = reading;
} else {
// フォールバック
finalReading = '';
}
デバッグ方法
UniDicのフィールドを確認する方法:
# APIエンドポイントで確認
curl -X POST http://localhost:3000/api/analyze \
-H "Content-Type: application/json" \
-d '{"text": "映画"}' | jq '.raw_aws_result.tokens[0]'
重要なフィールドの確認:
# Pythonでの確認例
import sys, json
data = json.load(sys.stdin)
token = data.get('raw_aws_result', {}).get('tokens', [])[0]
print(f"surface_reading: {token.get('surface_reading')}") # エーガ
print(f"formBase: {token.get('formBase')}") # エイガ(正解!)
print(f"goshu: {token.get('goshu')}") # エーガ
Lambda API側の実装
MeCab + UniDicを使用したPython実装:
# UniDicのフィールド定義
unidic_fields = [
'pos', # 品詞
'pos_detail_1', # 品詞細分類1
'pos_detail_2', # 品詞細分類2
'pos_detail_3', # 品詞細分類3
'conjugation_type', # 活用型
'conjugated_form', # 活用形
'base_form', # 原形
'reading', # 原形の読み
'surface_form', # 表層形
'surface_reading', # 表層形の読み(活用形の読み)
'lemma', # 語彙素
'goshu', # 語種
'orthBase', # 書字形基本形
'orthVariant', # 表記
'formBase', # 形態素基本形(重要!)
'formVariant', # 異形態
]
# フィールドの抽出
for i, field in enumerate(unidic_fields):
if i < len(features):
token[field] = features[i]
実装のポイント
1. 優先順位の設定
// 読み仮名の候補を優先順位で取得
const readingCandidates = [
token.formBase, // 最優先:正しい仮名表記
token.conjugated_reading, // 活用形の読み
token.reading, // 基本的な読み
];
2. 漢字判定の実装
function containsKanji(text: string): boolean {
if (!text) return false;
return /[\u4e00-\u9faf]/.test(text);
}
3. カタカナ→ひらがな変換
function katakanaToHiragana(text: string): string {
if (!text) return '';
return text.replace(/[\u30A1-\u30FF]/g, (match) => {
const code = match.charCodeAt(0);
if (code >= 0x30A1 && code <= 0x30F6) {
return String.fromCharCode(code - 0x60);
}
return match;
});
}
パフォーマンス最適化
// キャッシュを活用
const CACHE_SIZE = 500;
const analysisCache = new Map<string, any>();
// キャッシュチェック
if (analysisCache.has(cacheKey)) {
return analysisCache.get(cacheKey);
}
// 結果をキャッシュ
analysisCache.set(cacheKey, result);
まとめ
日本語のルビ表示で正確な結果を得るためには:
- UniDicの
formBase
フィールドを優先的に使用する - 複雑な音韻変換ロジックを自前で実装しない
- 信頼できる辞書データソースを最大限活用する
この方法により:
- コードがシンプルになり保守性が向上
- 「映画」→「えいが」、「生活」→「せいかつ」など正確な表示が実現
- 日本語の複雑な特性への個別対応が不要に
UniDicには既に必要なデータが全て揃っているため、それを正しく活用することが重要です。