hnwの日記

mb_check_encodingは何をチェックするのか(その1 SJIS編)


(2009/02/15 17:20)「個人的な感想」を追記しました。また、下記はPHP5.2.1以降の挙動です。PHP5.2.0以前のmb_check_encodingは更にカオスなので、あまり使い物にならないと思います。



(2009/02/16 12:30)追記2:バグっぽいと思った件は本当にバグで、修正がhttp://news.php.net/php.cvs/56276の通り取り込まれました。PHP5.2.9から修正される予定です。



(2009/02/22 16:20)追記3:他のエンコーディングについても調査しました。「(その2 EUC-JP編)」と「(その3 UTF-8編)」も合わせてご覧下さい。


PHPのmb_check_encoding関数が一体何のチェックをしているのか、エンコーディングごとに一通り調べてみます。


まずはSJISSJIS-win(CP932)について調べてみました。

SJIS

SJISというのはJISの第一水準&第二水準のエンコーディング形式の一つ、というくらいの理解で良いと思います。つまり、1区1点から94区94点までにより構成され、ISO2022-JPやEUC-JPと漏れなく相互変換が可能です。


mb_check_encodingに関して言うとまさにその通りで、0x8140(1区1点)から0xEFFC(94区94点)まで、94*94=8836文字全てがtrueになります。この範囲には未使用の区や未定義の文字も多数含まれますが、これも含め「SJISとして正しい」ということなのでしょう。


また、0x00から0x7Fまでの7bit文字と、0xA1から0xDFまでの半角カナについては、1バイトで正しい文字として扱われます。


逆に、不正な文字列として扱われるのは次のような文字です。

  • 1byteでは存在し得ない文字で終わっている文字列。つまり、1byte文字または2byte文字に続いて0x80から0xA0、0xE0から0xFFまでの1byteがある場合。
  • 95区以降の2byte文字。0xF0 0x40など。
  • 2byte文字の1byte目に続いて2byte目には有り得ない1byteが続く場合。0x81 0xFDなど。


…と思ったのですが、実は0x81 0x3AなどはSJISとして不正なはずなのにmb_check_encodingがtrueを返します。

$ php -r 'var_dump(mb_check_encoding("\x81\x3a", "SJIS"));'
bool(true)


僕の誤解でなければバグレポを出そうかと思うのですが、このバイト列には何か歴史的経緯があったりするんでしょうか。


手元の環境で言うと、次のような2バイトがSJISとして正しいと認識されるようです。

  • 1バイト目: 0x81から0x88、0xEBから0xEFの13個のいずれか
  • 2バイト目: 0x3Aから0x3Fの6個のいずれか


この条件のうち0x82 0x3Aと0x82 0x3Fを除いた76パターンについて、mb_check_encodingがtrueになります。

SJIS-win

SJIS-winというのは、先のSJISに加えてIBM拡張文字やら何やらを含んでるもの、というくらいの認識で良いかと思います。SJISとして表現できる全ての文字を使う代わりに他のISO-2022系のエンコーディングに変換できないかもしれないエンコーディング、くらいに考えても良いでしょう(CP51932やらISO-2022-JP-MSやらがあるので、PHPについて言えば変換できるんですけどね)。PHP的には「CP932」も同じ意味です。


ところで、mb_check_encodingの第2引数にSJIS-winを与えたときの挙動は、SJISのときの単純な延長とはなっていません。0x8140(1区1点)から0xFCFC(120区94点)まで全ての2バイト文字のうち、以下の文字に対してfalseを返します。

  • 13区(NEC特殊文字)の9文字
    • 0x8790(≒), 0x8791(≡), 0x8792(∫), 0x8795(√), 0x8796(⊥), 0x8797(∠), 0x879A(∵), 0x879B(∩), 0x879C(∪)
  • 89区から92区(0xED40から0xEEFCまで、いわゆる「NEC選定IBM拡張文字」)の374文字
    • 上記範囲のうち、0xEEEDと0xEEEE(この2文字は字体が未定義)は除く
  • 115区(IBM拡張文字)の15文字
    • 0xFA4Aから0xFA53(ローマ数字のIからX)
    • 0xFA54 ¬
    • 0xFA58 (株)
    • 0xFA59 No.
    • 0xFA5A TEL
    • 0xFA5B ∵


これらの文字は全て代替の文字があり、WindowsIMEから入力しようとしても、他の2バイト文字として入力されてしまいます。


それ以外については基本的にSJISと同様です。謎の2バイトを正しいSJIS-winとして判断する点も同じですが、これは少しだけ範囲が異なります。

  • 1バイト目: 0x81から0x88、0xEBから0xED、0xF0の12個のいずれか
  • 2バイト目: 0x3Aから0x3Fの6個のいずれか


この条件のうち0x82 0x3Aと0x82 0x3Fを除いた70パターンについて、mb_check_encodingがtrueになります。

UTF-8へのマッピング

上で見てきたように、mb_check_encodingは字体が存在するかどうかをチェックする関数ではありません。mb_check_encodingがtrueを返した場合でも、可読かどうか、UTF-8に変換できるかどうか、などは判断できません。


たとえば、既に登場した13区のNEC特殊文字、0x8790(≒)について考えてみます。この文字をmb_check_encodingにかけてみると、"SJIS"ではtrue、"SJIS-win"ではfalseとなります。一方で、mb_convert_encodingでUTF-8に変換しようとすると、"SJIS"からだと"?"になってしまいます。SJIS-winからだと適切な文字(U+2252)になります。

個人的な感想

僕としては「不正な文字として解釈されるかもしれないバイト列を検出する」という意図の関数だと期待していたので、SJIS-winの重複文字のチェックは余計な気がします。


「どういう意図で何をチェックしているのか」がわかれば使いどころも明らかになると思いますが、僕にはよくわかりませんでした。

まとめ

  • mb_check_encodingがtrueを返したからといって、文字として可読とは限らない。
    • 逆に、falseを返しても可読なことがある。
    • UTF-8への変換が可能かどうかとも無関係。
  • mb_check_encoding($str, "SJIS")は、下記を正しい文字として認識し、それ以外を不正とみなす
    • 0x00から0x7Fまでの1バイト文字
    • 1区から94区までのSJIS2バイト文字
    • 0xA1から0xDFまでの半角カナ
    • 謎の2バイト(バグかも)
  • mb_check_encoding($str, "SJIS-win")は、下記を正しい文字として認識し、それ以外を不正とみなす
    • 0x00から0x7Fまでの1バイト文字
    • 1区から120区までのSJIS2バイト文字
      • ただし、13区、89区から93区、115区の重複文字は不正とみなす
    • 0xA1から0xDFまでの半角カナ
    • 謎の2バイト(バグかも)