[PHP]strpbrk関数を完全解説!文字集合から文字を検索する方法

PHP

こんにちは!今回は、PHPの標準関数であるstrpbrk()について詳しく解説していきます。文字列内で指定した文字集合のいずれかの文字が最初に現れる位置を検索できる関数です!

strpbrk関数とは?

strpbrk()関数は、文字列内で指定した文字集合のいずれかの文字が最初に現れる位置から、文字列の最後までを返す関数です。

“string pointer break”の略で、複数の区切り文字を一度に検索したい場合や、特定の文字集合が現れる位置を見つけたい場合に便利です!

基本的な構文

strpbrk(string $string, string $characters): string|false
  • $string: 検索対象の文字列
  • $characters: 検索する文字の集合
  • 戻り値:
  • 見つかった場合: その文字から文字列の最後までの部分文字列
  • 見つからない場合: false

基本的な使用例

シンプルな使用例

// 区切り文字を検索
$text = "Hello, World!";

// カンマまたは感嘆符を検索
$result = strpbrk($text, ",!");
echo $result . "\n";
// 出力: , World!(最初に見つかったのはカンマ)

// 感嘆符から
$result = strpbrk($text, "!");
echo $result . "\n";
// 出力: !

// 複数の文字を指定
$result = strpbrk($text, "oW");
echo $result . "\n";
// 出力: o, World!(最初に見つかったのは'o')

見つからない場合

$text = "Hello World";

// 含まれていない文字を検索
$result = strpbrk($text, "xyz");
var_dump($result);
// 出力: bool(false)

複数の区切り文字

$csv = "apple,banana;orange:grape";

// カンマ、セミコロン、コロンのいずれかを検索
$result = strpbrk($csv, ",;:");
echo $result . "\n";
// 出力: ,banana;orange:grape(最初に見つかったのはカンマ)

実践的な使用例

例1: 区切り文字の検出

class DelimiterDetector {
    /**
     * 最初の区切り文字を検出
     */
    public static function findFirstDelimiter($text, $delimiters) {
        $result = strpbrk($text, $delimiters);

        if ($result === false) {
            return null;
        }

        return [
            'delimiter' => $result[0],
            'position' => strlen($text) - strlen($result),
            'remaining' => $result
        ];
    }

    /**
     * 使用されている区切り文字を判定
     */
    public static function detectDelimiter($text, $possibleDelimiters = ",;\t|") {
        $result = strpbrk($text, $possibleDelimiters);

        if ($result === false) {
            return null;
        }

        return $result[0];
    }

    /**
     * すべての区切り文字の位置を取得
     */
    public static function findAllDelimiters($text, $delimiters) {
        $positions = [];
        $offset = 0;

        while (($remaining = strpbrk(substr($text, $offset), $delimiters)) !== false) {
            $pos = strlen($text) - strlen($remaining) - (strlen($text) - strlen(substr($text, $offset)));
            $actualPos = $offset + (strlen(substr($text, $offset)) - strlen($remaining));

            $positions[] = [
                'delimiter' => $remaining[0],
                'position' => $actualPos
            ];

            $offset = $actualPos + 1;

            if ($offset >= strlen($text)) {
                break;
            }
        }

        return $positions;
    }

    /**
     * 最も頻繁に使用される区切り文字を検出
     */
    public static function detectMostCommonDelimiter($text, $delimiters = ",;\t|") {
        $counts = [];

        foreach (str_split($delimiters) as $delimiter) {
            $counts[$delimiter] = substr_count($text, $delimiter);
        }

        arsort($counts);

        foreach ($counts as $delimiter => $count) {
            if ($count > 0) {
                return [
                    'delimiter' => $delimiter,
                    'count' => $count
                ];
            }
        }

        return null;
    }
}

// 使用例
$csv = "apple,banana,orange,grape";
$tsv = "name\tage\tcity";
$mixed = "item1;item2,item3|item4";

echo "=== 最初の区切り文字検出 ===\n";
$info = DelimiterDetector::findFirstDelimiter($csv, ",;\t|");
print_r($info);
// delimiter => ,, position => 5

echo "\n=== 区切り文字判定 ===\n";
echo "CSV: " . DelimiterDetector::detectDelimiter($csv) . "\n";  // ,
echo "TSV: " . DelimiterDetector::detectDelimiter($tsv) . "\n";  // \t
echo "Mixed: " . DelimiterDetector::detectDelimiter($mixed) . "\n";  // ;

echo "\n=== 最も頻繁な区切り文字 ===\n";
$common = DelimiterDetector::detectMostCommonDelimiter($csv);
print_r($common);
// delimiter => ,, count => 3

例2: トークナイザー

class SimpleTokenizer {
    private $text;
    private $position = 0;

    public function __construct($text) {
        $this->text = $text;
    }

    /**
     * 次のトークンを取得
     */
    public function nextToken($delimiters = " \t\n\r") {
        // 先頭の区切り文字をスキップ
        while ($this->position < strlen($this->text) && 
               strpos($delimiters, $this->text[$this->position]) !== false) {
            $this->position++;
        }

        if ($this->position >= strlen($this->text)) {
            return null;
        }

        // 次の区切り文字までを取得
        $remaining = substr($this->text, $this->position);
        $nextDelimiter = strpbrk($remaining, $delimiters);

        if ($nextDelimiter === false) {
            // 区切り文字が見つからない = 最後のトークン
            $token = $remaining;
            $this->position = strlen($this->text);
        } else {
            $tokenLength = strlen($remaining) - strlen($nextDelimiter);
            $token = substr($remaining, 0, $tokenLength);
            $this->position += $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->position = 0;
    }
}

// 使用例
$text = "apple,banana;orange|grape\tmelon";

$tokenizer = new SimpleTokenizer($text);

echo "=== トークン抽出 ===\n";
while (($token = $tokenizer->nextToken(",;|\t")) !== null) {
    echo "Token: [{$token}]\n";
}
/*
Token: [apple]
Token: [banana]
Token: [orange]
Token: [grape]
Token: [melon]
*/

echo "\n=== すべてのトークン取得 ===\n";
$tokenizer->reset();
$tokens = $tokenizer->getAllTokens(",;|\t");
print_r($tokens);

例3: パス解析

class PathParser {
    /**
     * 最初のパス区切り文字を検出
     */
    public static function findFirstSeparator($path) {
        $separators = "/\\";
        $result = strpbrk($path, $separators);

        if ($result === false) {
            return null;
        }

        return [
            'separator' => $result[0],
            'position' => strlen($path) - strlen($result)
        ];
    }

    /**
     * パスをセグメントに分割
     */
    public static function splitPath($path) {
        $segments = [];
        $current = $path;

        while (strlen($current) > 0) {
            $nextSep = strpbrk($current, "/\\");

            if ($nextSep === false) {
                // 最後のセグメント
                if (strlen($current) > 0) {
                    $segments[] = $current;
                }
                break;
            }

            $segmentLength = strlen($current) - strlen($nextSep);

            if ($segmentLength > 0) {
                $segments[] = substr($current, 0, $segmentLength);
            }

            // セグメントと区切り文字をスキップ
            $current = substr($nextSep, 1);
        }

        return $segments;
    }

    /**
     * パスの形式を判定(Windows/Unix)
     */
    public static function detectPathStyle($path) {
        $backslash = strpbrk($path, "\\");
        $slash = strpbrk($path, "/");

        if ($backslash !== false && $slash === false) {
            return 'windows';
        } elseif ($slash !== false && $backslash === false) {
            return 'unix';
        } elseif ($backslash !== false && $slash !== false) {
            return 'mixed';
        } else {
            return 'none';
        }
    }

    /**
     * パス区切り文字を正規化
     */
    public static function normalizePath($path, $targetSeparator = '/') {
        $result = '';
        $current = $path;

        while (strlen($current) > 0) {
            $nextSep = strpbrk($current, "/\\");

            if ($nextSep === false) {
                $result .= $current;
                break;
            }

            $segmentLength = strlen($current) - strlen($nextSep);
            $result .= substr($current, 0, $segmentLength);
            $result .= $targetSeparator;

            $current = substr($nextSep, 1);
        }

        return $result;
    }
}

// 使用例
$paths = [
    'C:\\Users\\Documents\\file.txt',
    '/home/user/documents/file.txt',
    'folder\\subfolder/file.txt'
];

echo "=== パス形式判定 ===\n";
foreach ($paths as $path) {
    $style = PathParser::detectPathStyle($path);
    echo "{$path}: {$style}\n";
}

echo "\n=== パス分割 ===\n";
foreach ($paths as $path) {
    echo "\n{$path}:\n";
    $segments = PathParser::splitPath($path);
    print_r($segments);
}

echo "\n=== パス正規化 ===\n";
foreach ($paths as $path) {
    echo PathParser::normalizePath($path) . "\n";
}

例4: 文字検証

class CharacterValidator {
    /**
     * 無効な文字が含まれているかチェック
     */
    public static function containsInvalidChars($text, $invalidChars) {
        return strpbrk($text, $invalidChars) !== false;
    }

    /**
     * 最初の無効な文字を検出
     */
    public static function findFirstInvalidChar($text, $invalidChars) {
        $result = strpbrk($text, $invalidChars);

        if ($result === false) {
            return null;
        }

        return [
            'character' => $result[0],
            'position' => strlen($text) - strlen($result),
            'context' => substr($text, max(0, strlen($text) - strlen($result) - 5), 10)
        ];
    }

    /**
     * すべての無効な文字を検出
     */
    public static function findAllInvalidChars($text, $invalidChars) {
        $invalid = [];
        $offset = 0;

        while ($offset < strlen($text)) {
            $remaining = substr($text, $offset);
            $result = strpbrk($remaining, $invalidChars);

            if ($result === false) {
                break;
            }

            $position = $offset + (strlen($remaining) - strlen($result));

            $invalid[] = [
                'character' => $result[0],
                'position' => $position
            ];

            $offset = $position + 1;
        }

        return $invalid;
    }

    /**
     * ファイル名の検証
     */
    public static function validateFilename($filename) {
        // Windowsで使用できない文字
        $invalidChars = '<>:"/\\|?*';

        $result = strpbrk($filename, $invalidChars);

        if ($result === false) {
            return ['valid' => true];
        }

        return [
            'valid' => false,
            'error' => 'ファイル名に使用できない文字が含まれています',
            'invalid_char' => $result[0],
            'position' => strlen($filename) - strlen($result)
        ];
    }

    /**
     * 特殊文字のチェック
     */
    public static function hasSpecialChars($text) {
        $specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?/~`';
        return strpbrk($text, $specialChars) !== false;
    }
}

// 使用例
echo "=== 無効文字検出 ===\n";
$text = "Hello<World>";
$invalid = CharacterValidator::findFirstInvalidChar($text, '<>');
print_r($invalid);
// character => <, position => 5

echo "\n=== すべての無効文字 ===\n";
$text = "file<name>test";
$allInvalid = CharacterValidator::findAllInvalidChars($text, '<>');
print_r($allInvalid);

echo "\n=== ファイル名検証 ===\n";
$filenames = [
    'document.txt',
    'file:name.txt',
    'my/file.txt',
    'valid_name.pdf'
];

foreach ($filenames as $filename) {
    $result = CharacterValidator::validateFilename($filename);
    echo "{$filename}: " . ($result['valid'] ? 'OK' : 'NG - ' . $result['invalid_char']) . "\n";
}

echo "\n=== 特殊文字チェック ===\n";
var_dump(CharacterValidator::hasSpecialChars('Hello123'));      // false
var_dump(CharacterValidator::hasSpecialChars('Hello@World'));   // true

例5: URL解析

class UrlAnalyzer {
    /**
     * クエリ文字列の開始位置を検出
     */
    public static function findQueryStart($url) {
        $result = strpbrk($url, '?#');

        if ($result === false) {
            return null;
        }

        return [
            'character' => $result[0],
            'position' => strlen($url) - strlen($result),
            'remaining' => $result
        ];
    }

    /**
     * URLを構成要素に分割
     */
    public static function parseUrl($url) {
        // プロトコル区切りを検出
        $protocolEnd = strpbrk($url, ':');

        if ($protocolEnd !== false && substr($protocolEnd, 0, 3) === '://') {
            $protocol = substr($url, 0, strlen($url) - strlen($protocolEnd));
            $afterProtocol = substr($protocolEnd, 3);
        } else {
            $protocol = null;
            $afterProtocol = $url;
        }

        // パスまたはクエリの開始を検出
        $pathStart = strpbrk($afterProtocol, '/?#');

        if ($pathStart !== false) {
            $host = substr($afterProtocol, 0, strlen($afterProtocol) - strlen($pathStart));
            $path = $pathStart;
        } else {
            $host = $afterProtocol;
            $path = '';
        }

        return [
            'protocol' => $protocol,
            'host' => $host,
            'path' => $path
        ];
    }

    /**
     * URLパラメータの区切り文字を検出
     */
    public static function detectParamSeparator($queryString) {
        $separators = '&;';
        $result = strpbrk($queryString, $separators);

        if ($result === false) {
            return null;
        }

        return $result[0];
    }
}

// 使用例
$url = "https://example.com/path/to/page?key1=value1&key2=value2#section";

echo "=== クエリ開始位置 ===\n";
$queryStart = UrlAnalyzer::findQueryStart($url);
print_r($queryStart);

echo "\n=== URL解析 ===\n";
$parsed = UrlAnalyzer::parseUrl($url);
print_r($parsed);

$queryStrings = [
    'key1=value1&key2=value2',
    'key1=value1;key2=value2',
    'single_param=value'
];

echo "\n=== パラメータ区切り文字 ===\n";
foreach ($queryStrings as $qs) {
    $separator = UrlAnalyzer::detectParamSeparator($qs);
    echo "{$qs}: " . ($separator ?? 'none') . "\n";
}

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

class DataCleaner {
    /**
     * 制御文字を検出
     */
    public static function findControlChars($text) {
        // 制御文字(ASCII 0-31)
        $controlChars = '';
        for ($i = 0; $i < 32; $i++) {
            $controlChars .= chr($i);
        }

        $result = strpbrk($text, $controlChars);

        if ($result === false) {
            return null;
        }

        return [
            'found' => true,
            'position' => strlen($text) - strlen($result),
            'ascii_code' => ord($result[0])
        ];
    }

    /**
     * 制御文字を除去
     */
    public static function removeControlChars($text) {
        $result = '';
        $controlChars = '';

        for ($i = 0; $i < 32; $i++) {
            if ($i !== 9 && $i !== 10 && $i !== 13) {  // タブ、LF、CRは保持
                $controlChars .= chr($i);
            }
        }

        $current = $text;

        while (strlen($current) > 0) {
            $nextControl = strpbrk($current, $controlChars);

            if ($nextControl === false) {
                $result .= $current;
                break;
            }

            $cleanLength = strlen($current) - strlen($nextControl);
            $result .= substr($current, 0, $cleanLength);

            // 制御文字をスキップ
            $current = substr($nextControl, 1);
        }

        return $result;
    }

    /**
     * 空白文字を正規化
     */
    public static function normalizeWhitespace($text) {
        $whitespace = " \t\n\r\0\x0B";
        $result = '';
        $inWhitespace = false;
        $i = 0;

        while ($i < strlen($text)) {
            if (strpos($whitespace, $text[$i]) !== false) {
                if (!$inWhitespace) {
                    $result .= ' ';
                    $inWhitespace = true;
                }
            } else {
                $result .= $text[$i];
                $inWhitespace = false;
            }
            $i++;
        }

        return trim($result);
    }
}

// 使用例
$dirtyText = "Hello\0World\x01Test";

echo "=== 制御文字検出 ===\n";
$control = DataCleaner::findControlChars($dirtyText);
print_r($control);

echo "\n=== 制御文字除去 ===\n";
$cleaned = DataCleaner::removeControlChars($dirtyText);
echo "元: " . var_export($dirtyText, true) . "\n";
echo "後: " . var_export($cleaned, true) . "\n";

$spacedText = "Hello    World\t\tTest\n\nData";
echo "\n=== 空白正規化 ===\n";
echo "元: " . var_export($spacedText, true) . "\n";
echo "後: " . var_export(DataCleaner::normalizeWhitespace($spacedText), true) . "\n";

例7: テキスト解析

class TextAnalyzer {
    /**
     * 文の終わりを検出
     */
    public static function findSentenceEnd($text) {
        $endMarkers = '.!?';
        $result = strpbrk($text, $endMarkers);

        if ($result === false) {
            return null;
        }

        return [
            'marker' => $result[0],
            'position' => strlen($text) - strlen($result)
        ];
    }

    /**
     * テキストを文に分割
     */
    public static function splitIntoSentences($text) {
        $sentences = [];
        $current = $text;
        $endMarkers = '.!?';

        while (strlen(trim($current)) > 0) {
            $result = strpbrk($current, $endMarkers);

            if ($result === false) {
                // 最後の文(終端記号なし)
                $sentences[] = trim($current);
                break;
            }

            $sentenceLength = strlen($current) - strlen($result) + 1;
            $sentence = trim(substr($current, 0, $sentenceLength));

            if (strlen($sentence) > 0) {
                $sentences[] = $sentence;
            }

            $current = trim(substr($result, 1));
        }

        return $sentences;
    }

    /**
     * 引用符を検出
     */
    public static function findQuotes($text) {
        $quotes = '"\'`';
        $positions = [];
        $offset = 0;

        while ($offset < strlen($text)) {
            $remaining = substr($text, $offset);
            $result = strpbrk($remaining, $quotes);

            if ($result === false) {
                break;
            }

            $position = $offset + (strlen($remaining) - strlen($result));

            $positions[] = [
                'quote' => $result[0],
                'position' => $position
            ];

            $offset = $position + 1;
        }

        return $positions;
    }
}

// 使用例
$text = "Hello world! How are you? I'm fine. Thank you.";

echo "=== 文の終わり検出 ===\n";
$end = TextAnalyzer::findSentenceEnd($text);
print_r($end);

echo "\n=== 文に分割 ===\n";
$sentences = TextAnalyzer::splitIntoSentences($text);
foreach ($sentences as $i => $sentence) {
    echo ($i + 1) . ". {$sentence}\n";
}

$quotedText = 'He said "Hello" and she replied \'Hi\' back.';
echo "\n=== 引用符検出 ===\n";
$quotes = TextAnalyzer::findQuotes($quotedText);
print_r($quotes);

strcspn()との違い

$text = "Hello, World!";

// strpbrk(): 指定文字が見つかった位置から最後まで
$result1 = strpbrk($text, ",!");
echo $result1 . "\n";  // , World!

// strcspn(): 指定文字が見つかるまでの長さ
$result2 = strcspn($text, ",!");
echo $result2 . "\n";  // 5("Hello"の長さ)

// 組み合わせて使用
$length = strcspn($text, ",!");
$substring = substr($text, 0, $length);
echo $substring . "\n";  // Hello

パフォーマンスの考慮

// 大量のテキストで区切り文字を検索
$text = str_repeat("word1 word2,word3;word4\t", 1000);

// strpbrk()を使用
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
    strpbrk($text, " ,;\t");
}
$time1 = microtime(true) - $start;

// strpos()を複数回使用
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
    $pos1 = strpos($text, ' ');
    $pos2 = strpos($text, ',');
    $pos3 = strpos($text, ';');
    $pos4 = strpos($text, "\t");
    $minPos = min(
        $pos1 !== false ? $pos1 : PHP_INT_MAX,
        $pos2 !== false ? $pos2 : PHP_INT_MAX,
        $pos3 !== false ? $pos3 : PHP_INT_MAX,
        $pos4 !== false ? $pos4 : PHP_INT_MAX
    );
}
$time2 = microtime(true) - $start;

echo "strpbrk(): {$time1}秒\n";
echo "複数のstrpos(): {$time2}秒\n";

// strpbrk()の方が効率的

まとめ

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

できること:

  • 複数の文字を一度に検索
  • 最初に見つかった文字から最後までを取得
  • 区切り文字の検出

他の関数との違い:

  • strstr(): 特定の文字列を検索
  • strpbrk(): 文字集合のいずれかを検索
  • strcspn(): 指定文字が見つかるまでの長さを返す
  • strpos(): 位置(整数)を返す

推奨される使用場面:

  • 複数の区切り文字を扱う
  • CSVやTSVの解析
  • パス区切り文字の検出
  • 無効な文字の検出
  • トークナイザーの実装
  • 文の終わりの検出

利点:

  • 複数の文字を一度に検索できる
  • 効率的(複数回のstrpos()より高速)
  • バイナリセーフ

注意点:

  • 最初に見つかった文字のみ
  • 見つからない場合はfalseを返す
  • 大文字小文字を区別する

関連関数:

  • strcspn(): 指定文字が現れるまでの長さ
  • strspn(): 指定文字に含まれる部分の長さ
  • strstr(): 部分文字列の検索
  • strtok(): トークン分割

strpbrk()は、複数の区切り文字や特定の文字集合を扱う場合に非常に便利です。効率的に文字列を解析できるので、パーサーやトークナイザーの実装に積極的に活用しましょう!

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