[PHP]strspn関数を完全解説!指定文字に含まれる部分の長さを取得

PHP

こんにちは!今回は、PHPの標準関数であるstrspn()について詳しく解説していきます。文字列の先頭から、指定した文字集合に含まれる文字が続く長さを取得できる関数です!

strspn関数とは?

strspn()関数は、文字列の先頭から、指定した文字集合に含まれる文字が連続する長さを返す関数です。

“string span”の略で、strcspn()の逆の動作をします。数値や英字のみの部分を抽出したい場合や、特定の文字集合の連続をチェックする際に便利です!

基本的な構文

strspn(
    string $string,
    string $characters,
    int $offset = 0,
    ?int $length = null
): int
  • $string: 対象の文字列
  • $characters: 検索する文字の集合
  • $offset: 検索開始位置(オプション)
  • $length: 検索する長さ(オプション)
  • 戻り値: 指定文字に含まれる連続した文字の長さ

基本的な使用例

シンプルな使用例

$text = "12345abc";

// 先頭から数字が続く長さ
$length = strspn($text, "0123456789");
echo $length . "\n";  // 5

// 先頭から英字が続く長さ
$text2 = "abcde123";
$length = strspn($text2, "abcdefghijklmnopqrstuvwxyz");
echo $length . "\n";  // 5

strcspn()との違い

$text = "abc123xyz";

// strspn(): 指定文字に含まれる部分の長さ
$length1 = strspn($text, "abc");
echo "strspn: {$length1}\n";  // 3("abc"の長さ)

// strcspn(): 指定文字に含まれない部分の長さ
$length2 = strcspn($text, "123");
echo "strcspn: {$length2}\n";  // 3("abc"の長さ)

// 補完的な関係
$text2 = "12345abc";
echo strspn($text2, "0123456789") . "\n";    // 5
echo strcspn($text2, "abcdefghijklmnopqrstuvwxyz") . "\n";  // 5

オフセットと長さの使用

$text = "abc123def456";

// 先頭から
$length = strspn($text, "abc");
echo $length . "\n";  // 3

// 位置3から
$length = strspn($text, "0123456789", 3);
echo $length . "\n";  // 3("123"の長さ)

// 位置3から5文字分のみチェック
$length = strspn($text, "0123456789", 3, 5);
echo $length . "\n";  // 3

空文字列と一致しない場合

// 空文字列
$empty = "";
echo strspn($empty, "abc") . "\n";  // 0

// 一致しない場合
$text = "xyz123";
echo strspn($text, "0123456789") . "\n";  // 0

実践的な使用例

例1: 数値の抽出

class NumberExtractor {
    /**
     * 先頭の数値部分の長さを取得
     */
    public static function getNumericLength($text) {
        return strspn($text, "0123456789");
    }
    
    /**
     * 先頭の数値を抽出
     */
    public static function extractLeadingNumber($text) {
        $length = self::getNumericLength($text);
        
        if ($length === 0) {
            return null;
        }
        
        return substr($text, 0, $length);
    }
    
    /**
     * 符号付き数値を抽出
     */
    public static function extractSignedNumber($text) {
        $offset = 0;
        
        // 符号をチェック
        if (!empty($text) && ($text[0] === '+' || $text[0] === '-')) {
            $offset = 1;
        }
        
        $length = strspn($text, "0123456789", $offset);
        
        if ($length === 0) {
            return null;
        }
        
        return substr($text, 0, $offset + $length);
    }
    
    /**
     * 小数を含む数値を抽出
     */
    public static function extractDecimalNumber($text) {
        $offset = 0;
        
        // 符号をチェック
        if (!empty($text) && ($text[0] === '+' || $text[0] === '-')) {
            $offset = 1;
        }
        
        // 整数部分
        $intLength = strspn($text, "0123456789", $offset);
        $totalLength = $offset + $intLength;
        
        // 小数点をチェック
        if (isset($text[$totalLength]) && $text[$totalLength] === '.') {
            $totalLength++;
            
            // 小数部分
            $decLength = strspn($text, "0123456789", $totalLength);
            $totalLength += $decLength;
        }
        
        if ($totalLength === $offset) {
            return null;
        }
        
        return substr($text, 0, $totalLength);
    }
    
    /**
     * 16進数を抽出
     */
    public static function extractHexNumber($text) {
        $offset = 0;
        
        // 0xプレフィックスをチェック
        if (strlen($text) >= 2 && 
            $text[0] === '0' && 
            ($text[1] === 'x' || $text[1] === 'X')) {
            $offset = 2;
        }
        
        $length = strspn($text, "0123456789ABCDEFabcdef", $offset);
        
        if ($length === 0) {
            return null;
        }
        
        return substr($text, 0, $offset + $length);
    }
}

// 使用例
$texts = [
    '12345abc',
    '+123.45deg',
    '-789xyz',
    '0xFF00AA',
    '3.14159',
    'abc123'
];

echo "=== 数値抽出 ===\n";
foreach ($texts as $text) {
    echo "\n{$text}:\n";
    echo "  整数: " . (NumberExtractor::extractLeadingNumber($text) ?? 'なし') . "\n";
    echo "  符号付き: " . (NumberExtractor::extractSignedNumber($text) ?? 'なし') . "\n";
    echo "  小数: " . (NumberExtractor::extractDecimalNumber($text) ?? 'なし') . "\n";
    echo "  16進数: " . (NumberExtractor::extractHexNumber($text) ?? 'なし') . "\n";
}

例2: 文字種別の検証

class CharacterValidator {
    /**
     * 先頭が英字のみか確認
     */
    public static function startsWithAlpha($text) {
        $length = strspn($text, 
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
        
        return $length > 0;
    }
    
    /**
     * 先頭が英数字のみか確認
     */
    public static function startsWithAlphanumeric($text) {
        $length = strspn($text, 
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
        
        return $length > 0;
    }
    
    /**
     * 全体が特定の文字種のみか確認
     */
    public static function isOnlyCharacters($text, $allowedChars) {
        return strspn($text, $allowedChars) === strlen($text);
    }
    
    /**
     * 英字のみの長さを取得
     */
    public static function getAlphaLength($text) {
        return strspn($text, 
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
    }
    
    /**
     * 空白文字の長さを取得
     */
    public static function getWhitespaceLength($text) {
        return strspn($text, " \t\n\r\0\x0B");
    }
    
    /**
     * 識別子として有効な長さを取得
     */
    public static function getIdentifierLength($text) {
        // 最初の文字は英字またはアンダースコア
        if (empty($text)) {
            return 0;
        }
        
        $firstChar = strspn($text, 
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_");
        
        if ($firstChar === 0) {
            return 0;
        }
        
        // 2文字目以降は英数字またはアンダースコア
        $restLength = strspn($text, 
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_", 
            1);
        
        return 1 + $restLength;
    }
}

// 使用例
$texts = [
    'abc123',
    '123abc',
    'hello_world',
    '   spaces',
    'validIdentifier123',
    '123invalid'
];

echo "=== 文字種別検証 ===\n";
foreach ($texts as $text) {
    echo "\n{$text}:\n";
    echo "  英字で始まる: " . (CharacterValidator::startsWithAlpha($text) ? 'Yes' : 'No') . "\n";
    echo "  英数字で始まる: " . (CharacterValidator::startsWithAlphanumeric($text) ? 'Yes' : 'No') . "\n";
    echo "  英字長: " . CharacterValidator::getAlphaLength($text) . "\n";
    echo "  空白長: " . CharacterValidator::getWhitespaceLength($text) . "\n";
    echo "  識別子長: " . CharacterValidator::getIdentifierLength($text) . "\n";
}

echo "\n=== 文字種のみか確認 ===\n";
var_dump(CharacterValidator::isOnlyCharacters('12345', '0123456789'));  // true
var_dump(CharacterValidator::isOnlyCharacters('abc123', 'abc'));        // false

例3: トークナイザー

class SimpleTokenizer {
    /**
     * 次の数値トークンを取得
     */
    public static function extractNumber($text, &$position) {
        // 先頭の空白をスキップ
        $wsLength = strspn($text, " \t\n\r", $position);
        $position += $wsLength;
        
        // 数値の長さを取得
        $numLength = strspn($text, "0123456789", $position);
        
        if ($numLength === 0) {
            return null;
        }
        
        $number = substr($text, $position, $numLength);
        $position += $numLength;
        
        return $number;
    }
    
    /**
     * 次の単語トークンを取得
     */
    public static function extractWord($text, &$position) {
        // 先頭の空白をスキップ
        $wsLength = strspn($text, " \t\n\r", $position);
        $position += $wsLength;
        
        // 単語の長さを取得
        $wordLength = strspn($text, 
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 
            $position);
        
        if ($wordLength === 0) {
            return null;
        }
        
        $word = substr($text, $position, $wordLength);
        $position += $wordLength;
        
        return $word;
    }
    
    /**
     * すべてのトークンを取得
     */
    public static function tokenize($text) {
        $tokens = [];
        $position = 0;
        $length = strlen($text);
        
        while ($position < $length) {
            // 空白をスキップ
            $wsLength = strspn($text, " \t\n\r", $position);
            $position += $wsLength;
            
            if ($position >= $length) {
                break;
            }
            
            // 数値をチェック
            $numLength = strspn($text, "0123456789", $position);
            if ($numLength > 0) {
                $tokens[] = [
                    'type' => 'number',
                    'value' => substr($text, $position, $numLength)
                ];
                $position += $numLength;
                continue;
            }
            
            // 単語をチェック
            $wordLength = strspn($text, 
                "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 
                $position);
            if ($wordLength > 0) {
                $tokens[] = [
                    'type' => 'word',
                    'value' => substr($text, $position, $wordLength)
                ];
                $position += $wordLength;
                continue;
            }
            
            // その他の文字
            $tokens[] = [
                'type' => 'other',
                'value' => $text[$position]
            ];
            $position++;
        }
        
        return $tokens;
    }
}

// 使用例
$text = "hello 123 world 456 test";

echo "=== トークン抽出 ===\n";
$position = 0;

while (($word = SimpleTokenizer::extractWord($text, $position)) !== null) {
    echo "単語: {$word}\n";
    
    $number = SimpleTokenizer::extractNumber($text, $position);
    if ($number !== null) {
        echo "数値: {$number}\n";
    }
}

echo "\n=== すべてのトークン ===\n";
$tokens = SimpleTokenizer::tokenize("abc 123 xyz 789 + - *");
foreach ($tokens as $token) {
    echo "{$token['type']}: {$token['value']}\n";
}

例4: データクリーニング

class DataCleaner {
    /**
     * 先頭の空白を削除(長さを返す)
     */
    public static function trimLeadingWhitespace($text) {
        $wsLength = strspn($text, " \t\n\r\0\x0B");
        return substr($text, $wsLength);
    }
    
    /**
     * 先頭の特定文字を削除
     */
    public static function trimLeadingCharacters($text, $chars) {
        $length = strspn($text, $chars);
        return substr($text, $length);
    }
    
    /**
     * 有効な文字のみを抽出
     */
    public static function extractValidChars($text, $validChars) {
        $length = strspn($text, $validChars);
        return substr($text, 0, $length);
    }
    
    /**
     * 先頭のゼロを除去
     */
    public static function removeLeadingZeros($number) {
        $zeroLength = strspn($number, "0");
        
        if ($zeroLength === strlen($number)) {
            return "0";  // すべてゼロの場合
        }
        
        return substr($number, $zeroLength);
    }
    
    /**
     * 数値部分とそれ以外を分離
     */
    public static function separateNumericPrefix($text) {
        $numLength = strspn($text, "0123456789");
        
        return [
            'numeric' => substr($text, 0, $numLength),
            'rest' => substr($text, $numLength)
        ];
    }
}

// 使用例
echo "=== データクリーニング ===\n";

$text = "   hello world";
echo "空白削除: '" . DataCleaner::trimLeadingWhitespace($text) . "'\n";

$text = "###Title";
echo "記号削除: '" . DataCleaner::trimLeadingCharacters($text, "#") . "'\n";

$text = "abc123xyz";
echo "英字のみ抽出: '" . DataCleaner::extractValidChars($text, 
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") . "'\n";

$numbers = ['00123', '0000', '0050'];
echo "\n先頭ゼロ除去:\n";
foreach ($numbers as $num) {
    echo "{$num} → " . DataCleaner::removeLeadingZeros($num) . "\n";
}

$text = "123abc456";
echo "\n数値とそれ以外:\n";
$result = DataCleaner::separateNumericPrefix($text);
print_r($result);

例5: フォーマット検証

class FormatValidator {
    /**
     * 郵便番号の形式をチェック
     */
    public static function validateZipCode($zip) {
        // 123-4567 形式を想定
        $digitLength = strspn($zip, "0123456789");
        
        if ($digitLength !== 3) {
            return false;
        }
        
        if (!isset($zip[3]) || $zip[3] !== '-') {
            return false;
        }
        
        $secondLength = strspn($zip, "0123456789", 4);
        
        return $secondLength === 4 && strlen($zip) === 8;
    }
    
    /**
     * 電話番号の形式をチェック
     */
    public static function validatePhoneNumber($phone) {
        // 090-1234-5678 形式を想定
        $parts = explode('-', $phone);
        
        if (count($parts) !== 3) {
            return false;
        }
        
        // 各部分が数字のみかチェック
        foreach ($parts as $part) {
            if (strspn($part, "0123456789") !== strlen($part)) {
                return false;
            }
        }
        
        return strlen($parts[0]) === 3 && 
               strlen($parts[1]) === 4 && 
               strlen($parts[2]) === 4;
    }
    
    /**
     * IPアドレスの形式をチェック
     */
    public static function validateIpAddress($ip) {
        $parts = explode('.', $ip);
        
        if (count($parts) !== 4) {
            return false;
        }
        
        foreach ($parts as $part) {
            // 数字のみかチェック
            if (strspn($part, "0123456789") !== strlen($part)) {
                return false;
            }
            
            // 0-255の範囲かチェック
            $num = (int)$part;
            if ($num < 0 || $num > 255) {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * 日付形式をチェック
     */
    public static function validateDate($date) {
        // YYYY-MM-DD 形式を想定
        if (strlen($date) !== 10) {
            return false;
        }
        
        // 年(4桁)
        if (strspn($date, "0123456789", 0, 4) !== 4) {
            return false;
        }
        
        // ハイフン
        if ($date[4] !== '-' || $date[7] !== '-') {
            return false;
        }
        
        // 月(2桁)
        if (strspn($date, "0123456789", 5, 2) !== 2) {
            return false;
        }
        
        // 日(2桁)
        if (strspn($date, "0123456789", 8, 2) !== 2) {
            return false;
        }
        
        return true;
    }
}

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

$zipCodes = ['123-4567', '12-34567', 'abc-defg', '1234567'];
echo "\n郵便番号:\n";
foreach ($zipCodes as $zip) {
    $valid = FormatValidator::validateZipCode($zip);
    echo "{$zip}: " . ($valid ? 'OK' : 'NG') . "\n";
}

$phones = ['090-1234-5678', '09012345678', '090-123-4567'];
echo "\n電話番号:\n";
foreach ($phones as $phone) {
    $valid = FormatValidator::validatePhoneNumber($phone);
    echo "{$phone}: " . ($valid ? 'OK' : 'NG') . "\n";
}

$ips = ['192.168.1.1', '256.1.1.1', '192.168.1'];
echo "\nIPアドレス:\n";
foreach ($ips as $ip) {
    $valid = FormatValidator::validateIpAddress($ip);
    echo "{$ip}: " . ($valid ? 'OK' : 'NG') . "\n";
}

例6: パーサー

class CsvParser {
    /**
     * CSVフィールドを解析
     */
    public static function parseField($line, &$position) {
        $length = strlen($line);
        
        // 先頭の空白をスキップ
        $wsLength = strspn($line, " \t", $position);
        $position += $wsLength;
        
        if ($position >= $length) {
            return null;
        }
        
        // 引用符で囲まれている場合
        if ($line[$position] === '"') {
            $position++;
            $fieldStart = $position;
            
            // 終了引用符を探す
            while ($position < $length) {
                if ($line[$position] === '"') {
                    $field = substr($line, $fieldStart, $position - $fieldStart);
                    $position++;
                    
                    // カンマまでスキップ
                    $position += strcspn($line, ",", $position);
                    if ($position < $length && $line[$position] === ',') {
                        $position++;
                    }
                    
                    return $field;
                }
                $position++;
            }
            
            return substr($line, $fieldStart);
        }
        
        // 通常のフィールド(カンマまで)
        $fieldLength = strcspn($line, ",", $position);
        $field = trim(substr($line, $position, $fieldLength));
        $position += $fieldLength;
        
        if ($position < $length && $line[$position] === ',') {
            $position++;
        }
        
        return $field;
    }
    
    /**
     * CSV行を解析
     */
    public static function parseLine($line) {
        $fields = [];
        $position = 0;
        
        while ($position < strlen($line)) {
            $field = self::parseField($line, $position);
            
            if ($field !== null) {
                $fields[] = $field;
            }
        }
        
        return $fields;
    }
}

// 使用例
$csvLines = [
    'apple,banana,orange',
    '"Smith, John",30,Tokyo',
    'abc, def , ghi'
];

echo "=== CSV解析 ===\n";
foreach ($csvLines as $line) {
    echo "\n{$line}:\n";
    $fields = CsvParser::parseLine($line);
    print_r($fields);
}

例7: バイナリデータ処理

class BinaryDataProcessor {
    /**
     * 16進数文字列の長さを取得
     */
    public static function getHexLength($text) {
        return strspn($text, "0123456789ABCDEFabcdef");
    }
    
    /**
     * 2進数文字列の長さを取得
     */
    public static function getBinaryLength($text) {
        return strspn($text, "01");
    }
    
    /**
     * Base64文字列の長さを取得
     */
    public static function getBase64Length($text) {
        return strspn($text, 
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=");
    }
    
    /**
     * 16進数文字列を抽出
     */
    public static function extractHex($text) {
        $length = self::getHexLength($text);
        
        if ($length === 0) {
            return null;
        }
        
        return substr($text, 0, $length);
    }
    
    /**
     * 16進数文字列を検証
     */
    public static function isValidHex($text) {
        return strspn($text, "0123456789ABCDEFabcdef") === strlen($text) &&
               strlen($text) % 2 === 0;
    }
}

// 使用例
$hexStrings = [
    'FF00AA',
    '123456',
    'GHIJKL',
    '1234567'  // 奇数桁
];

echo "=== 16進数処理 ===\n";
foreach ($hexStrings as $hex) {
    echo "\n{$hex}:\n";
    echo "  16進数長: " . BinaryDataProcessor::getHexLength($hex) . "\n";
    echo "  有効: " . (BinaryDataProcessor::isValidHex($hex) ? 'Yes' : 'No') . "\n";
}

$binaryStrings = ['10101010', '11001100', '1234'];
echo "\n=== 2進数長 ===\n";
foreach ($binaryStrings as $bin) {
    echo "{$bin}: " . BinaryDataProcessor::getBinaryLength($bin) . "\n";
}

パフォーマンスの考慮

// 大量のデータで文字集合の長さを計算
$text = str_repeat("abcdefghij", 10000);
$chars = "abcdefghijklmnopqrstuvwxyz";

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

// 手動チェック
$start = microtime(true);
for ($i = 0; $i < 10000; $i++) {
    $length = 0;
    for ($j = 0; $j < strlen($text); $j++) {
        if (strpos($chars, $text[$j]) === false) {
            break;
        }
        $length++;
    }
}
$time2 = microtime(true) - $start;

echo "strspn(): {$time1}秒\n";
echo "手動チェック: {$time2}秒\n";

// strspn()は非常に高速に最適化されている

まとめ

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

できること:

  • 指定文字集合に含まれる連続文字の長さを取得
  • 先頭からの有効文字をチェック
  • 文字種別の検証

strcspn()との関係:

  • strspn(): 指定文字に含まれる部分の長さ
  • strcspn(): 指定文字に含まれない部分の長さ
  • 補完的な関係

推奨される使用場面:

  • 数値の抽出
  • 文字種別の検証
  • トークナイザーの実装
  • データクリーニング
  • フォーマット検証
  • パーサーの実装

利点:

  • 効率的な文字集合チェック
  • バイナリセーフ
  • 高速に最適化されている

注意点:

  • 先頭からの連続した文字のみカウント
  • 途中で違う文字が現れたら終了
  • マルチバイト文字には注意

関連関数:

  • strcspn(): 指定文字に含まれない部分の長さ
  • strpbrk(): 指定文字集合のいずれかを検索
  • preg_match(): 正規表現でのパターンマッチング

パフォーマンス:

  • 非常に高速(C言語レベルで最適化)
  • 複雑な文字集合チェックに適している

strspn()は、特定の文字集合に含まれる部分の長さを効率的に取得したい場合に非常に便利です。数値抽出やトークナイザー、データ検証など、様々な場面で活用できる関数です!

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