[PHP]substr関数を完全解説!部分文字列を取得する方法

PHP

こんにちは!今回は、PHPの標準関数であるsubstr()について詳しく解説していきます。文字列から指定した位置・長さの部分を取得できる、最も基本的で重要な関数の一つです!

substr関数とは?

substr()関数は、文字列から指定した位置・長さの部分文字列を取得する関数です。

“substring”の略で、文字列の切り出し、分割、トリミングなど、様々な文字列操作の基礎となる関数です!

基本的な構文

substr(string $string, int $offset, ?int $length = null): string
  • $string: 対象の文字列
  • $offset: 開始位置(0から始まる)
  • $length: 取得する長さ(省略可能)
  • 戻り値: 部分文字列、失敗時はfalse

基本的な使用例

シンプルな切り出し

$text = "Hello World";

// 位置6から最後まで
echo substr($text, 6) . "\n";
// 出力: World

// 位置0から5文字
echo substr($text, 0, 5) . "\n";
// 出力: Hello

// 位置6から5文字
echo substr($text, 6, 5) . "\n";
// 出力: World

負のオフセット

$text = "Hello World";

// 末尾から5文字
echo substr($text, -5) . "\n";
// 出力: World

// 末尾から5文字目から3文字
echo substr($text, -5, 3) . "\n";
// 出力: Wor

負の長さ

$text = "Hello World";

// 先頭から末尾5文字を除く
echo substr($text, 0, -6) . "\n";
// 出力: Hello

// 位置6から末尾1文字を除く
echo substr($text, 6, -1) . "\n";
// 出力: Worl

範囲外の指定

$text = "Hello";

// 開始位置が文字列長を超える
var_dump(substr($text, 10));
// bool(false)

// 長さが0以下
echo substr($text, 0, 0) . "\n";
// 出力: ""(空文字列)

// 負のオフセットが大きすぎる
echo substr($text, -10, 3) . "\n";
// 出力: Hel(先頭から扱われる)

実践的な使用例

例1: テキストの切り詰め

class TextTruncator {
    /**
     * 指定長で切り詰め
     */
    public static function truncate($text, $maxLength, $suffix = '...') {
        if (strlen($text) <= $maxLength) {
            return $text;
        }
        
        return substr($text, 0, $maxLength - strlen($suffix)) . $suffix;
    }
    
    /**
     * 単語の途中で切らない
     */
    public static function truncateWords($text, $maxLength, $suffix = '...') {
        if (strlen($text) <= $maxLength) {
            return $text;
        }
        
        $truncated = substr($text, 0, $maxLength - strlen($suffix));
        
        // 最後のスペースを探す
        $lastSpace = strrpos($truncated, ' ');
        
        if ($lastSpace !== false && $lastSpace > $maxLength * 0.7) {
            $truncated = substr($truncated, 0, $lastSpace);
        }
        
        return $truncated . $suffix;
    }
    
    /**
     * 中央を省略
     */
    public static function truncateMiddle($text, $maxLength, $separator = '...') {
        if (strlen($text) <= $maxLength) {
            return $text;
        }
        
        $sideLength = floor(($maxLength - strlen($separator)) / 2);
        
        $start = substr($text, 0, $sideLength);
        $end = substr($text, -$sideLength);
        
        return $start . $separator . $end;
    }
    
    /**
     * 文の途中で切らない
     */
    public static function truncateSentences($text, $maxLength) {
        if (strlen($text) <= $maxLength) {
            return $text;
        }
        
        $truncated = substr($text, 0, $maxLength);
        
        // 最後の句点を探す
        $lastPeriod = max(
            strrpos($truncated, '.'),
            strrpos($truncated, '。')
        );
        
        if ($lastPeriod !== false && $lastPeriod > $maxLength * 0.5) {
            return substr($text, 0, $lastPeriod + 1);
        }
        
        return self::truncateWords($text, $maxLength);
    }
    
    /**
     * 先頭を省略
     */
    public static function truncateStart($text, $maxLength, $prefix = '...') {
        if (strlen($text) <= $maxLength) {
            return $text;
        }
        
        return $prefix . substr($text, -(($maxLength - strlen($prefix))));
    }
}

// 使用例
$longText = "This is a very long text that needs to be truncated for display purposes.";

echo "=== テキスト切り詰め ===\n";
echo TextTruncator::truncate($longText, 30) . "\n";
// This is a very long text...

echo "\n=== 単語の途中で切らない ===\n";
echo TextTruncator::truncateWords($longText, 30) . "\n";
// This is a very long...

echo "\n=== 中央省略 ===\n";
$path = "/very/long/path/to/some/file/document.pdf";
echo TextTruncator::truncateMiddle($path, 25) . "\n";
// /very/long...ument.pdf

echo "\n=== 先頭省略 ===\n";
echo TextTruncator::truncateStart($longText, 30) . "\n";
// ...for display purposes.

例2: ファイル名とパスの処理

class PathHelper {
    /**
     * 拡張子を取得
     */
    public static function getExtension($filename) {
        $pos = strrpos($filename, '.');
        
        if ($pos === false) {
            return '';
        }
        
        return substr($filename, $pos + 1);
    }
    
    /**
     * ファイル名(拡張子除く)を取得
     */
    public static function getBasename($filename) {
        $pos = strrpos($filename, '.');
        
        if ($pos === false) {
            return $filename;
        }
        
        return substr($filename, 0, $pos);
    }
    
    /**
     * ディレクトリ名を取得
     */
    public static function getDirectory($path) {
        $pos = max(strrpos($path, '/'), strrpos($path, '\\'));
        
        if ($pos === false) {
            return '';
        }
        
        return substr($path, 0, $pos);
    }
    
    /**
     * ファイル名のみを取得
     */
    public static function getFilename($path) {
        $pos = max(strrpos($path, '/'), strrpos($path, '\\'));
        
        if ($pos === false) {
            return $path;
        }
        
        return substr($path, $pos + 1);
    }
    
    /**
     * 最後のn個のディレクトリを取得
     */
    public static function getLastDirectories($path, $n) {
        $parts = preg_split('#[/\\\\]#', $path);
        $parts = array_filter($parts); // 空要素を除去
        
        if (count($parts) <= $n) {
            return $path;
        }
        
        $lastParts = array_slice($parts, -$n);
        return implode('/', $lastParts);
    }
}

// 使用例
$files = [
    '/var/www/html/index.php',
    'C:\\Users\\Documents\\report.pdf',
    'image.backup.jpg',
    'README'
];

echo "=== パス処理 ===\n";
foreach ($files as $file) {
    echo "\nファイル: {$file}\n";
    echo "  拡張子: " . PathHelper::getExtension($file) . "\n";
    echo "  ベース名: " . PathHelper::getBasename($file) . "\n";
    echo "  ディレクトリ: " . PathHelper::getDirectory($file) . "\n";
    echo "  ファイル名: " . PathHelper::getFilename($file) . "\n";
}

$path = '/var/www/html/images/2024/january/photo.jpg';
echo "\n=== 最後の3ディレクトリ ===\n";
echo PathHelper::getLastDirectories($path, 3) . "\n";
// 2024/january/photo.jpg

例3: メールアドレスとURL処理

class EmailUrlHelper {
    /**
     * メールアドレスをマスク
     */
    public static function maskEmail($email) {
        $atPos = strpos($email, '@');
        
        if ($atPos === false) {
            return $email;
        }
        
        $local = substr($email, 0, $atPos);
        $domain = substr($email, $atPos);
        
        $localLength = strlen($local);
        $visibleChars = min(3, $localLength);
        
        $masked = substr($local, 0, $visibleChars) . 
                  str_repeat('*', max(0, $localLength - $visibleChars));
        
        return $masked . $domain;
    }
    
    /**
     * ドメインの一部をマスク
     */
    public static function maskDomain($email) {
        $atPos = strpos($email, '@');
        
        if ($atPos === false) {
            return $email;
        }
        
        $local = substr($email, 0, $atPos + 1);
        $domain = substr($email, $atPos + 1);
        
        $dotPos = strpos($domain, '.');
        
        if ($dotPos === false) {
            return $email;
        }
        
        $domainName = substr($domain, 0, $dotPos);
        $tld = substr($domain, $dotPos);
        
        $maskedDomain = substr($domainName, 0, 1) . 
                        str_repeat('*', strlen($domainName) - 1);
        
        return $local . $maskedDomain . $tld;
    }
    
    /**
     * URLからドメインを抽出
     */
    public static function extractDomain($url) {
        // プロトコルを除去
        $url = preg_replace('#^https?://#', '', $url);
        
        // パスを除去
        $slashPos = strpos($url, '/');
        if ($slashPos !== false) {
            $url = substr($url, 0, $slashPos);
        }
        
        return $url;
    }
    
    /**
     * URLを短縮表示
     */
    public static function shortenUrl($url, $maxLength = 40) {
        if (strlen($url) <= $maxLength) {
            return $url;
        }
        
        $protocol = '';
        if (preg_match('#^(https?://)#', $url, $matches)) {
            $protocol = $matches[1];
            $url = substr($url, strlen($protocol));
        }
        
        $sideLength = floor(($maxLength - strlen($protocol) - 3) / 2);
        
        $start = substr($url, 0, $sideLength);
        $end = substr($url, -$sideLength);
        
        return $protocol . $start . '...' . $end;
    }
}

// 使用例
echo "=== メールアドレスマスク ===\n";
$emails = [
    'john.doe@example.com',
    'admin@company.co.jp',
    'a@test.org'
];

foreach ($emails as $email) {
    echo "{$email} → " . EmailUrlHelper::maskEmail($email) . "\n";
}

echo "\n=== ドメインマスク ===\n";
foreach ($emails as $email) {
    echo "{$email} → " . EmailUrlHelper::maskDomain($email) . "\n";
}

echo "\n=== URL短縮表示 ===\n";
$url = 'https://example.com/very/long/path/to/some/page.html';
echo EmailUrlHelper::shortenUrl($url) . "\n";

例4: データ抽出

class DataExtractor {
    /**
     * 固定長フォーマットからデータを抽出
     */
    public static function extractFixedWidth($line, $positions) {
        $data = [];
        
        foreach ($positions as $field => $info) {
            $start = $info['start'];
            $length = $info['length'];
            
            $value = substr($line, $start, $length);
            $data[$field] = trim($value);
        }
        
        return $data;
    }
    
    /**
     * 特定の位置の文字を抽出
     */
    public static function extractCharAt($text, $position) {
        if ($position < 0 || $position >= strlen($text)) {
            return null;
        }
        
        return substr($text, $position, 1);
    }
    
    /**
     * 範囲を指定してデータを抽出
     */
    public static function extractRange($text, $start, $end) {
        if ($start < 0 || $end > strlen($text) || $start >= $end) {
            return '';
        }
        
        return substr($text, $start, $end - $start);
    }
    
    /**
     * 文字列の先頭n文字を取得
     */
    public static function first($text, $n) {
        return substr($text, 0, $n);
    }
    
    /**
     * 文字列の最後n文字を取得
     */
    public static function last($text, $n) {
        return substr($text, -$n);
    }
    
    /**
     * 文字列から中央部分を取得
     */
    public static function middle($text, $length) {
        $totalLength = strlen($text);
        
        if ($totalLength <= $length) {
            return $text;
        }
        
        $start = floor(($totalLength - $length) / 2);
        
        return substr($text, $start, $length);
    }
}

// 使用例
echo "=== 固定長フォーマット ===\n";
$line = "John      30    Developer         ";
$positions = [
    'name' => ['start' => 0, 'length' => 10],
    'age' => ['start' => 10, 'length' => 6],
    'role' => ['start' => 16, 'length' => 18]
];

$data = DataExtractor::extractFixedWidth($line, $positions);
print_r($data);

echo "\n=== 文字抽出 ===\n";
$text = "Hello World";
echo "先頭5文字: " . DataExtractor::first($text, 5) . "\n";
echo "最後5文字: " . DataExtractor::last($text, 5) . "\n";
echo "中央6文字: " . DataExtractor::middle($text, 6) . "\n";
echo "範囲(3-8): " . DataExtractor::extractRange($text, 3, 8) . "\n";

例5: 文字列の分割

class StringSplitter {
    /**
     * 指定長で分割
     */
    public static function splitByLength($text, $length) {
        $chunks = [];
        $textLength = strlen($text);
        
        for ($i = 0; $i < $textLength; $i += $length) {
            $chunks[] = substr($text, $i, $length);
        }
        
        return $chunks;
    }
    
    /**
     * 先頭と残りに分割
     */
    public static function splitFirst($text, $length) {
        return [
            'first' => substr($text, 0, $length),
            'rest' => substr($text, $length)
        ];
    }
    
    /**
     * 最後と残りに分割
     */
    public static function splitLast($text, $length) {
        return [
            'rest' => substr($text, 0, -$length),
            'last' => substr($text, -$length)
        ];
    }
    
    /**
     * 前後を分割
     */
    public static function splitAround($text, $position) {
        return [
            'before' => substr($text, 0, $position),
            'after' => substr($text, $position)
        ];
    }
    
    /**
     * 3分割(前・中・後)
     */
    public static function splitThree($text, $start, $end) {
        return [
            'before' => substr($text, 0, $start),
            'middle' => substr($text, $start, $end - $start),
            'after' => substr($text, $end)
        ];
    }
}

// 使用例
echo "=== 指定長で分割 ===\n";
$text = "1234567890ABCDEFGHIJ";
$chunks = StringSplitter::splitByLength($text, 5);
print_r($chunks);
// ['12345', '67890', 'ABCDE', 'FGHIJ']

echo "\n=== 先頭と残り ===\n";
$result = StringSplitter::splitFirst("Hello World", 5);
print_r($result);
// ['first' => 'Hello', 'rest' => ' World']

echo "\n=== 3分割 ===\n";
$text = "Before[CONTENT]After";
$result = StringSplitter::splitThree($text, 6, 15);
print_r($result);
// ['before' => 'Before', 'middle' => '[CONTENT]', 'after' => 'After']

例6: バリデーション

class StringValidator {
    /**
     * プレフィックスをチェック
     */
    public static function hasPrefix($text, $prefix) {
        $length = strlen($prefix);
        return substr($text, 0, $length) === $prefix;
    }
    
    /**
     * サフィックスをチェック
     */
    public static function hasSuffix($text, $suffix) {
        $length = strlen($suffix);
        return substr($text, -$length) === $suffix;
    }
    
    /**
     * 特定の位置に特定の文字列があるかチェック
     */
    public static function hasAtPosition($text, $substring, $position) {
        $length = strlen($substring);
        return substr($text, $position, $length) === $substring;
    }
    
    /**
     * ファイル名が特定の拡張子か
     */
    public static function hasExtension($filename, $extension) {
        $ext = '.' . ltrim($extension, '.');
        return self::hasSuffix($filename, $ext);
    }
    
    /**
     * 最初のn文字が同じかチェック
     */
    public static function samePrefix($str1, $str2, $length) {
        return substr($str1, 0, $length) === substr($str2, 0, $length);
    }
}

// 使用例
echo "=== プレフィックスチェック ===\n";
var_dump(StringValidator::hasPrefix("Hello World", "Hello"));  // true
var_dump(StringValidator::hasPrefix("Hello World", "World"));  // false

echo "\n=== サフィックスチェック ===\n";
var_dump(StringValidator::hasSuffix("document.pdf", ".pdf"));  // true
var_dump(StringValidator::hasSuffix("document.pdf", ".doc"));  // false

echo "\n=== 拡張子チェック ===\n";
var_dump(StringValidator::hasExtension("file.jpg", "jpg"));    // true
var_dump(StringValidator::hasExtension("file.jpg", ".jpg"));   // true

echo "\n=== プレフィックス比較 ===\n";
var_dump(StringValidator::samePrefix("apple", "application", 3));  // true

例7: フォーマット処理

class StringFormatter {
    /**
     * クレジットカード番号をマスク
     */
    public static function maskCreditCard($number) {
        $clean = preg_replace('/[^0-9]/', '', $number);
        $length = strlen($clean);
        
        if ($length < 4) {
            return str_repeat('*', $length);
        }
        
        $masked = str_repeat('*', $length - 4);
        $last4 = substr($clean, -4);
        
        return $masked . $last4;
    }
    
    /**
     * 電話番号をフォーマット
     */
    public static function formatPhoneNumber($phone, $format = '###-####-####') {
        $clean = preg_replace('/[^0-9]/', '', $phone);
        
        // 最初の3桁、次の4桁、最後の4桁
        $part1 = substr($clean, 0, 3);
        $part2 = substr($clean, 3, 4);
        $part3 = substr($clean, 7, 4);
        
        return "{$part1}-{$part2}-{$part3}";
    }
    
    /**
     * 郵便番号をフォーマット
     */
    public static function formatZipCode($zip) {
        $clean = preg_replace('/[^0-9]/', '', $zip);
        
        $part1 = substr($clean, 0, 3);
        $part2 = substr($clean, 3, 4);
        
        return "{$part1}-{$part2}";
    }
    
    /**
     * 社会保障番号をマスク
     */
    public static function maskSSN($ssn) {
        $clean = preg_replace('/[^0-9]/', '', $ssn);
        
        $masked = '***-**-' . substr($clean, -4);
        
        return $masked;
    }
}

// 使用例
echo "=== クレジットカードマスク ===\n";
echo StringFormatter::maskCreditCard('1234567812345678') . "\n";
// ************5678

echo "\n=== 電話番号フォーマット ===\n";
echo StringFormatter::formatPhoneNumber('09012345678') . "\n";
// 090-1234-5678

echo "\n=== 郵便番号フォーマット ===\n";
echo StringFormatter::formatZipCode('1234567') . "\n";
// 123-4567

echo "\n=== SSNマスク ===\n";
echo StringFormatter::maskSSN('123456789') . "\n";
// ***-**-6789

mb_substr()との違い

// ASCII文字列
$text = "Hello World";
echo substr($text, 0, 5) . "\n";     // Hello
echo mb_substr($text, 0, 5) . "\n";  // Hello(同じ)

// マルチバイト文字列
$japanese = "こんにちは";

// substr(): バイト単位
echo substr($japanese, 0, 6) . "\n";
// こん(UTF-8で6バイト = 2文字)

// mb_substr(): 文字単位
echo mb_substr($japanese, 0, 2) . "\n";
// こん(2文字)

// 日本語などのマルチバイト文字にはmb_substr()を使用

パフォーマンスの考慮

// 大量の部分文字列抽出
$text = str_repeat("abcdefghijklmnopqrstuvwxyz", 10000);

// substr()
$start = microtime(true);
for ($i = 0; $i < 10000; $i++) {
    substr($text, 100, 50);
}
$time1 = microtime(true) - $start;

// 手動抽出
$start = microtime(true);
for ($i = 0; $i < 10000; $i++) {
    $result = '';
    for ($j = 100; $j < 150; $j++) {
        $result .= $text[$j];
    }
}
$time2 = microtime(true) - $start;

echo "substr(): {$time1}秒\n";
echo "手動抽出: {$time2}秒\n";

// substr()は高度に最適化されており非常に高速

まとめ

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

できること:

  • 部分文字列の取得
  • 文字列の切り詰め
  • 先頭・末尾の取得

引数の使い方:

  • 正のオフセット: 先頭から数える
  • 負のオフセット: 末尾から数える
  • 正の長さ: 取得する文字数
  • 負の長さ: 末尾から除外する文字数

推奨される使用場面:

  • テキストの切り詰め
  • ファイルパスの処理
  • データのマスキング
  • 固定長フォーマットの解析
  • 文字列の分割

注意点:

  • バイト単位で処理(マルチバイト文字注意)
  • 範囲外の指定はfalseを返す
  • 負のインデックスが便利

関連関数:

  • mb_substr(): マルチバイト対応版
  • str_split(): 文字列を配列に分割
  • substr_replace(): 部分文字列を置換
  • substr_count(): 部分文字列の出現回数

よく使うパターン:

// 先頭n文字
substr($str, 0, $n)

// 最後n文字
substr($str, -$n)

// 先頭n文字を除く
substr($str, $n)

// 最後n文字を除く
substr($str, 0, -$n)

// 中央部分
substr($str, $start, $length)

substr()は、文字列操作の基本中の基本です。オフセットと長さの指定方法を理解して、マルチバイト文字に注意しながら、適切に使いこなしましょう!

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