(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から修正される予定です。
PHPのmb_check_encoding関数が一体何のチェックをしているのか、エンコーディングごとに一通り調べてみます。
まずはSJISとSJIS-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 ∵
これらの文字は全て代替の文字があり、WindowsでIMEから入力しようとしても、他の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バイト(バグかも)