2026.09.23 泊まれる場所4,223 立ち寄りスポット59,693 営業中を確認したキャンプ場1,005 note を読む

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が入っているだけで、「存在しない」と判定されていた

出典

まる子パパ

まる子パパ

会社員。受託開発のエンジニアを15年以上続けていて、その前は寿司職人・長距離運転手・農業。 技術ブログは、キャンプの合間に踏んだバグと、AI・Web開発の運用の失敗を書いています。 このサイト自体が、開発に携わっているCMS「コンテナ」の稼働中の実例です。 このサイトについて / 小さな自動化の相談

—

ほかにも書いています

受託・note・スタンプ

※受託の相談とnoteは、運営者が個人で受けている・書いているものです。

技術ブログ一覧へ戻る