PHPのstrlenとmb_strlenの違い|「まる子」はstrlenだと9になる。serializeの数字もバイト数だった
PHPのstrlenはバイト数、mb_strlenは文字数を返す。
UTF-8の日本語は多くが1文字3バイトなので、文字数制限や切り詰めでずれる。自分のCMSが本文をserializeで保存していて大事だった違いを、PHPマニュアルと実行結果で整理した。
PHPの strlen と mb_strlen の違いは、数えているものが違う、の一点だ。strlen はバイト数、mb_strlen は文字数を返す。
UTF-8の日本語は、ひらがなや漢字の多くが1文字3バイトなので、strlen('まる子') は 3 ではなく 9 になる。
この記事では、PHPマニュアルの説明と、手元の PHP 8.3 で実際に動かした結果で、この違いと取り違えやすい場面を整理する。
関数の仕様は、どれも PHP マニュアルに書いてあることだ。
実行結果は、このサイトの開発環境(PHP 8.3)で僕が動かして確かめた。
「僕は〜と思う」と書いたところだけが、僕の意見になる。
まず結果だけ並べてみる
strlen('a'); // 1
strlen('あ'); // 3
mb_strlen('あ'); // 1
strlen('まる子'); // 9
mb_strlen('まる子'); // 3
strlen('🐶'); // 4
mb_strlen('🐶'); // 1英数字だけなら、どちらで数えても同じ数になる。
日本語が1文字でも混ざると、そこから数字が合わなくなる。
絵文字はさらに大きく、犬の絵文字1つで4バイトだった。
strlenが返すのはバイト数
PHPマニュアルの strlen のページには、文字列の長さをバイト単位で返す、と書いてある。
わざわざ注意書きとして、返すのはバイト数で文字数ではない、とまで念を押している。
バイトは、コンピュータが文字を保存するときの箱の数だと思えばいい。
UTF-8という文字コードでは、英数字は1箱、日本語の多くは3箱、絵文字は4箱を使う。strlen は、この箱の数を数えている。
mb_strlenが返すのは文字数
mb_strlen は、マルチバイト文字の1文字を1個として数える、とマニュアルにある。
人間が「3文字」と思う数え方に近いのはこちらだ。
2つめの引数で文字コードを指定できる。
省略すると内部文字エンコーディングが使われるので、設定が UTF-8 以外になっている環境では結果が変わることがある。
僕の手元は mb_internal_encoding() が UTF-8 を返したので、省略しても上の結果になった。
取り違えやすい場面
1. 文字数の制限チェック
「お名前は20文字まで」のような入力チェックを strlen で書くと、日本語だと7文字ほどで弾かれてしまう。
20バイトは、3バイトの文字なら6文字と2バイトぶんしか入らないからだ。
人に見せる「◯文字まで」は、mb_strlen で数えるのが筋だと僕は思う。
2. substrで切ると、文字が壊れる
一覧に本文の冒頭だけ出したいとき、substr で切ると文字の途中で切れることがある。
$s = 'キャンプ場';
substr($s, 0, 4); // "キ" と、壊れた1バイト
mb_substr($s, 0, 2); // "キャ"substr もバイト単位で切るので、4バイト目で切ると「キ」(3バイト)のあとに「ャ」の1バイト目だけが残る。
画面では「キ�」のように、読めない記号として出てしまう。mb_substr は文字の位置で数えるので、こういう切れ方はしない。
3. 見た目の幅で切りたいとき
全角と半角が混ざった見出しを、同じくらいの見た目の長さでそろえたいなら mb_strimwidth がある。
マニュアルによると、半角文字は1、全角文字は2として幅を数える。
mb_strimwidth('キャンプ場の予約は早めに', 0, 10, '…');
// "キャンプ…"最後の引数は、切ったときに末尾に付ける文字だ。
文字数ではなく「幅」で切るので、英数字の多いタイトルと日本語のタイトルが並んでも、見た目の長さがそろいやすい。
僕のCMSで、この違いが大事だった理由
このサイトは、自分で作ったPHPのCMSで動いている。
記事の本文は、PHPの serialize という形でデータベースに保存している。
serialize('まる子');
// s:9:"まる子";この s:9 の 9 は、文字数ではなくバイト数だ。
手元で s:3:"まる子"; と、文字数で書き換えたものを unserialize に渡すと、結果は false になった。
数字が1つずれただけで、中身ごと読めなくなる。
実際に、7月には本文に改行文字が混ざったSQLを本番へ入れて、記事の本文が unserialize に失敗したことがある。
それ以来、記事を入れるSQLは手で組まず、PHP自身に serialize させるスクリプトで作っている。
バイト数の計算を人間がやらない、というのが一番の対策だと僕は思う。
似た話で、リード文に「\n」という2文字がそのまま表示されていたことも前に書いた。
こちらは改行の書き方を一文字直しただけで、二十一記事がまとめて直った。
絵文字は、mb_strlenでも1にならないことがある
家族の絵文字のように、いくつかの絵文字をつなげて1つに見せているものがある。
手元で試すと、strlen は 18、mb_strlen は 5 だった。
画面では1文字に見えるのに、mb_strlen でも5と数える。
見た目どおりの1文字で数えたいときは、grapheme_strlen という関数がある。
同じ絵文字で試したら 1 になった。
ただし intl という拡張が入っている環境でしか使えないので、使う前に function_exists('grapheme_strlen') で確かめておくと安心だ。
まとめ
strlenはバイト数、mb_strlenは文字数を返す- UTF-8の日本語は多くが1文字3バイト、絵文字は4バイト
- 人に見せる「◯文字まで」は
mb_strlen、文字の途中で切りたくないならmb_substr - 見た目の幅でそろえたいなら
mb_strimwidth serializeのs:の数字はバイト数。手で書き換えない
PHPのほかの「似ているのに違う」話は、こちらにも書いている。
issetとarray_key_existsの違い|nullが入っているだけで、「存在しない」と判定されていた
出典
ほかにも書いています
受託・note・スタンプこういう仕組みを作る相談を受けています
毎日の更新・データ集め・集計の自動化/3万円〜収益がいくらだったか、どの施策が外れたか
note / 個人の視点で掘り下げていますスタンプラリーに挑戦する
行った場所は、現地チェックインで記録に残せます※受託の相談とnoteは、運営者が個人で受けている・書いているものです。
