PHP Tips | 文字列処理:文字数・切り出し – 指定開始位置から安全に部分取得

PHP PHP
スポンサーリンク

このユーティリティがやりたいことの全体像

「指定開始位置から安全に部分取得」というのは、ざっくり言うとこういうことです。

文字列の「何文字目から」「何文字分」を取り出したい。
しかも、日本語(マルチバイト)を途中で壊さず、安全に取り出したい。

例えば、こんな文字列があるとします。

$text = "これはサンプルの文章です";
PHP

ここから「3文字目から5文字分」を取り出したい、というときに、

これはサンプルの文章です
  ↑ここから5文字 → 「はサンプル」

のように、狙った位置から、狙った長さだけを「文字単位」で取り出す——
これを「安全に」やるのが今回のテーマです。


なぜ「安全に」が必要なのか(substr の罠)

substr は「バイト数」で数える

PHP の substr() は、文字ではなく「バイト数」で位置を指定します。

$text = "これはサンプル";

$part = substr($text, 2, 4);  // ダメな例(バイト単位)

echo $part;
PHP

UTF-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;
    }
}
PHP

mb_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');
}
PHP

mb_substr の第3引数(長さ)を null にすると、「開始位置から最後まで」という意味になります。

「ここから先は全部欲しい」というときに便利です。

5. 長さがマイナスの場合の扱い

if ($length < 0) {
    return '';
}
PHP

mb_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

タイトルとURLをコピーしました