このユーティリティがやりたいことの全体像
「指定開始位置から安全に部分取得」というのは、ざっくり言うとこういうことです。
文字列の「何文字目から」「何文字分」を取り出したい。
しかも、日本語(マルチバイト)を途中で壊さず、安全に取り出したい。
例えば、こんな文字列があるとします。
$text = "これはサンプルの文章です";
PHPここから「3文字目から5文字分」を取り出したい、というときに、
これはサンプルの文章です
↑ここから5文字 → 「はサンプル」
のように、狙った位置から、狙った長さだけを「文字単位」で取り出す——
これを「安全に」やるのが今回のテーマです。
なぜ「安全に」が必要なのか(substr の罠)
substr は「バイト数」で数える
PHP の substr() は、文字ではなく「バイト数」で位置を指定します。
$text = "これはサンプル";
$part = substr($text, 2, 4); // ダメな例(バイト単位)
echo $part;
PHPUTF-8 の日本語は 1文字が 3バイトになることが多いので、
「2バイト目から4バイト分」といった指定は、ほぼ確実に「文字の途中」で切れてしまいます。
結果として、
- 文字化けする
- 意味不明な文字列になる
- 後続の処理でエラーの原因になる
といった問題が起こります。
「文字数」で考えるなら mb_substr 一択
日本語を含む文字列を「人間の感覚どおり」に扱いたいときは、
- 文字数を数える:
mb_strlen - 部分文字列を取得する:
mb_substr
このセットを使うのが必須です。
mb_substr は、「開始位置」も「長さ」も「文字数単位」で指定できます。
これが「安全に部分取得」の土台になります。
mb_substr の基本を押さえる
mb_substr のシグネチャ
mb_substr はこういう形をしています。
string mb_substr(
string $string,
int $start,
?int $length = null,
?string $encoding = null
)
PHPそれぞれの意味はこうです。
- 第1引数:元の文字列
- 第2引数:開始位置(0 が先頭)
- 第3引数:取得する文字数(省略すると「最後まで」)
- 第4引数:エンコーディング(
'UTF-8'を明示するのが基本)
具体例でイメージをつかむ
$text = "これはサンプルの文章です";
// 先頭から3文字
echo mb_substr($text, 0, 3, 'UTF-8'); // これはサ
// 3文字目から5文字
echo mb_substr($text, 2, 5, 'UTF-8'); // はサンプル
// 4文字目から最後まで
echo mb_substr($text, 3, null, 'UTF-8'); // プルの文章です
PHPここでのポイントは、
- 「何文字目から」を 0 始まりで数える(0,1,2,…)
- 「何文字分」を指定するのも「文字数」単位
- エンコーディング
'UTF-8'を必ず明示する
この3つです。
「安全に部分取得」するためのユーティリティ関数
完成形を先に見てみる
実務でそのまま使える形の関数を先に出します。
/**
* 指定開始位置からマルチバイト安全に部分取得(UTF-8 前提)
*
* @param string $text 対象文字列
* @param int $start 開始位置(0 が先頭)
* @param int|null $length 取得する文字数(null なら最後まで)
* @return string 取得した部分文字列(範囲外なら空文字)
*/
function mb_substring_safe(string $text, int $start, ?int $length = null): string
{
// 全体の文字数
$totalLength = mb_strlen($text, 'UTF-8');
// 開始位置が文字列の長さ以上なら、空文字を返す
if ($start >= $totalLength) {
return '';
}
// 開始位置がマイナスなら、後ろから数える(例: -1 は最後の1文字)
if ($start < 0) {
$start = $totalLength + $start;
if ($start < 0) {
$start = 0;
}
}
// 長さが指定されていない場合は、最後まで
if ($length === null) {
return mb_substr($text, $start, null, 'UTF-8');
}
// 長さがマイナスの場合は、「末尾から何文字残すか」という意味になるので、
// ここでは「安全のため 0 とみなす」など、プロジェクト方針で決める。
if ($length < 0) {
return '';
}
// 開始位置+長さが全体を超えていても、mb_substr は安全に処理してくれるが、
// 明示的に調整しておきたいならここで制御してもよい。
return mb_substr($text, $start, $length, 'UTF-8');
}
PHPまずはシンプルな使い方
$text = "これはサンプルの文章です";
// 2文字目から5文字
echo mb_substring_safe($text, 1, 5); // れはサンプル
// 3文字目から最後まで
echo mb_substring_safe($text, 2, null); // はサンプルの文章です
PHP関数の中身を一つずつかみ砕く
1. 全体の文字数を把握する
$totalLength = mb_strlen($text, 'UTF-8');
PHPここで、「この文字列は全部で何文字あるか」を知っておきます。
これを基準にして、「開始位置が範囲内かどうか」「マイナス指定をどう扱うか」を判断します。
2. 開始位置が範囲外なら空文字を返す
if ($start >= $totalLength) {
return '';
}
PHP例えば、文字列が 10 文字しかないのに、$start = 20 のような指定が来た場合、
「何も取れない」のが自然です。
ここで空文字を返しておくと、呼び出し側も扱いやすくなります。
3. マイナスの開始位置を「後ろから数える」として扱う
if ($start < 0) {
$start = $totalLength + $start;
if ($start < 0) {
$start = 0;
}
}
PHPmb_substr 自体も、マイナスの開始位置を「後ろから数える」として扱ってくれますが、
自分で明示的に補正しておくと、挙動がイメージしやすくなります。
例えば、文字列が 10 文字のときに、
$start = -1→ 9 文字目(最後の1文字)$start = -3→ 7 文字目(後ろから3文字目)
というように変換されます。
$totalLength + $start でこれを実現しています。
4. 長さが null のときは「最後まで」
if ($length === null) {
return mb_substr($text, $start, null, 'UTF-8');
}
PHPmb_substr の第3引数(長さ)を null にすると、「開始位置から最後まで」という意味になります。
「ここから先は全部欲しい」というときに便利です。
5. 長さがマイナスの場合の扱い
if ($length < 0) {
return '';
}
PHPmb_substr では、長さをマイナスにすると「末尾から何文字残すか」という意味になりますが、
初心者向けユーティリティとしては、ここを許可すると混乱の元になります。
なので、
「この関数では、長さは 0 以上の整数だけを受け付ける」
というルールにしてしまい、マイナスが来たら空文字を返す、という方針にしています。
(ここはプロジェクトの方針に合わせて変えても構いません)
実務での具体的なシチュエーション例
例題1:ページングされたテキストの「続きを読む」
長い本文を、最初の一部だけ表示して「続きを読む」リンクを出す、というよくあるパターン。
$body = $row['body']; // プレーンテキスト前提
// 先頭から 100 文字だけ表示
$preview = mb_substring_safe($body, 0, 100);
echo nl2br(htmlspecialchars($preview, ENT_QUOTES, 'UTF-8'));
// 「続きを読む」リンクは別途
PHPここで substr を使ってしまうと、日本語が途中で壊れる可能性があります。mb_substring_safe を使えば、「100文字」という感覚どおりに切り出せます。
例題2:固定フォーマットの一部を取り出す
例えば、ログの1行がこういう形式だとします。
[2025-01-01 12:34:56] INFO user_id=12345 action=login
ここから「ログレベル(INFO)」だけを取り出したい、というケース。
$line = "[2025-01-01 12:34:56] INFO user_id=12345 action=login";
// 先頭から 21 文字は日時とスペースだと分かっているとする
$level = mb_substring_safe($line, 22, 4); // "INFO"
echo $level;
PHPログが英数字だけなら substr でも動きますが、
日本語が混ざる可能性があるなら、最初から mb_substring_safe に統一しておく方が安心です。
もう少しだけ踏み込んだ話
「開始位置」と「長さ」をどう数えるかの共通認識
この手の関数で一番ハマりやすいのは、「何文字目から?」の数え方です。
- プログラムの世界:0 始まり(0,1,2,…)
- 人間の感覚:1 始まり(1文字目、2文字目,…)
mb_substring_safe のような関数をチームで使うときは、
「開始位置は 0 始まりで指定する」
というルールを、コメントやドキュメントに明記しておくと、後から混乱しません。
「安全」とは「範囲外でも壊れない」こと
今回の関数で意識している「安全さ」は、主に次の2つです。
- マルチバイト文字を途中で切らない(
mb_substrを使う) - 範囲外の開始位置やおかしな長さが来ても、エラーではなく「空文字」などで返す
これによって、
- 呼び出し側が「毎回ガードを書く」必要が減る
- 想定外の入力が来ても、致命的なエラーになりにくい
というメリットがあります。
まとめ:今日からの「部分取得」の基本形
押さえておきたいポイントをコンパクトにまとめると、こうなります。
- 日本語を含む文字列の部分取得は、
substrではなく必ずmb_substrを使う。 - 「開始位置」「長さ」は「文字数単位」で考える。
- 範囲外の開始位置やマイナス値などをどう扱うかを、ユーティリティ関数の中で決めておくと、呼び出し側が楽になる。
そして、実務ユーティリティとしては、この関数をプロジェクト共通で置いておくとかなり便利です。
function mb_substring_safe(string $text, int $start, ?int $length = null): string
{
$totalLength = mb_strlen($text, 'UTF-8');
if ($start >= $totalLength) {
return '';
}
if ($start < 0) {
$start = $totalLength + $start;
if ($start < 0) {
$start = 0;
}
}
if ($length === null) {
return mb_substr($text, $start, null, 'UTF-8');
}
if ($length < 0) {
return '';
}
return mb_substr($text, $start, $length, 'UTF-8');
}
PHP