PHP ではマルチバイトの文字数を求めるのに mb_strlen
が使われますが、本体にもマルチバイトを扱う機能があります。次のコードで htmlspecialchars で文字数を求めてみます。
コード
$str = 'あいうえお';
var_dump(5 === str_len($str, 'UTF-8'));
function str_len(string $str, string $encoding = 'UTF-8'): int
{
$str = str_scrub($str, $encoding);
$size = strlen($str);
$length = 0;
$char = '';
for ($i = 0; $i < $size; ++$i) {
$char .= $str[$i];
if (str_valid_encoding($char, $encoding)) {
++$length;
$char = '';
}
}
return $length;
}
function str_valid_encoding(string $str, string $encoding = 'UTF-8'): bool
{
return $str === htmlspecialchars_decode(htmlspecialchars($str, ENT_QUOTES, $encoding));
}
function str_scrub(string $str, string $encoding = 'UTF-8'): string
{
return htmlspecialchars_decode(htmlspecialchars($str, ENT_SUBSTITUTE, $encoding));
}
コードの解説
最初にユーザー定義関数の str_scrub
を使って不正なバイト列を置き換えます。ENT_SUBSTITUTE
フラグを指定しています。PHP 8.1 であれば ENT_SUBSTITUTE
はデフォルトフラグなので、指定する必要はありません。同等の機能をもつ mb_scrub
は PHP 7.2.0 とそれ以降のバージョンで利用できます。
for ループのなかでは配列アクセス構文を使い、前から1バイトずつ取り出して連結し、正しい文字エンコーディングであるかどうかを str_valid_encoding
でチェックしています。文字はそのままではわかりづらいので、バイト列の16進数表記を bin2hex
で求めてみます。
// e3 81 82 e3 81 84 e3 81 86 e3 81 88 e3 81 8a"
var_dump(bin2hex('あいうえお'));
「あいうえお」は次のように表記することもできます。
// あいうえお
var_dump("\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86\xe3\x81\x88\xe3\x81\x8a");
1文字目の「あ」の処理について調べてみましょう。「あ」を構成するバイトは16進数で「e3 81 82」です。
1、2回目のループで $char
に投入された「0xe3」および「0xe3 0x81」は「あ」の一部しかない不正なバイト列なので、str_valid_encoding
の判定は false になります。3回目のループで $char
は「e3 81 82」のバイト列になり、正しい文字エンコーディングなので、$length
の値は 1
増えます。計算が終わったら $char
を空にして次の文字に備えます。
冗長ですが、次のように「あ」はチェックされました。
// false
// false
// true
var_dump(
str_valid_encoding("\xe3"),
str_valid_encoding("\xe3\x81"),
str_valid_encoding("\xe3\x81\x82")
);
function str_valid_encoding(string $str, string $encoding = 'UTF-8'): bool
{
return $str === htmlspecialchars_decode(htmlspecialchars($str, ENT_QUOTES, $encoding));
}
最後にパフォーマンスに関して、1文字ずつバイトを取り出す必要があるので、mb_strlen
よりもだいぶ遅くなりますので、実用では使い物にはなりません。文字コードの学習教材にとどめておくべきでしょう。次の簡易ベンチマークコードでは40倍以上の速度の差がありました。
echo 'str_len', PHP_EOL,
benchmark(function() {
str_len("あいうえお");
}, 300000), PHP_EOL;
echo 'mb_strlen', PHP_EOL,
benchmark(function() {
mb_strlen("あいうえお");
}, 300000), PHP_EOL;
function benchmark(callable $callable, int $runs)
{
$start = microtime(true);
while(--$runs) {
$callable();
}
$stop = microtime(true);
return $stop - $start;
}
結果
str_len
0.67694401741028
mb_strlen
0.013685941696167