PHPでのカタカナ/ひらがなのローマ字変換

これは2009/10/15にAmebloに投稿した記事です。

背景

ブログ情報サイトBlogara:ブロガラでは、あるテーマに関連するブログや関連サイトの情報の管理を行いますが、
当然のように、まず数多くのブログの情報をDBに登録する必要があります。

勿論、キーボードで名前やブログURLなどを一文字づつ入力していたのでは気の遠くなるような作業になりますので、
WebAPIを使った情報取得や、管理画面での入力フォームの項目へのコピペでの記入がメインとなりますが、
フォームの入力値の自動変換でも書いている通り、
情報ソースの文字形式とBlogaraでの文字形式が異なるケースも有り、そのままでは利用出来ない事が多々あります。
そのような場合には、可能であればサーバー/クライアント側スクリプトで文字列変換処理を行い、
Blogaraでの内部管理に適した形式に変換していますが、
機能として実装されていない変換が必要な場合には手間の掛かる手動での入力で行う事になります。

そこでここでは、その実装されていなかった変換機能の1つである
名前のふりがな(カタカナ又は平仮名)からローマ字表記の名前への変換を、
WebAPIと連携し易いサーバー側のPHPでの実装を行います。

指針

  • 諸般の都合から、動作環境はクロージャが使えるPHP5.3.xでは無くPHP5.2.xとなる。
  • 文字列はUTF-8として扱う。
  • 変換前の文字列は全角カタカナ(又全角カタカナに変換出来る文字)と空白で構成された文字列であり、それ以外の文字は考慮しない。
  • 変換した後のローマ字文字列は、現状では単に補足情報としてのみ利用する。
  • ローマ字文字列を、変更する事で他に追加作業を要するような影響を及ぼす用途には使わない。
  • 変更は、管理画面の入力フォームからいつでも可能。

このように、今回は氏名のフリガナの文字列をローマ字(基本的にはヘボン式)に変換する事を目的とした処理であり、
また上記のような特徴がある為、クリティカルな用途には利用しない事に加え、
カタカナとalphabetが混在するような処理が複雑化するケースには対応しないので、処理はかなり簡素化出来ます。

今回はPHPでの実装となりますが、
既にBlogaraでは各種文字列操作をstaticメソッドとして持つクラスが存在しますので、
カナ->ローマ字変換関数も、そのクラスのstaticメソッドとして追加する事にします。
その為、独立したクラスでは無く、他の多数の機能を抱えるクラス内メソッドとしての実装になりますので、
正直あまりコード量が多くなるのは望ましくありません。

という訳で、要するに今回は多少アレでも(コード量的に)軽い方が良いという事になります。

文字列変換

PHPでの文字列変換となると、
豊富に用意された各種文字列操作関数や正規表現(preg/ereg)を使う事が考えられます。

ここでBlogaraの文字列操作クラスに既に実装している機能とコードを振り返って確認してみると、
記号(%$など)の半角/全角相互変換メソッドが有り、
その変換では半角記号と全角記号を配列として持ち、str_replaceを利用した一括変換を行っていますので、
ローマ字変換でも、それと同様の処理を行う事を第一候補とします。

まず、日本語処理という事で、
ひらがなカタカナ変換や半角/全角カタカナ変換もあるmbstring系の関数を見てみましたが、
残念ながらローマ字変換は無い模様です。

となると次に行うのは調べ物の大定番である検索サイト利用ですが、
やはりそこそこ需要がある機能のようで、PHP ローマ字で検索を行うといくつかヒットします。
が、一文字づつparseしていくという丁寧でより正確性を求めた実装が多いようですので、
今回目指すstr_replaceによるお手軽路線とは方向性が微妙に異なる模様です。

という訳で早速str_replaceによる実装に移ります。

str_replaceを使った一括変換と配列の並び

str_replaceについての解説はネット上に腐るほど有りますので割愛しますが、
今回のような処理では、変換候補となる第一引数の配列の要素の並びが重要となります。

str_replaceでは配列の先頭から順に変換を行う為に、
例えば、変換配列において"キ"が"キャ"より前にある場合には、
"キャット"という文字列が"kyatto"では無く"kiャtto"と変換され、"ャ"が中途半端な文字として残ってしまいます。
よって一文字単位の変換候補は配列の最後に押しやってしまう事が必要です。

と、このような注意点がある以外は、上記指針で書いたような前提が有り単純な変換しか行わない今回の実装では、
変換元のカナと変換後のローマ字の変換配列を作成するのが多少面倒なだけで、
str_replaceの一行で済む処理ですが、
実際には後述する前処理や、次節に書くような個別の特殊ケースに対応する必要があります。


情報ソースによって異なる文字形式に対応する為に、まず変換前の前処理を行います。
変換元の文字中の半角カタカナを全角カタカナに変換し、またひらがなから全角カタカナへの変換処理を行い、
手間が掛からない範囲で可能な限り出来る自動変換を適用します。

その後に、str_replaceでの変換。今回はコードの分量を少なくするという目的の為に、
ここで利用する変換候補配列が長大(要素数169)になっていますが敢えて気にしません。:)
str_replaceを使った変換はこれだけでOKですが、
追加処理としてローマ字表記特有のケースを考慮する必要もあります。

// カタカナ->ローマ字変換
// $kkはカタカナの変換候補配列、$kkrはそれに対応したローマ字の文字配列
$kk = array( 'キャ','キュ','キョ',... // 長いので省略
$kkr = array( 'kya','kyu','kyo',... 
$str = str_replace( $kk, $kkr, $str );

カタカナ->ローマ字変換での追加処理

オ段の長音

人名のローマ字表記を行う時に考えるのは、
例えば太田(オオタ)という名前は、Oota/Ōta/Ohta/Otaのどれになるのか、
あるいは太郎(タロウ)はTarou/Tarō/Taroのどれかという点ですが、
今回の変換ではOta/Taroのような表記に統一します。

が、統一すると一言で言っても実際は中々面倒な処理となります。
単純に下記のような変換を適用する事でも一応は望む結果を得られますが、
望まない副作用のような変換も行ってしまう事が問題となります。

$str = preg_replace( '/o[ou]/u', 'o', $str );
// "おおた たろう[oota tarou]" -> "ota taro" となり、このような単純なケースでは望む結果になりますが、
// ouが連続した文字列の場合には、
// "とおの よおう[toono yoou]" -> "tono you"
// "おうおおおじ[ouoooji](王大叔父)" -> "oooji"
// "おうむをもううがおそう[oumuwomouugaosou](オウムを猛雨が襲う)" -> "omuwomougaoso"
// "かおをおおう おううさん[kaowooou ouusan](顔を覆う 奥羽さん)" -> "kaowoo ousan"
// と、訳判らない文字列となるので、
// とりあえずの大味な回避策として下記のような変換にしていますが、
// とても一般文章に適用出来るものでは有りません。
$str = preg_replace( '/(\b|[^ou])o[ou](\b|[^ou])/u', '$1o$2', $str );

撥音(ン)の扱い

ヘボン式のローマ字表記では、基本的に"ん"はnですが、その後に続く子音がbpmの場合には、
nでは無くmとなります。

// トンボ->tombo、タンポポ->tampopo
$str = preg_replace( '/n([bpm])/u', 'm$1', $str );

促音(ッ)の扱い

カタカナ"ッ"で表記される促音のローマ字表記は、直後に置かれる子音を重ねて書く事で表現します。
が、カタカナの変換配列に"ッ"を含む変換候補を記述すると、さらにアホのように変換候補配列が巨大化しますので、
これは処理的に解決する事にします。

$str = preg_replace_callback( '/ッ+(.)?/u', 'kk2rProc', $str );

function kk2rProc( $ma ) {
  $char = $ma[1];
// 直後の文字がアルファベットの子音の小文字の場合だけが処理対象
  if ( !preg_match('/^[a-z]$/u',$char) || preg_match('/^[aiueo]$/u',$char) ) return $char;

// chiなどcの場合には例外的にcchiでは無くtchiとなる
  if ( $char == 'c' )
    return 'tc';
  else
    return $char.$char;
}

長音符(ー)の扱い

今回は、長音符(ー)や単独の捨て仮名(ァィゥェォ)は無視するので、それらの文字は捨てるだけになりますが、
ちなみに長音符に対応するとしたら下記のようなコードになります。

// 母音の後にhを置く表記。 オオタ->ohta
$str = preg_replace( '/([aiueo])ー/u', '$1h', $str );

// 伸ばす母音にマクロンを使う表記。 オオタ->ōta、フーン->fūn
$macron = array( 'a'=>'ā', 'i'=>'ī','u'=>'ū','e'=>'ē','o'=>'ō');
$str = preg_replace( '/([aiueo])ー/ue', '$macron["$1"]', $str );

元の文字列に半角英字小文字が含まれている場合の対応

前提として、カタカナ/空白以外の文字が含まれる事を考慮していませんが、
半角英字小文字が含まれた文字列も対象とするならば、
下記のような処理をカタカナ->ローマ字変換のstr_replaceと追加処理の前後に行います。

ただ、このような処理をフリガナという文字数の限られた文字列に対してでは無く、
一般文章のように長文になる事も十分予想出来る文字列を対象に行うのなら、str_replaceでの一括変換では無く、
for(あるいはforeach)ループ内で1文字づつparseする処理にした方が良いと思われます。
# 既に追加処理中で何度も使っていますが、
# str_replace/preg_replaceという文字列全体を対象とする処理を多数行うのは効率的にかなり微妙なので。

// 半角英字小文字を退避
// 実際にはこの処理の前に、元の文字列中に%##abc##%のようなフレーズが(偶然)存在する場合の前処理が必要。
$str = preg_replace( '/([a-z]+)/u', '%##$1##%', $str );

// カタカナ->ローマ字変換と追加処理
$str = str_replace( self::$kk, self::$kkr, $str );
$str = preg_replace( '/n([bpm])/u', 'm$1', $str );

// 元の文字列の半角英字小文字を復元
$str = preg_replace( '/%##([a-z]+)##%/u', '$1', $str );

# 2010/05/19追記
# わざわざ####を付けて退避させるくらいなら、/[ァ-ヴ]+/のように変換対象となる部分文字列を抜き出し、
# それに対してpreg_replace_callbackで指定した関数で変換するようにした方が良いのでは。
# と、半年振りに自分で書いたブログ記事を見て思った次第。

実装

カタカナ->ローマ字変換処理だけを抜き出した物を付記します。
コメントアウトをしている前処理のひらがな->カタカナ変換や、後処理の特定文字コードの抽出は、
各利用者の方で好き勝手に実装して下さい。
特定文字コードの抽出処理を行っていない状態では、ァィゥなどの変換非対象カナ文字がゴミとして残ります。

このコードはMITライセンス形態で配布しています。
言わずもがなですが、これを利用した事により不利益を被ったとしても当方は一切関知しません。

// copyright (c) 2009 ksy AT blogara.jp <http://blogara.jp/>
// This script is freely distributable under the terms of an MIT-style license.

define( 'CNV_MD_ALPHA', 0x02 );
define( 'CNV_MD_SPACE', 0x08 );
define( 'CNV_MD_ALSP', 0x0a );

class TextUtil {
  protected static $kk = array( 'キャ','キュ','キョ','シャ','シュ','ショ','チャ','チュ','チョ','ニャ','ニュ','ニョ','ヒャ','ヒュ','ヒョ','ミャ','ミュ','ミョ','リャ','リュ','リョ','ギャ','ギュ','ギョ','ジャ','ジュ','ジョ','ヂャ','ヂュ','ヂョ','ビャ','ビュ','ビョ','ピャ','ピュ','ピョ','イェ','ウァ','ウィ','ウェ','ウォ','ウュ','キェ','クァ','クィ','クェ','クォ','クヮ','シェ','チェ','ツァ','ツィ','ツェ','ツォ','ツュ','ティ','テュ','トゥ','ニェ','ヒェ','ファ','フィ','フェ','フォ','フィェ','フャ','フュ','フョ','ミェ','リェ','ヴァ','ヴィ','ヴェ','ヴォ','ヴィェ','ヴャ','ヴュ','ヴョ','ヴヰ','ヴヲ','ギェ','グァ','グィ','グェ','グォ','グヮ','ゲォ','ゲョ','ジェ','ディ','デュ','ドゥ','ビェ','ピェ','ア','イ','ウ','エ','オ','カ','キ','ク','ケ','コ','サ','シ','ス','セ','ソ','タ','チ','ツ','テ','ト','ナ','ニ','ヌ','ネ','ノ','ハ','ヒ','フ','ヘ','ホ','マ','ミ','ム','メ','モ','ヤ','ユ','ヨ','ラ','リ','ル','レ','ロ','ワ','ヲ','ン','ガ','ギ','グ','ゲ','ゴ','ザ','ジ','ズ','ゼ','ゾ','ダ','ヂ','ヅ','デ','ド','バ','ビ','ブ','ベ','ボ','パ','ピ','プ','ペ','ポ','ヰ','ヱ','ヲ','ヴ','・' );
  protected static $kkr = array( 'kya','kyu','kyo','sha','shu','sho','cha','chu','cho','nya','nyu','nyo','hya','hyu','hyo','mya','myu','myo','rya','ryu','ryo','gya','gyu','gyo','ja','ju','jo','ja','ju','jo','bya','byu','byo','pya','pyu','pyo','ye','wa','wi','we','wo','wyu','kye','kwa','kwi','kwe','kwo','kwa','she','che','tsa','tsi','tse','tso','tsyu','ti','tyu','tu','nye','hye','fa','fi','fe','fo','fye','fya','fyu','fyo','mye','rye','va','vi','ve','vo','vye','vya','vyu','vyo','vi','vo','gye','gwa','gwi','gwe','gwo','gwa','geo','geyo','je','di','dyu','du','bye','pye','a','i','u','e','o','ka','ki','ku','ke','ko','sa','shi','su','se','so','ta','chi','tsu','te','to','na','ni','nu','ne','no','ha','hi','fu','he','ho','ma','mi','mu','me','mo','ya','yu','yo','ra','ri','ru','re','ro','wa','wo','n','ga','gi','gu','ge','go','za','ji','zu','ze','zo','da','ji','zu','de','do','ba','bi','bu','be','bo','pa','pi','pu','pe','po','wi','we','wo','vu',' ' );

  public static function convertKKtoR( $str, $bHGtoKK=TRUE, $mode=CNV_MD_ALSP ) {
    if ( !$str ) return $str;

// 半角カタカナから全角カタカナへの変換
//    $str = self::convertHtoF( $str, CNV_MD_KKANA );

// ひらがなから全角カタカナへの変換
//    if ( $bHGtoKK )
//      $str = self::convertHK( $str, CNV_HGTOKK );
    $str = str_replace( self::$kk, self::$kkr, $str );
    $str = preg_replace( '/(\b|[^ou])o[ou](\b|[^ou])/u', '$1o$2', $str );
    $str = preg_replace( '/n([bpm])/u', 'm$1', $str );
    $str = preg_replace_callback( '/ッ+(.)?/u', 'TextUtil::convKK2toR', $str );

// 有効とする文字コード以外を削除
// 半角英字小文字を含む文字列を対象にする場合には、カタカナ->ローマ字変換の前にも行う方が良い
//    $mode |= CNV_MD_ALPHA;
//    return self::extractCode( $str, $mode );
	return $str;
  }

  protected static function convKK2toR( $ma ) {
    if ( !$ma || !isset($ma[1]) ) return '';
    $char = $ma[1];
    if ( !preg_match('/^[a-z]$/u',$char) || preg_match('/^[aiueo]$/u',$char) ) return $char;

    if ( $char == 'c' )
      return 'tc';
    else
      return $char.$char;
  }
}

今回は、単語のみで構成された名前の振り仮名の変換を目的とした処理ですので、
これを助詞などが含まれてくる一般文章に適用した場合には、かなり精度の怪しい文字列となると思われます。

ちなみに実際にBlogaraに実装されているフリガナからローマ字に変換する処理では、
この後処理として、文字列前後のホワイトスペースを削除し、連続するスペースを1つの半角スペースに変換、
Ota Taroのように単語境界の先頭を大文字化、スペースを挟んで2単語がある場合には、
Taro OTAというように苗字と思われる単語を全て大文字にし順序を逆にするまでが自動変換処理となります。

参考資料

PHPマニュアル: str_replace
PHPマニュアル: preg_replace_callback - staticメソッドの利用法
Wikipedia: ローマ字