ようこそゲストさん

CPA-LABテクニカル

2008/05/15(木) 日本語文字列のバイト数取得にstrlenだけではダメな理由-PHP

日本語(マルチバイト)の処理において、その文字数ではなく、バイト数を取得したい場合があります。
マルチバイトは必ずしも1文字2バイトとは限らず、EUC-JPでは概ね2バイトだけど特殊文字で3バイトの場合があり、UTF-8では概ね3文字バイトで表されるけれども、そうでない場合もある。
マルチバイト文字はエスケープシーケンス等様々な理由で、日本語の文字数×nでは答えがでないのです。
もっと単純な話でいえば、シングルバイト文字とマルチバイト文字が混ざった文字列のバイト数は、もちろん文字数×nでは求めることができません。

まず現状での最善策をご紹介

$volm = strlen(bin2hex($data)) / 2;

$data:バイト数を取得したいデータ
$volm:データ長(byte)
これは私のオリジナルではありません。Script雑感-php:バイト数の取得(strlen は mb_strlen にオーバーロードされる)様の引用です。

Qdmailの修正中に、どんな環境でも問題なく、日本語(マルチバイト文字)のバイト数を取得する必要がでてきてきたのですが、どうしてもスマートな方法を考えつきませんでした。
そこに、Script雑感さんのページに遭遇しました。たいへん助かりました。

参考

 $str_len_byte = mb_ereg('.*', $string, $array);
という方法もあるようです。[pgsql-jp: 34062 データサイズの取得方法 ]しかし正規表現は速度的に不利なのとスマートでないのとmb_*環境がない場合には使えないので、bin2hex関数をお薦めします。

なぜ、strlen単独ではダメなのか?

オーバーロード


インターネットで検索するといくつか候補がみつかるのですが、その多くは次のようなものです。
(環境によって誤動作する例)

$volm = strlen($data);

$data:バイト数を取得したいデータ
$volm:データ長(byte)
しかし、これでは誤動作することがあるのです。

PHPでは、php.iniの設定によっては、勝手に strlen が mb_strlen としてオーバーロードされます。
つまり、strlenを使っているつもりで、いつのまにか、mb_strlenを使っていることがあるのです。
関数のオーバーロード機能-PHPマニュアル
したがって、次のような事態になることもあります。
バイト数を数えたかったのに、マルチバイトの文字数を数えてしまった。
これでは意図するプログラミングができません。

php.iniの設定を変えればいい話なのか?

関数のオーバーロード機能-PHPマニュアルにあるように、php.iniを変更すれば、これらの意図しない動作を押さえることができます。
しかし、このphp.iniのmbstring.func_overloadの値を変えると、他のスクリプトへの影響がでる可能性があり、別の誤動作を生む可能性があります。
また、set_ini では、このディレクティブを変更することはできない仕様になっています*1ので、スクリプト内で一時的にmbstring.func_overloadをゼロにする対応も難しいです。。
このディレクティブを変更できるかどうかもphp.iniで変更できないことはないのですが、話がややこしくなり、お薦めできません。

したがって、php.iniでの対応は、あまり望ましくないといえます。

また、「自分の環境では、mbstring.func_overloadはゼロだから、別にいいや」という考え方もできますが、そのスクリプトを別のサーバーに移動させたときなどに、異常がでる可能性があります。

bin2hex関数の働き

bin2hexは、シングルバイト文字を16進数表記に変換する関数です。16進数表記の場合、シングルバイト文字1文字は必ず2文字の16進数に変換されます*2
したがって、その16進数表記の半分が、当該バイト数というわけですね。

*1 : バージョン4.3以降

*2 : 9などの一桁数値もゼロつきの09で返してくれる

1: へろへろ 2009年09月09日(水) 午後5時50分

どのサイトを見てもstrlenは全角文字は2バイトですと嘘が書いてあり困っていました。ありがとうございました。

2: 浜村拓夫 2009年09月29日(火) 午前10時32分

参考になりました!
どうもありがとうございます。(・∀・)

3: ちびん 2009年12月13日(日) 午前11時42分

「あ」の長さ3バイトなんですよ。おかしいですよね。。
解決しました。本当ありがとうございましす!!

4: abc 2011年12月08日(木) 午後0時05分

意図しないところで勝手にオーバーロードされてるなんて怖いですね。

5: ぺろぺろ 2011年12月22日(木) 午後3時14分

unicodeが出る前までは日本語文字は2byteだったんだよ!

6: ぽんた 2012年10月26日(金) 午後4時57分

bin2hexはあくまで2進数表記を変換する関数であって、上記のような動作をするという表記は公式サイトに一言もありません。結果オーライで動作するというレベルでは、ほかの方法と変わりないような気がします...。


名前:  非公開コメント   

  • TB-URL  http://www.cpa-lab.com/tech/0144/tb/
  • CakePHP(RC1.2)のemailコンポーネントで日本語はまだ厳しい CPA-LABテクニカル spok
    ■CakePHP(1.2RC2)のemailコンポーネント評価CakePHP 1.2 RC2 がリリースされたので、新しいemailコンポーネントを評価してみた。全般的に言えば、改行コード&英文周りはかなり改善されており、英語メールを送る分には、大丈夫かと...
  • PHPのstrlen関数で全角文字が3バイトになる件 浜村拓夫の世界
    PHPのstrlen関数で、文字列のバイト数を取得しようとしたら、全角文字1文字が2バイトではなく3バイトとしてカウントされてしまった。全角文字の...

2008/05/15(木) CakePHPコントローラーをメール受信をきっかけに起動する

はてブ情報 はてブに登録 はてブ数 cakephpspok

CakePHPシェル機能を使用せずに、空メールを実現する-さくらインターネット編

CakePHPにはシェル機能というものがあり、コマンドラインからCakePHPを利用できる。
この機能を利用して、CakePHPをメール受信をきっかけにリアルタイム処理することもできる。
例えばこちらの記事はそう。Writing Some Code-メール受信からのシェル機能実行
しかし、学習の量は少なければ少ないほど効率がいい。
あることを実現するのに、別の事柄を学ぶよりも、これまでの延長線上で可能な方がいい。
(ただし、WEBブラウザから直接叩かれることも想定されるのでご注意を 参考:CakePHPシェルの使い方)
というわけでここでは、CakePHPのシェル機能は使わずに、普通のControllerを、メール受信をきっかけに起動することをやってみたい。
1.2betaで動作確認をしていますが、1.1でも動作するはずです。
これはさくらインターネットで試していますが、".mailfilter"の部分さえ、ご自分の環境に合わせて設定できれば、後は同じです。さくらインターネットと同じ環境ではない人は、こちらの阿部辰也のブログ――人生はひまつぶし。-メール受信時に perl スクリプトを起動して自動処理させる方法が参考になるでしょう*1

.mailfilterの設定

まず、.mailfilterに以下のように記述する。
mailfilterについては、さくらインターネットでメール受信をリアルタイムに処理する、を参照。

ここでは、receivers_contoroller.php に記述された ReceiversContorollerを起動することを意図してみよう。
cc "| /usr/local/php-5.2.6/bin/php-cgi -q /home/user_name/hogehoge/app/webroot/index.php url=receivers/index"
サーバーにメールを残す必要がない場合は、cc を to に変更する。
注意点 パラメーターの渡し方
urlのパラメーターは、次のように'?'を使用して記述した場合には失敗する。
index.php?url=receivers/index
次のように、index.phpとurl=の間には、半角空白を挟んで指定する。
index.php url=receivers/index
なお、
/usr/local/php-5.2.6/bin/php-cgi
は起動するPHPの本体プログラムの指定である。これについては、さくらインターネットでメール受信をリアルタイムに処理する を参照のこと。
-q は、httpヘッダーを出力しないようにする設定である。

次に、receivers_contoroller.php に記述された ReceiversContorollerを起動してみよう。
<?php

class ReceiversController extends AppController{

    var $uses = null;

    function index(){

        $fp=fopen("php://stdin",'r');
        $content = null;
        while( !feof( $fp ) ){
           $content .= fgets( $fp , 4096 );
        }
        fclose( $fp );

        $this->render('void','void');
    }
}
?>
これで、$contentにメールの中身が入ることになる。あとは自由に料理しよう。
ここで、メールの中身とは、いわゆる本文のことではなく、メールヘッダーも含めたメール全体のことを示すことに留意すること。
Pear::MIMEでも解析できるが、簡単なものであれば、preg_matchで十分だろう。
また、いずれPearを使用しない方式でのメールデコードを紹介したい。

なお
        $this->render('void','void');
としたのは、メール起動の場合、画面出力は意味がないからである。
しかるべき場所に、layoutのvoid.ctp、viewのvoid.ctpを何も記述しない空ファイルとして配置しておこう。(CakePHP1.1の場合は、void.thtmlとなる。)

*1 : perlの話ですが、参考になるはずです。

Qdmailと組み合わせて空メール処理をCakePHPのコントローラーで処理する

recevers_controller.phpを以下のようにする。*2
<?php

class ReceiversController extends AppController{

  var $uses = null;
  var $components = array('Qdmail');

  function index(){

    $fp=fopen("php://stdin",'r');
    $content=null;
    while( !feof($fp) ){
      $content .= fgets($fp,4096);
    }
    fclose($fp);

    $header = preg_split( '/\r?\n\r?\n/is' , $content );

    if( 0 !== preg_match('/from:\s*<?([^<>@ ]+@[^<>@ ]+)>?[^<>@\r\n\s,]*\r?\n/is',$header[0],$matches)){

    //---------------------------
    // 本来はその他にも、携帯端末からの
     // メールであることを確かめる
     // ルーチンを置いたほうがよい。
     // いたずら、迷惑メールの発信元に
     // ならないようにするためである。
     // mail address varidationも必要だろう
     //--------------------------

    $to = str_replace( array("\r","\n") , '' , $matches[1] );
    $this->Qdmail->to($to);
    $this->Qdmail->from('from@example.com');
    $this->Qdmail->subject('空メールのテストです。');
    $this->Qdmail->cakeText( $to ,  'index' , 'default' );
    $this->Qdmail->send();
    }
    $this->render('void','void');
  }
}
?>
view,layoutの置き場は、QdmailをCakePHPで使うをご参照下さい。

以下のQRコードを読み取って、空メールを送ってみて下さい。
上記のコードでもって実現した*3返信メールをお送りします。
qr_caketest.png
(あえてメールアドレスは書いていません。)
貴方のメールアドレスは保存しないのでご安心ください。信用できない方はメールを送らないでください。

*2 : セキュリティは自己責任で

*3 : 実際にはもう少し手が入っている

1: 開発者 2009年12月22日(火) 午前10時56分

この方法だと、さくらでPHPのバージョンアップがあると、PHPの実行パスが変わってしまうため、その度に修正しなければならなくなります。


名前:  非公開コメント   

  • TB-URL  http://www.cpa-lab.com/tech/0139/tb/
  • CakePHPのシェルコマンドをさくらインターネットで使う CPA-LABテクニカル spok
    メール受信をきっかけにCakePHPコントローラーを起動する では、その名の通り、コントローラーをメール受信で起動させた。ここでは、CakePHP1.2で導入されたシェル機能(shell)を利用し、CakePHPのシェルをメール受信をきっかけに起動させてみ...