文字エンコーディング判定スクリプト

最近、忙しかったのですが、久しぶりに少し時間があったので、随分前に書いた文字エンコーディング判定スクリプトを見直してみました。とりあえず、出来上がったものを投稿しておきます。
何か適当な日本語の文字列を与えると、JIS/eucJP-win/SJIS-win/UTF-8 からそれなりに妥当な文字エンコーディングを返します。短い文字列で、mb_detect_encoding() だけでは判定に失敗する場合に役立つかもしれません。PHP 5.2.12 での動作を確認しています。

<?php
/**
 * 日本語文字列の文字エンコーディング判定(ASCII/JIS/eucJP-win/SJIS-win/UTF-8)
 */
function detect_encoding_ja( $str )
{
  $enc = mb_detect_encoding( $str, 'ASCII,JIS,eucJP-win,SJIS-win,UTF-8', TRUE );

  switch ( $enc ) {
  case FALSE    :
  case 'ASCII'  :
  case 'JIS'    :
  case 'UTF-16' :
  case 'UTF-8'  : break;
  case 'eucJP-win' :
    // ここで eucJP-win を検出した場合、eucJP-win として判定
    if ( mb_detect_encoding( $str, 'SJIS-win,UTF-8,eucJP-win', TRUE ) === 'eucJP-win' ) {
      break;
    }
    $_hint = "\xbf\xfd" . $str; // "\xbf\xfd" : EUC-JP "雀"

    // EUC-JP -> UTF-8 変換時にマッピングが変更される文字を削除( ≒ ≡ ∫ など)
    mb_regex_encoding( 'eucJP-win' );
    $_hint = mb_ereg_replace(
      "\xad[\xe2\xf5\xf6\xf7\xfa\xfb\xfc\xf0\xf1\xf2\xf5\xf6\xf7\xfa\xfb\xfc]|" .
      "\x8f\xf3[\xfd\xfe]|\x8f\xf4[\xa1-\xa8\xab\xac\xad]|\x8f\xa2\xf1",
      '', $_hint );

    $_tmp  = mb_convert_encoding( $_hint, 'UTF-8', 'eucJP-win' );
    $_tmp2 = mb_convert_encoding( $_tmp,  'eucJP-win', 'UTF-8' );
    if ( $_tmp2 === $_hint ) {
      // 例外処理( EUC-JP 以外と認識する範囲 )
      if (
        // SJIS と重なる範囲(2バイト|3バイト|iモード絵文字|1バイト文字)
        ! preg_match( '/^(?:'
        . '(?:[\x8e\xe0-\xe9][\x80-\xfc])+|'
        . '(?:\xea[\x80-\xa4])+|'
        . '(?:\x8f[\xb0-\xef][\xe0-\xef][\x40-\x7f])+|'
        . '(?:\xf8[\x9f-\xfc])+|'
        . '(?:\xf9[\x40-\x49\x50-\x52\x55-\x57\x5b-\x5e\x72-\x7e\x80-\xb0\xb1-\xfc])+|'
        . '[\x00-\x7e]+'
        . ')+$/', $str ) &&

        // UTF-8 と重なる範囲(全角英数字・記号|漢字|1バイト文字)
        ! preg_match( '/^(?:'
        . '(?:\xef[\xbc-\xbd][\x80-\xbf])+|(?:\xef\xbe[\x80-\x9f])+|(?:\xef\xbf[\xa0-\xa5])+|'
        . '(?:[\xe4-\xe9][\x8e-\x8f\xa1-\xbf][\x8f\xa0-\xef])+|'
        . '[\x00-\x7e]+'
        . ')+$/', $str )
      ) {
        // 条件式の範囲に入らなかった場合は、eucJP-win として検出
        break;
      }
      // 例外処理2(一部の頻度の多そうな熟語を eucJP-win として判定)
      // (狡猾|珈琲|琥珀|瑪瑙|碼碯|絨緞|耄碌|膃肭臍|薔薇|蜥蜴|蝌蚪)
      if ( preg_match( '/^(?:'
        . '\xe0\xc4\xe0\xd1|\xe0\xdd\xe0\xea|\xe0\xe8\xe0\xe1|\xe0\xf5\xe0\xef|'
        . '\xe2\xfb\xe2\xf5|\xe5\xb0\xe5\xcb|\xe6\xce\xe2\xf1|\xe9\xac\xe9\xaf|'
        . '\xe9\xf2\xe9\xee|\xe9\xf8\xe9\xd1|\xe7\xac\xe6\xed\xe7\xc1|'
        . '[\x00-\x7e]+'
        . ')+$/', $str )
      ) {
        break;
      }
    }

  default :
    // ここで SJIS-win と判断された場合は、文字コードは SJIS-win として判定
    $enc = mb_detect_encoding( $str, 'UTF-8,SJIS-win', TRUE );
    if ( $enc === 'SJIS-win' ) {
      break;
    }
    $enc = 'SJIS-win';

    // UTF-8 の記号と日本語の範囲の場合は UTF-8 として検出(記号|全角英数字・記号|漢字|1バイト文字)
    if ( preg_match( '/^(?:'
      . '(?:[\xc2-\xd4][\x80-\xbf])+|'
      . '(?:\xef[\xa4-\xab][\x80-\xbf])+|'
      . '(?:\xef[\xbc-\xbd][\x80-\xbf])+|'
      . '(?:\xef\xbe[\x80-\x9f])+|'
      . '(?:\xef\xbf[\xa0-\xa5])+|'
      . '(?:[\xe2-\xe9][\x80-\xbf][\x80-\xbf])+|'
      . '[\x09\x0a\x0d\x20-\x7e]+|'
      . ')+$/', $str )
      ) {
      $enc = 'UTF-8';
    }
    // UTF-8 と SJIS 2文字が重なる範囲への対処(SJIS を優先)
    if ( preg_match( '/^(?:[\xe4-\xe9][\x80-\xbf][\x80-\x9f][\x00-\x7f])+/', $str ) ) {
      $enc = 'SJIS-win';
    }
  }
  return $enc;
}

SJIS-win と UTF-8 が重なる部分の判定が難しいので、場合によってはうまく判定できない場合があります。

以下については、以前に書いた時と変わりません。

  • EUC-JP のいわゆる半角カタカナ(\x8e[\xa1-\xfc])や、EUC-JP の一部の文字([\xe0-\xea][\xa1-\xfc])のみで構成される文字列は SJIS-win として判定される
  • UTF-8 の一部の記号(数学記号とギリシア文字の一部)が含まれる文字列が eucJP-win として判定される