PHPでテキスト処理

PHPでテキスト処理を書いた時のメモ

UTF-8の文字列を文字の配列に分割する

最初に書いたというか、Webからコピペして整形したコードはコレ。

function mbStringToArray($text,$enc="UTF8"){
    $result= array();
    $len = mb_strlen($text,$enc);
    while($len){
        $result[] = mb_substr($text,0,1,$enc);
        $text = mb_substr($text,1,--$len,$enc);
    }
    return $result;
}

53k文字のテキストで11秒とかヘコむ。mb_substrを使いまくりなのが重い理由。
1回目のは先頭から1文字を切り出してるだけだからいいけど、2回目のは「残り全部」を計算するのにわざわざテキストを最後までスキャンすることになって、そんなに長くないテキストでも目に見えて重い。

そこで2回目のスキャンを省略したのがコレ。

function mbStringToArray2(&$result,$text,$enc="UTF-8"){
    $len = strlen($text);
    while($text != ''){
        $a = mb_substr($text,0,1,$enc);
        $text = substr($text,strlen($a));
        $result[] = $a;
    }
}

53k文字のテキストで1.45秒。大分マシになったが、大きな部分文字列の切り出しがまだ重い。

で、困った時の正規表現頼みで書き換えたのがコレ。

function text_split_utf8(&$result,$text){
	$match = array(); $offset = 0;
	while(preg_match('/[\x00-\x7f]|[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/',$text,$match,PREG_OFFSET_CAPTURE,$offset)){
		$result[] = $match[0][0];
		$offset = $match[0][1]+strlen($match[0][0]);
	}
}

53k文字のテキストで0.3秒。substr多用から比べると、驚きの早さ。

そういや、ここまでは出力を配列へのリファレンスにしてたけど、配列を返すとどのくらい変わるんだろう。

function text_split_utf8_b($text){
	$result = array();
	$match = array(); $offset = 0;
	while(preg_match('/[\x00-\x7f]|[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/',$text,$match,PREG_OFFSET_CAPTURE,$offset)){
		$result[] = $match[0][0];
		$offset = $match[0][1]+strlen($match[0][0]);
	}
	return $result;
}

53k文字のテキストで0.3秒。あれ、コストは変わらないんだ…。

mb_strlenは重い。ではstrlenは?

てな話をIRCでしてたら、「PHPはstrlenが重い」という話題があった。ついでにコレもベンチマークしてみる。

0.024615/100 = 0.000246 sec/call : case 92441 mb_strlen
0.000089/100 = 0.000001 sec/call : case 4 mb_strlen
0.000072/100 = 0.000001 sec/call : case 92441 strlen
0.000087/100 = 0.000001 sec/call : case 4 strlen
0.017745/100 = 0.000177 sec/call : case 92441 mb_substr
0.000105/100 = 0.000001 sec/call : case 92441 substr

strlenの方は、長さに依存した差は出なかった。まあ普通は言語の内部で文字列の長さは保持してるだろうし妥当な結果。ただ、環境によってはstrlenをmb_strlenでオーバライドして使ってる場合もあるので注意だね。

それに比べるとmb_strlenは、1文字1文字のバイト幅を調べるので重い。
substrとmb_substrにも同様の違いがある。これはもう仕方ないね。