[PHP]substr_replace関数を完全解説!部分文字列を置換する方法

PHP

こんにちは!今回は、PHPの標準関数であるsubstr_replace()について詳しく解説していきます。文字列の指定した位置・長さの部分を別の文字列に置き換えることができる、非常に強力な関数です!

substr_replace関数とは?

substr_replace()関数は、文字列の指定した位置から指定した長さの部分を別の文字列に置き換える関数です。

文字列の挿入、削除、部分置換など、様々な文字列操作を一つの関数で実現できます!

基本的な構文

substr_replace(
    array|string $string,
    array|string $replace,
    array|int $offset,
    array|int|null $length = null
): string|array
  • $string: 対象の文字列(または配列)
  • $replace: 置換する文字列
  • $offset: 置換を開始する位置
  • $length: 置換する長さ(省略可能)
  • 戻り値: 置換後の文字列(または配列)

基本的な使用例

部分文字列の置換

$text = "Hello World";

// 位置6から5文字を"PHP"に置換
$result = substr_replace($text, "PHP", 6, 5);
echo $result . "\n";
// 出力: Hello PHP

// 位置0から5文字を"Hi"に置換
$result = substr_replace($text, "Hi", 0, 5);
echo $result . "\n";
// 出力: Hi World

文字列の挿入

$text = "Hello World";

// 位置6に"Beautiful "を挿入(長さ0で挿入)
$result = substr_replace($text, "Beautiful ", 6, 0);
echo $result . "\n";
// 出力: Hello Beautiful World

// 位置0に"Say "を挿入
$result = substr_replace($text, "Say ", 0, 0);
echo $result . "\n";
// 出力: Say Hello World

文字列の削除

$text = "Hello World";

// 位置5から6文字を削除(空文字列で置換)
$result = substr_replace($text, "", 5, 6);
echo $result . "\n";
// 出力: Hello

// 位置0から6文字を削除
$result = substr_replace($text, "", 0, 6);
echo $result . "\n";
// 出力: World

負のオフセット

$text = "Hello World";

// 末尾から5文字目から5文字を"PHP"に置換
$result = substr_replace($text, "PHP", -5, 5);
echo $result . "\n";
// 出力: Hello PHP

// 末尾から5文字目に"Beautiful "を挿入
$result = substr_replace($text, "Beautiful ", -5, 0);
echo $result . "\n";
// 出力: Hello Beautiful World

負の長さ

$text = "Hello World";

// 位置0から末尾2文字を除いた部分を"Hi"に置換
$result = substr_replace($text, "Hi", 0, -2);
echo $result . "\n";
// 出力: Hild

// 位置6から末尾を除いた部分を"PHP"に置換
$result = substr_replace($text, "PHP", 6, -1);
echo $result . "\n";
// 出力: Hello PHPd

長さを省略(最後まで置換)

$text = "Hello World";

// 位置6から最後まで置換
$result = substr_replace($text, "PHP", 6);
echo $result . "\n";
// 出力: Hello PHP

// 位置0から最後まで置換
$result = substr_replace($text, "New Text", 0);
echo $result . "\n";
// 出力: New Text

配列の処理

$strings = ["apple", "banana", "orange"];

// すべての文字列の位置0から1文字を"X"に置換
$result = substr_replace($strings, "X", 0, 1);
print_r($result);
/*
Array (
    [0] => Xpple
    [1] => Xanana
    [2] => Xrange
)
*/

// 異なる置換文字列を使用
$replaces = ["A", "B", "O"];
$result = substr_replace($strings, $replaces, 0, 1);
print_r($result);
/*
Array (
    [0] => Apple
    [1] => Banana
    [2] => Orange
)
*/

実践的な使用例

例1: テキストの編集

class TextEditor {
    /**
     * 指定位置に文字列を挿入
     */
    public static function insert($text, $insertion, $position) {
        return substr_replace($text, $insertion, $position, 0);
    }
    
    /**
     * 指定範囲を削除
     */
    public static function delete($text, $start, $length) {
        return substr_replace($text, "", $start, $length);
    }
    
    /**
     * 指定範囲を置換
     */
    public static function replace($text, $replacement, $start, $length) {
        return substr_replace($text, $replacement, $start, $length);
    }
    
    /**
     * 先頭に追加
     */
    public static function prepend($text, $prefix) {
        return substr_replace($text, $prefix, 0, 0);
    }
    
    /**
     * 末尾に追加
     */
    public static function append($text, $suffix) {
        return substr_replace($text, $suffix, strlen($text), 0);
    }
    
    /**
     * 先頭n文字を削除
     */
    public static function removeFirst($text, $n) {
        return substr_replace($text, "", 0, $n);
    }
    
    /**
     * 末尾n文字を削除
     */
    public static function removeLast($text, $n) {
        return substr_replace($text, "", -$n);
    }
    
    /**
     * 中央部分を置換
     */
    public static function replaceMiddle($text, $replacement) {
        $length = strlen($text);
        $start = floor($length / 4);
        $replaceLength = floor($length / 2);
        
        return substr_replace($text, $replacement, $start, $replaceLength);
    }
}

// 使用例
$text = "Hello World";

echo "=== テキスト編集 ===\n";
echo "元: {$text}\n";
echo "挿入: " . TextEditor::insert($text, "Beautiful ", 6) . "\n";
echo "削除: " . TextEditor::delete($text, 5, 6) . "\n";
echo "置換: " . TextEditor::replace($text, "PHP", 6, 5) . "\n";
echo "前置: " . TextEditor::prepend($text, "Say ") . "\n";
echo "後置: " . TextEditor::append($text, "!") . "\n";
echo "先頭3文字削除: " . TextEditor::removeFirst($text, 3) . "\n";
echo "末尾5文字削除: " . TextEditor::removeLast($text, 5) . "\n";
echo "中央置換: " . TextEditor::replaceMiddle($text, "[CENSORED]") . "\n";

例2: マスキング処理

class DataMasker {
    /**
     * クレジットカード番号をマスク
     */
    public static function maskCreditCard($number) {
        $clean = preg_replace('/[^0-9]/', '', $number);
        $length = strlen($clean);
        
        if ($length < 4) {
            return str_repeat('*', $length);
        }
        
        // 最後の4桁以外をマスク
        return substr_replace($clean, str_repeat('*', $length - 4), 0, $length - 4);
    }
    
    /**
     * メールアドレスをマスク
     */
    public static function maskEmail($email) {
        $atPos = strpos($email, '@');
        
        if ($atPos === false || $atPos < 2) {
            return $email;
        }
        
        $localPart = substr($email, 0, $atPos);
        $domain = substr($email, $atPos);
        
        $visibleChars = min(2, strlen($localPart));
        $maskLength = strlen($localPart) - $visibleChars;
        
        $masked = substr_replace($localPart, str_repeat('*', $maskLength), $visibleChars, $maskLength);
        
        return $masked . $domain;
    }
    
    /**
     * 電話番号をマスク
     */
    public static function maskPhone($phone) {
        $clean = preg_replace('/[^0-9]/', '', $phone);
        $length = strlen($clean);
        
        if ($length < 4) {
            return str_repeat('*', $length);
        }
        
        // 中央部分をマスク
        $start = 3;
        $maskLength = $length - 7;
        
        return substr_replace($clean, str_repeat('*', $maskLength), $start, $maskLength);
    }
    
    /**
     * 名前をマスク(姓は表示、名はマスク)
     */
    public static function maskName($name) {
        $parts = explode(' ', $name);
        
        if (count($parts) < 2) {
            // 1語の場合、最初の文字以外をマスク
            return substr_replace($name, str_repeat('*', strlen($name) - 1), 1);
        }
        
        // 姓以外をマスク
        for ($i = 1; $i < count($parts); $i++) {
            $parts[$i] = substr($parts[$i], 0, 1) . str_repeat('*', strlen($parts[$i]) - 1);
        }
        
        return implode(' ', $parts);
    }
    
    /**
     * 住所をマスク
     */
    public static function maskAddress($address) {
        $length = strlen($address);
        
        // 最初の10文字と最後の5文字を表示、中央をマスク
        if ($length <= 15) {
            return substr_replace($address, str_repeat('*', $length - 5), 5, $length - 10);
        }
        
        $maskLength = $length - 15;
        return substr_replace($address, str_repeat('*', $maskLength), 10, $maskLength);
    }
}

// 使用例
echo "=== マスキング処理 ===\n";

$creditCard = "1234-5678-9012-3456";
echo "クレジットカード: " . DataMasker::maskCreditCard($creditCard) . "\n";
// ************3456

$email = "john.doe@example.com";
echo "メール: " . DataMasker::maskEmail($email) . "\n";
// jo******@example.com

$phone = "090-1234-5678";
echo "電話: " . DataMasker::maskPhone($phone) . "\n";
// 090****5678

$name = "John Doe";
echo "名前: " . DataMasker::maskName($name) . "\n";
// John D**

$address = "123 Main Street, Springfield, IL 62701";
echo "住所: " . DataMasker::maskAddress($address) . "\n";

例3: 文字列のフォーマット

class StringFormatter {
    /**
     * クレジットカード番号をフォーマット
     */
    public static function formatCreditCard($number) {
        $clean = preg_replace('/[^0-9]/', '', $number);
        
        // 4桁ごとにハイフンを挿入
        $formatted = $clean;
        for ($i = 4; $i < strlen($clean); $i += 5) {
            $formatted = substr_replace($formatted, '-', $i, 0);
        }
        
        return $formatted;
    }
    
    /**
     * 電話番号をフォーマット
     */
    public static function formatPhone($phone, $format = '###-####-####') {
        $clean = preg_replace('/[^0-9]/', '', $phone);
        
        // フォーマットに従って挿入
        $result = '';
        $cleanIndex = 0;
        
        for ($i = 0; $i < strlen($format); $i++) {
            if ($format[$i] === '#') {
                if ($cleanIndex < strlen($clean)) {
                    $result .= $clean[$cleanIndex];
                    $cleanIndex++;
                }
            } else {
                $result .= $format[$i];
            }
        }
        
        return $result;
    }
    
    /**
     * 郵便番号をフォーマット
     */
    public static function formatZipCode($zip) {
        $clean = preg_replace('/[^0-9]/', '', $zip);
        
        if (strlen($clean) === 7) {
            return substr_replace($clean, '-', 3, 0);
        }
        
        return $clean;
    }
    
    /**
     * 日付をフォーマット
     */
    public static function formatDate($date, $fromFormat = 'Ymd', $toFormat = 'Y-m-d') {
        if (strlen($date) === 8) {
            // YYYYMMDDからYYYY-MM-DDへ
            $date = substr_replace($date, '-', 4, 0);
            $date = substr_replace($date, '-', 7, 0);
        }
        
        return $date;
    }
    
    /**
     * IBANコードをフォーマット
     */
    public static function formatIBAN($iban) {
        $clean = preg_replace('/[^A-Z0-9]/i', '', strtoupper($iban));
        
        // 4文字ごとにスペースを挿入
        $formatted = $clean;
        for ($i = 4; $i < strlen($clean); $i += 5) {
            $formatted = substr_replace($formatted, ' ', $i, 0);
        }
        
        return $formatted;
    }
}

// 使用例
echo "=== フォーマット処理 ===\n";

$card = "1234567890123456";
echo "カード番号: " . StringFormatter::formatCreditCard($card) . "\n";
// 1234-5678-9012-3456

$phone = "09012345678";
echo "電話番号: " . StringFormatter::formatPhone($phone) . "\n";
// 090-1234-5678

$zip = "1234567";
echo "郵便番号: " . StringFormatter::formatZipCode($zip) . "\n";
// 123-4567

$date = "20240223";
echo "日付: " . StringFormatter::formatDate($date) . "\n";
// 2024-02-23

$iban = "GB82WEST12345698765432";
echo "IBAN: " . StringFormatter::formatIBAN($iban) . "\n";
// GB82 WEST 1234 5698 7654 32

例4: コード生成と修正

class CodeGenerator {
    /**
     * 変数名を修正
     */
    public static function fixVariableName($code, $oldName, $newName) {
        // $を含めない場合は追加
        if ($oldName[0] !== '$') {
            $oldName = '$' . $oldName;
        }
        if ($newName[0] !== '$') {
            $newName = '$' . $newName;
        }
        
        return str_replace($oldName, $newName, $code);
    }
    
    /**
     * インデントを追加
     */
    public static function addIndentation($code, $spaces = 4) {
        $lines = explode("\n", $code);
        $indent = str_repeat(' ', $spaces);
        
        foreach ($lines as $i => $line) {
            if (!empty(trim($line))) {
                $lines[$i] = substr_replace($line, $indent, 0, 0);
            }
        }
        
        return implode("\n", $lines);
    }
    
    /**
     * コメントを挿入
     */
    public static function insertComment($code, $comment, $position) {
        $commentLine = "// {$comment}\n";
        return substr_replace($code, $commentLine, $position, 0);
    }
    
    /**
     * 関数名を変更
     */
    public static function renameFucntion($code, $oldName, $newName) {
        // function キーワード後の関数名を置換
        $pattern = 'function ' . $oldName;
        $replacement = 'function ' . $newName;
        
        return str_replace($pattern, $replacement, $code);
    }
}

// 使用例
$code = <<<'CODE'
function calculateTotal($items) {
    $total = 0;
    foreach ($items as $item) {
        $total += $item;
    }
    return $total;
}
CODE;

echo "=== コード生成 ===\n";
echo "元のコード:\n{$code}\n\n";

echo "変数名変更:\n";
$modified = CodeGenerator::fixVariableName($code, 'total', 'sum');
echo $modified . "\n\n";

echo "インデント追加:\n";
$indented = CodeGenerator::addIndentation($code);
echo $indented . "\n";

例5: パス操作

class PathModifier {
    /**
     * ファイル名を変更
     */
    public static function changeFilename($path, $newFilename) {
        $lastSlash = max(strrpos($path, '/'), strrpos($path, '\\'));
        
        if ($lastSlash === false) {
            return $newFilename;
        }
        
        return substr_replace($path, $newFilename, $lastSlash + 1);
    }
    
    /**
     * 拡張子を変更
     */
    public static function changeExtension($path, $newExtension) {
        $lastDot = strrpos($path, '.');
        
        if ($lastDot === false) {
            return $path . '.' . ltrim($newExtension, '.');
        }
        
        $newExt = '.' . ltrim($newExtension, '.');
        return substr_replace($path, $newExt, $lastDot);
    }
    
    /**
     * ディレクトリ部分を変更
     */
    public static function changeDirectory($path, $newDirectory) {
        $lastSlash = max(strrpos($path, '/'), strrpos($path, '\\'));
        
        if ($lastSlash === false) {
            return $newDirectory . '/' . $path;
        }
        
        $filename = substr($path, $lastSlash + 1);
        return rtrim($newDirectory, '/\\') . '/' . $filename;
    }
    
    /**
     * パスの一部を置換
     */
    public static function replacePart($path, $oldPart, $newPart) {
        return str_replace($oldPart, $newPart, $path);
    }
}

// 使用例
$path = "/var/www/html/images/photo.jpg";

echo "=== パス操作 ===\n";
echo "元: {$path}\n";
echo "ファイル名変更: " . PathModifier::changeFilename($path, "image.jpg") . "\n";
echo "拡張子変更: " . PathModifier::changeExtension($path, "png") . "\n";
echo "ディレクトリ変更: " . PathModifier::changeDirectory($path, "/home/user/pictures") . "\n";
echo "パス置換: " . PathModifier::replacePart($path, "images", "photos") . "\n";

例6: センシティブ情報の削除

class SensitiveDataRemover {
    /**
     * ログからIPアドレスを削除
     */
    public static function removeIpAddresses($log) {
        // 簡易的なIPアドレスマッチング
        return preg_replace_callback(
            '/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',
            function($matches) {
                return str_repeat('*', strlen($matches[0]));
            },
            $log
        );
    }
    
    /**
     * メールアドレスを削除
     */
    public static function removeEmails($text) {
        return preg_replace_callback(
            '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/',
            function($matches) {
                return str_repeat('*', strlen($matches[0]));
            },
            $text
        );
    }
    
    /**
     * APIキーを削除
     */
    public static function removeApiKeys($text) {
        // api_key=xxx または apiKey: xxx 形式
        return preg_replace_callback(
            '/(api[_-]?key[\s:=]+)([a-zA-Z0-9]{20,})/i',
            function($matches) {
                return $matches[1] . str_repeat('*', strlen($matches[2]));
            },
            $text
        );
    }
    
    /**
     * パスワードを削除
     */
    public static function removePasswords($text) {
        return preg_replace_callback(
            '/(password[\s:=]+)([^\s,;]+)/i',
            function($matches) {
                return $matches[1] . str_repeat('*', strlen($matches[2]));
            },
            $text
        );
    }
}

// 使用例
$log = "User 192.168.1.100 logged in with email user@example.com";
echo "=== センシティブ情報削除 ===\n";
echo "元: {$log}\n";
echo "IP削除: " . SensitiveDataRemover::removeIpAddresses($log) . "\n";
echo "Email削除: " . SensitiveDataRemover::removeEmails($log) . "\n";

$config = "api_key=abcdef1234567890xyz password=secret123";
echo "\n設定: {$config}\n";
echo "APIキー削除: " . SensitiveDataRemover::removeApiKeys($config) . "\n";
echo "パスワード削除: " . SensitiveDataRemover::removePasswords($config) . "\n";

例7: テンプレート処理

class TemplateProcessor {
    /**
     * プレースホルダーを置換
     */
    public static function fillTemplate($template, $data) {
        $result = $template;
        
        foreach ($data as $key => $value) {
            $placeholder = '{' . $key . '}';
            $result = str_replace($placeholder, $value, $result);
        }
        
        return $result;
    }
    
    /**
     * 条件付きブロックを処理
     */
    public static function processConditional($template, $condition, $show) {
        $startTag = "{{if {$condition}}}";
        $endTag = "{{/if}}";
        
        $start = strpos($template, $startTag);
        
        if ($start === false) {
            return $template;
        }
        
        $end = strpos($template, $endTag, $start);
        
        if ($end === false) {
            return $template;
        }
        
        $blockLength = $end + strlen($endTag) - $start;
        
        if ($show) {
            // タグのみ削除、内容は保持
            $content = substr($template, $start + strlen($startTag), $end - $start - strlen($startTag));
            return substr_replace($template, $content, $start, $blockLength);
        } else {
            // ブロック全体を削除
            return substr_replace($template, '', $start, $blockLength);
        }
    }
    
    /**
     * 繰り返しブロックを処理
     */
    public static function processLoop($template, $loopName, $items) {
        $startTag = "{{foreach {$loopName}}}";
        $endTag = "{{/foreach}}";
        
        $start = strpos($template, $startTag);
        
        if ($start === false) {
            return $template;
        }
        
        $end = strpos($template, $endTag, $start);
        
        if ($end === false) {
            return $template;
        }
        
        $blockContent = substr($template, $start + strlen($startTag), $end - $start - strlen($startTag));
        $blockLength = $end + strlen($endTag) - $start;
        
        $output = '';
        foreach ($items as $item) {
            $itemOutput = $blockContent;
            foreach ($item as $key => $value) {
                $itemOutput = str_replace('{' . $key . '}', $value, $itemOutput);
            }
            $output .= $itemOutput;
        }
        
        return substr_replace($template, $output, $start, $blockLength);
    }
}

// 使用例
echo "=== テンプレート処理 ===\n";

$template = "Hello {name}, welcome to {site}!";
$data = ['name' => 'John', 'site' => 'MyWebsite'];
echo TemplateProcessor::fillTemplate($template, $data) . "\n";

$conditionalTemplate = "Header {{if premium}}Premium Content{{/if}} Footer";
echo "\nプレミアム表示: " . TemplateProcessor::processConditional($conditionalTemplate, 'premium', true) . "\n";
echo "通常表示: " . TemplateProcessor::processConditional($conditionalTemplate, 'premium', false) . "\n";

$loopTemplate = "Users: {{foreach users}}{name}, {{/foreach}}";
$users = [
    ['name' => 'Alice'],
    ['name' => 'Bob'],
    ['name' => 'Charlie']
];
echo "\nループ: " . TemplateProcessor::processLoop($loopTemplate, 'users', $users) . "\n";

str_replace()との違い

$text = "Hello World";

// str_replace(): パターンマッチで置換
$result1 = str_replace("World", "PHP", $text);
echo $result1 . "\n";  // Hello PHP

// substr_replace(): 位置と長さで置換
$result2 = substr_replace($text, "PHP", 6, 5);
echo $result2 . "\n";  // Hello PHP

// str_replace()はパターンを探す
// substr_replace()は位置を指定する

パフォーマンステスト

$text = str_repeat("Hello World ", 10000);

// substr_replace()
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
    substr_replace($text, "PHP", 6, 5);
}
$time1 = microtime(true) - $start;

// substr() + 文字列連結
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
    substr($text, 0, 6) . "PHP" . substr($text, 11);
}
$time2 = microtime(true) - $start;

echo "substr_replace(): {$time1}秒\n";
echo "substr() + 連結: {$time2}秒\n";

// substr_replace()の方が効率的

まとめ

substr_replace()関数の特徴をまとめると:

できること:

  • 部分文字列の置換
  • 文字列の挿入(長さ0で置換)
  • 文字列の削除(空文字列で置換)
  • 負のオフセット・長さに対応

推奨される使用場面:

  • テキスト編集
  • マスキング処理
  • フォーマット処理
  • コード生成
  • パス操作
  • テンプレート処理

利点:

  • 位置ベースの正確な置換
  • 挿入・削除・置換を一つの関数で実現
  • 配列の一括処理に対応

注意点:

  • バイト単位で処理(マルチバイト文字注意)
  • 負の値の挙動を理解する
  • 配列処理時のインデックス対応

よく使うパターン:

// 挿入
substr_replace($str, $insert, $pos, 0)

// 削除
substr_replace($str, "", $start, $length)

// 置換
substr_replace($str, $replace, $start, $length)

// 末尾まで置換
substr_replace($str, $replace, $start)

// 先頭に追加
substr_replace($str, $prefix, 0, 0)

// 末尾に追加
substr_replace($str, $suffix, strlen($str), 0)

関連関数:

  • str_replace(): パターンマッチで置換
  • substr(): 部分文字列を取得
  • str_pad(): 文字列をパディング
  • mb_substr_replace(): マルチバイト対応版(存在しない場合は自作)

substr_replace()は、位置ベースの文字列操作が必要な場合に非常に便利です。挿入、削除、置換を柔軟に行えるので、様々な文字列処理で活躍します!

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