[PHP]strcspn関数を完全解説!文字列から特定文字までの長さを取得

PHP

こんにちは!今回は、PHPの標準関数であるstrcspn()について詳しく解説していきます。文字列内で指定した文字集合に含まれない部分の長さを取得できる、便利な関数です!

strcspn関数とは?

strcspn()関数は、文字列の先頭から、指定した文字集合のいずれかの文字が最初に現れるまでの長さを返す関数です。

“complementary span”(補完的な範囲)の略で、strspn()の逆の動作をします。特定の文字が出現するまでの部分を切り出したい場合に便利です!

基本的な構文

strcspn(
    string $string,
    string $characters,
    int $offset = 0,
    ?int $length = null
): int
  • $string: 対象の文字列
  • $characters: 検索する文字の集合
  • $offset: 検索開始位置(オプション)
  • $length: 検索する長さ(オプション)
  • 戻り値: 指定文字が現れるまでの長さ(文字数)

基本的な使用例

シンプルな使用例

$text = "Hello World";

// スペースまでの長さ
$length = strcspn($text, " ");
echo $length . "\n";  // 5

// "Hello"の部分(5文字)

// 複数の文字を指定
$length = strcspn($text, " W");
echo $length . "\n";  // 5

// "H"または"W"またはスペースまで

文字列の抽出

$text = "apple,banana,orange";

// カンマまでの部分を取得
$length = strcspn($text, ",");
$first = substr($text, 0, $length);
echo $first . "\n";  // apple

オフセットの使用

$text = "apple,banana,orange";

// 6文字目('b')から開始
$length = strcspn($text, ",", 6);
echo $length . "\n";  // 6("banana"の長さ)

$second = substr($text, 6, $length);
echo $second . "\n";  // banana

実践的な使用例

例1: CSVパーサー

class SimpleCsvParser {
    public static function parseLine($line, $delimiter = ',', $enclosure = '"') {
        $values = [];
        $pos = 0;
        $len = strlen($line);
        
        while ($pos < $len) {
            // 先頭の空白をスキップ
            $pos += strspn($line, " \t", $pos);
            
            if ($pos >= $len) {
                break;
            }
            
            // 引用符で囲まれている場合
            if ($line[$pos] === $enclosure) {
                $pos++; // 開始引用符をスキップ
                
                // 終了引用符を探す
                $valueStart = $pos;
                $valueEnd = strpos($line, $enclosure, $pos);
                
                if ($valueEnd === false) {
                    $valueEnd = $len;
                }
                
                $value = substr($line, $valueStart, $valueEnd - $valueStart);
                $values[] = $value;
                
                $pos = $valueEnd + 1;
                
                // デリミタまでスキップ
                $pos += strcspn($line, $delimiter, $pos);
                $pos++; // デリミタをスキップ
            } else {
                // 通常の値(デリミタまで)
                $valueLen = strcspn($line, $delimiter, $pos);
                $value = substr($line, $pos, $valueLen);
                $values[] = trim($value);
                
                $pos += $valueLen + 1;
            }
        }
        
        return $values;
    }
    
    public static function extractColumn($lines, $columnIndex) {
        $column = [];
        
        foreach ($lines as $line) {
            $values = self::parseLine($line);
            
            if (isset($values[$columnIndex])) {
                $column[] = $values[$columnIndex];
            }
        }
        
        return $column;
    }
}

// 使用例
$csvLine = 'apple,banana,"orange, red",grape';
$values = SimpleCsvParser::parseLine($csvLine);
print_r($values);
// Array ( [0] => apple [1] => banana [2] => orange, red [3] => grape )

$csvLines = [
    'Name,Age,City',
    'Alice,25,Tokyo',
    'Bob,30,Osaka',
    'Charlie,35,Kyoto'
];

$names = SimpleCsvParser::extractColumn($csvLines, 0);
print_r($names);
// Array ( [0] => Name [1] => Alice [2] => Bob [3] => Charlie )

例2: URL・パスの解析

class PathParser {
    public static function extractProtocol($url) {
        // "://" までの部分を取得
        $length = strcspn($url, ":");
        
        if ($length === strlen($url)) {
            return null;
        }
        
        return substr($url, 0, $length);
    }
    
    public static function extractHost($url) {
        // プロトコルをスキップ
        $start = strpos($url, "://");
        
        if ($start === false) {
            $start = 0;
        } else {
            $start += 3;
        }
        
        // "/" または "?" または "#" までの長さ
        $length = strcspn($url, "/?#", $start);
        
        return substr($url, $start, $length);
    }
    
    public static function extractPath($url) {
        // ホスト部分をスキップ
        $hostEnd = strpos($url, "://");
        
        if ($hostEnd !== false) {
            $hostEnd += 3;
            $pathStart = $hostEnd + strcspn($url, "/", $hostEnd);
        } else {
            $pathStart = strcspn($url, "/");
        }
        
        // "?" または "#" までの長さ
        $length = strcspn($url, "?#", $pathStart);
        
        return substr($url, $pathStart, $length);
    }
    
    public static function extractFilename($path) {
        // 最後の "/" の後ろから開始
        $lastSlash = strrpos($path, '/');
        
        if ($lastSlash === false) {
            $start = 0;
        } else {
            $start = $lastSlash + 1;
        }
        
        // "?" または "#" までの長さ
        $length = strcspn($path, "?#", $start);
        
        return substr($path, $start, $length);
    }
}

// 使用例
$url = "https://example.com:8080/path/to/file.html?key=value#section";

echo "プロトコル: " . PathParser::extractProtocol($url) . "\n";
// https

echo "ホスト: " . PathParser::extractHost($url) . "\n";
// example.com:8080

echo "パス: " . PathParser::extractPath($url) . "\n";
// /path/to/file.html

echo "ファイル名: " . PathParser::extractFilename($url) . "\n";
// file.html

例3: トークナイザー

class SimpleTokenizer {
    private $text;
    private $pos = 0;
    private $length;
    
    public function __construct($text) {
        $this->text = $text;
        $this->length = strlen($text);
    }
    
    public function nextToken($delimiters = " \t\n\r") {
        // 先頭の区切り文字をスキップ
        $this->pos += strspn($this->text, $delimiters, $this->pos);
        
        if ($this->pos >= $this->length) {
            return null;
        }
        
        // 次の区切り文字までの長さ
        $tokenLength = strcspn($this->text, $delimiters, $this->pos);
        
        $token = substr($this->text, $this->pos, $tokenLength);
        $this->pos += $tokenLength;
        
        return $token;
    }
    
    public function getAllTokens($delimiters = " \t\n\r") {
        $tokens = [];
        
        while (($token = $this->nextToken($delimiters)) !== null) {
            $tokens[] = $token;
        }
        
        return $tokens;
    }
    
    public function reset() {
        $this->pos = 0;
    }
}

// 使用例
$text = "Hello World\tThis is\na test";
$tokenizer = new SimpleTokenizer($text);

echo "トークン抽出:\n";
while (($token = $tokenizer->nextToken()) !== null) {
    echo "  [{$token}]\n";
}
// [Hello] [World] [This] [is] [a] [test]

$tokenizer->reset();
$tokens = $tokenizer->getAllTokens();
print_r($tokens);

例4: バリデーション

class InputValidator {
    public static function validateAlphanumeric($input) {
        // 英数字以外の文字が含まれていないかチェック
        $validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        
        $invalidPos = strcspn($input, $validChars);
        
        if ($invalidPos === strlen($input)) {
            return ['valid' => true];
        }
        
        return [
            'valid' => false,
            'error' => '無効な文字が含まれています',
            'position' => $invalidPos,
            'character' => $input[$invalidPos]
        ];
    }
    
    public static function validateNumeric($input) {
        // 数字以外の文字が含まれていないかチェック
        $validChars = '0123456789';
        
        $invalidPos = strcspn($input, $validChars);
        
        return $invalidPos === strlen($input);
    }
    
    public static function validateFilename($filename) {
        // ファイル名に使用できない文字をチェック
        $invalidChars = '/\\?%*:|"<>';
        
        $pos = strcspn($filename, $invalidChars);
        
        if ($pos === strlen($filename)) {
            return ['valid' => true];
        }
        
        return [
            'valid' => false,
            'error' => 'ファイル名に使用できない文字が含まれています',
            'position' => $pos,
            'character' => $filename[$pos]
        ];
    }
    
    public static function validateEmail($email) {
        // @の前の部分をチェック
        $localLength = strcspn($email, '@');
        
        if ($localLength === 0 || $localLength === strlen($email)) {
            return [
                'valid' => false,
                'error' => '無効なメールアドレス形式'
            ];
        }
        
        $local = substr($email, 0, $localLength);
        $domain = substr($email, $localLength + 1);
        
        // ドメイン部分のチェック
        if (empty($domain) || strcspn($domain, '.') === strlen($domain)) {
            return [
                'valid' => false,
                'error' => 'ドメイン部分が無効'
            ];
        }
        
        return [
            'valid' => true,
            'local' => $local,
            'domain' => $domain
        ];
    }
}

// 使用例
$result = InputValidator::validateAlphanumeric('abc123');
print_r($result);  // valid => true

$result = InputValidator::validateAlphanumeric('abc-123');
print_r($result);  // valid => false, position => 3, character => -

var_dump(InputValidator::validateNumeric('12345'));   // true
var_dump(InputValidator::validateNumeric('123a45'));  // false

$result = InputValidator::validateFilename('document.txt');
print_r($result);  // valid => true

$result = InputValidator::validateFilename('file:name.txt');
print_r($result);  // valid => false

$result = InputValidator::validateEmail('user@example.com');
print_r($result);  // valid => true

例5: テキストフォーマッター

class TextFormatter {
    public static function extractSentences($text) {
        $sentences = [];
        $pos = 0;
        $length = strlen($text);
        
        while ($pos < $length) {
            // 先頭の空白をスキップ
            $pos += strspn($text, " \t\n\r", $pos);
            
            if ($pos >= $length) {
                break;
            }
            
            // 文末記号までの長さ
            $sentenceLength = strcspn($text, ".!?", $pos);
            
            // 文末記号を含める
            if ($pos + $sentenceLength < $length) {
                $sentenceLength++;
            }
            
            $sentence = trim(substr($text, $pos, $sentenceLength));
            
            if (!empty($sentence)) {
                $sentences[] = $sentence;
            }
            
            $pos += $sentenceLength;
        }
        
        return $sentences;
    }
    
    public static function extractWords($text, $minLength = 1) {
        $words = [];
        $pos = 0;
        $length = strlen($text);
        
        // 単語の区切り文字
        $delimiters = " \t\n\r.,;:!?()[]{}\"'";
        
        while ($pos < $length) {
            // 区切り文字をスキップ
            $pos += strspn($text, $delimiters, $pos);
            
            if ($pos >= $length) {
                break;
            }
            
            // 次の区切り文字までの長さ
            $wordLength = strcspn($text, $delimiters, $pos);
            
            $word = substr($text, $pos, $wordLength);
            
            if (strlen($word) >= $minLength) {
                $words[] = $word;
            }
            
            $pos += $wordLength;
        }
        
        return $words;
    }
    
    public static function truncateAtWord($text, $maxLength) {
        if (strlen($text) <= $maxLength) {
            return $text;
        }
        
        // maxLength以降の最初の空白を探す
        $truncated = substr($text, 0, $maxLength);
        
        // 最後の単語を切り捨てないように、最後の空白を探す
        $lastSpace = strrpos($truncated, ' ');
        
        if ($lastSpace !== false) {
            $truncated = substr($truncated, 0, $lastSpace);
        }
        
        return $truncated . '...';
    }
}

// 使用例
$text = "Hello world. This is a test. How are you?";

$sentences = TextFormatter::extractSentences($text);
print_r($sentences);
// Array ( [0] => Hello world. [1] => This is a test. [2] => How are you? )

$words = TextFormatter::extractWords($text);
print_r($words);
// Array ( [0] => Hello [1] => world [2] => This [3] => is [4] => a [5] => test [6] => How [7] => are [8] => you )

$long = "This is a very long sentence that needs to be truncated at a word boundary";
echo TextFormatter::truncateAtWord($long, 30) . "\n";
// This is a very long sentence...

例6: コマンドライン引数パーサー

class CommandLineParser {
    public static function parseArguments($commandLine) {
        $args = [];
        $pos = 0;
        $length = strlen($commandLine);
        
        while ($pos < $length) {
            // 先頭の空白をスキップ
            $pos += strspn($commandLine, " \t", $pos);
            
            if ($pos >= $length) {
                break;
            }
            
            // 引用符で囲まれている場合
            if ($commandLine[$pos] === '"' || $commandLine[$pos] === "'") {
                $quote = $commandLine[$pos];
                $pos++; // 開始引用符をスキップ
                
                // 終了引用符を探す
                $argStart = $pos;
                $endPos = strpos($commandLine, $quote, $pos);
                
                if ($endPos === false) {
                    $endPos = $length;
                }
                
                $arg = substr($commandLine, $argStart, $endPos - $argStart);
                $args[] = $arg;
                
                $pos = $endPos + 1;
            } else {
                // 通常の引数(空白まで)
                $argLength = strcspn($commandLine, " \t", $pos);
                $arg = substr($commandLine, $pos, $argLength);
                $args[] = $arg;
                
                $pos += $argLength;
            }
        }
        
        return $args;
    }
    
    public static function parseOptions($args) {
        $options = [];
        $positional = [];
        
        foreach ($args as $arg) {
            if (strpos($arg, '--') === 0) {
                // ロングオプション
                $optionLength = strcspn($arg, '=', 2);
                $name = substr($arg, 2, $optionLength - 2);
                
                if ($optionLength < strlen($arg)) {
                    $value = substr($arg, $optionLength + 1);
                } else {
                    $value = true;
                }
                
                $options[$name] = $value;
            } elseif (strpos($arg, '-') === 0 && strlen($arg) > 1) {
                // ショートオプション
                $options[substr($arg, 1)] = true;
            } else {
                // 位置引数
                $positional[] = $arg;
            }
        }
        
        return [
            'options' => $options,
            'positional' => $positional
        ];
    }
}

// 使用例
$commandLine = 'command --input="file.txt" --verbose -d output.txt';
$args = CommandLineParser::parseArguments($commandLine);
print_r($args);
// Array ( [0] => command [1] => --input=file.txt [2] => --verbose [3] => -d [4] => output.txt )

$parsed = CommandLineParser::parseOptions($args);
print_r($parsed);
/*
Array (
    [options] => Array (
        [input] => file.txt
        [verbose] => 1
        [d] => 1
    )
    [positional] => Array (
        [0] => command
        [1] => output.txt
    )
)
*/

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

class DataCleaner {
    public static function removeInvalidCharacters($text, $validChars) {
        $result = '';
        $pos = 0;
        $length = strlen($text);
        
        while ($pos < $length) {
            // 有効な文字の連続を取得
            $validLength = strspn($text, $validChars, $pos);
            
            if ($validLength > 0) {
                $result .= substr($text, $pos, $validLength);
                $pos += $validLength;
            }
            
            // 無効な文字をスキップ
            $invalidLength = strcspn($text, $validChars, $pos);
            $pos += $invalidLength;
        }
        
        return $result;
    }
    
    public static function extractDigits($text) {
        return self::removeInvalidCharacters($text, '0123456789');
    }
    
    public static function extractAlphabetic($text) {
        $validChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        return self::removeInvalidCharacters($text, $validChars);
    }
    
    public static function normalizeWhitespace($text) {
        $result = '';
        $pos = 0;
        $length = strlen($text);
        $whitespace = " \t\n\r";
        
        while ($pos < $length) {
            // 非空白文字を取得
            $nonWhiteLength = strcspn($text, $whitespace, $pos);
            
            if ($nonWhiteLength > 0) {
                $result .= substr($text, $pos, $nonWhiteLength);
                $pos += $nonWhiteLength;
            }
            
            // 空白をスキップして、単一のスペースに置換
            $whiteLength = strspn($text, $whitespace, $pos);
            
            if ($whiteLength > 0 && $pos + $whiteLength < $length) {
                $result .= ' ';
                $pos += $whiteLength;
            } else {
                $pos += $whiteLength;
            }
        }
        
        return trim($result);
    }
}

// 使用例
$text = "Phone: 123-456-7890";
echo DataCleaner::extractDigits($text) . "\n";
// 1234567890

$text = "Hello123World456";
echo DataCleaner::extractAlphabetic($text) . "\n";
// HelloWorld

$text = "Hello    World\t\tTest\n\nString";
echo DataCleaner::normalizeWhitespace($text) . "\n";
// Hello World Test String

strspn()との違い

$text = "12345abc";

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

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

// 逆の関係
$text2 = "abc12345";
echo strspn($text2, "abc") . "\n";    // 3
echo strcspn($text2, "0123456789") . "\n";  // 3

パフォーマンスの考慮

// 方法1: strcspn()を使用
function method1($text, $delimiter) {
    $pos = strcspn($text, $delimiter);
    return substr($text, 0, $pos);
}

// 方法2: strpos()を使用
function method2($text, $delimiter) {
    $pos = strpos($text, $delimiter);
    if ($pos === false) {
        return $text;
    }
    return substr($text, 0, $pos);
}

$text = "Hello World, this is a test";
$iterations = 100000;

$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    method1($text, ",");
}
$time1 = microtime(true) - $start;

$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    method2($text, ",");
}
$time2 = microtime(true) - $start;

echo "strcspn(): {$time1}秒\n";
echo "strpos(): {$time2}秒\n";

// strcspn()は複数の文字を一度に検索できるため、
// 複数の区切り文字がある場合に効率的

まとめ

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

できること:

  • 指定文字集合が現れるまでの長さを取得
  • 複数の区切り文字を一度に指定可能
  • オフセットと長さの指定でフレキシブルな検索

strspn()との関係:

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

推奨される使用場面:

  • CSVやTSVのパース
  • トークナイザーの実装
  • URL・パスの解析
  • 入力バリデーション
  • テキストフォーマッティング
  • コマンドライン引数の解析

関連関数:

  • strspn(): 指定文字に含まれる部分の長さ
  • strpos(): 指定文字列の位置を検索
  • strchr() / strstr(): 指定文字以降を取得
  • strtok(): トークン分割

利点:

  • 複数の区切り文字を一度に指定できる
  • 効率的な文字列検索
  • バイナリセーフ

strcspn()は、特定の文字が現れるまでの部分を効率的に取得したい場合に非常に便利です。strspn()と組み合わせることで、柔軟な文字列解析処理を実装できます!

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