[PHP]strtok関数を完全解説!文字列をトークンに分割する方法

PHP

こんにちは!今回は、PHPの標準関数であるstrtok()について詳しく解説していきます。文字列を区切り文字で分割してトークンを順番に取得できる、独特な動作をする関数です!

strtok関数とは?

strtok()関数は、文字列を区切り文字で分割し、トークンを1つずつ順番に取得する関数です。

“string token”の略で、C言語のstrtok()関数と同様の動作をします。内部状態を保持するため、最初の呼び出しと2回目以降で使い方が異なるという特徴があります!

基本的な構文

// 最初の呼び出し
strtok(string $string, string $token): string|false

// 2回目以降の呼び出し
strtok(string $token): string|false
  • $string: 分割する文字列(最初の呼び出しのみ)
  • $token: 区切り文字の集合
  • 戻り値:
    • トークンが見つかった場合: トークン文字列
    • これ以上トークンがない場合: false

基本的な使用例

シンプルな使用例

$text = "apple,banana,orange";

// 最初のトークン
$token = strtok($text, ',');
echo $token . "\n";  // apple

// 2番目のトークン
$token = strtok(',');
echo $token . "\n";  // banana

// 3番目のトークン
$token = strtok(',');
echo $token . "\n";  // orange

// これ以上ない
$token = strtok(',');
var_dump($token);  // bool(false)

ループでの使用

$text = "apple banana orange grape";

// すべてのトークンを取得
$token = strtok($text, ' ');

while ($token !== false) {
    echo $token . "\n";
    $token = strtok(' ');
}
/*
出力:
apple
banana
orange
grape
*/

複数の区切り文字

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

// カンマ、セミコロン、コロンで分割
$token = strtok($text, ',;:');

while ($token !== false) {
    echo $token . "\n";
    $token = strtok(',;:');
}
/*
出力:
apple
banana
orange
grape
*/

連続する区切り文字

$text = "apple,,banana,,,orange";

// 連続する区切り文字は無視される
$token = strtok($text, ',');

while ($token !== false) {
    echo $token . "\n";
    $token = strtok(',');
}
/*
出力:
apple
banana
orange
*/

実践的な使用例

例1: CSVパーサー

class CsvParser {
    /**
     * CSV行を解析
     */
    public static function parseLine($line, $delimiter = ',') {
        $fields = [];
        
        $token = strtok($line, $delimiter);
        
        while ($token !== false) {
            $fields[] = trim($token);
            $token = strtok($delimiter);
        }
        
        return $fields;
    }
    
    /**
     * TSV行を解析
     */
    public static function parseTsv($line) {
        return self::parseLine($line, "\t");
    }
    
    /**
     * 複数の区切り文字で解析
     */
    public static function parseMultiDelimiter($line, $delimiters) {
        $fields = [];
        
        $token = strtok($line, $delimiters);
        
        while ($token !== false) {
            $fields[] = trim($token);
            $token = strtok($delimiters);
        }
        
        return $fields;
    }
    
    /**
     * CSVファイル全体を解析
     */
    public static function parseFile($filename, $delimiter = ',') {
        if (!file_exists($filename)) {
            return [];
        }
        
        $lines = file($filename, FILE_IGNORE_NEW_LINES);
        $data = [];
        
        foreach ($lines as $line) {
            if (empty(trim($line))) {
                continue;
            }
            
            $data[] = self::parseLine($line, $delimiter);
        }
        
        return $data;
    }
}

// 使用例
$csvLines = [
    'apple,banana,orange',
    'red,yellow,orange',
    '100,150,80'
];

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

$tsvLine = "name\tage\tcity";
echo "\n=== TSV解析 ===\n";
print_r(CsvParser::parseTsv($tsvLine));

$mixedLine = "apple,banana;orange:grape";
echo "\n=== 複数区切り文字 ===\n";
print_r(CsvParser::parseMultiDelimiter($mixedLine, ',;:'));

例2: パス分割

class PathTokenizer {
    /**
     * パスをセグメントに分割
     */
    public static function splitPath($path) {
        $segments = [];
        
        // Unix/Windowsパスの両方に対応
        $token = strtok($path, '/\\');
        
        while ($token !== false) {
            $segments[] = $token;
            $token = strtok('/\\');
        }
        
        return $segments;
    }
    
    /**
     * ディレクトリ階層を取得
     */
    public static function getHierarchy($path) {
        $segments = self::splitPath($path);
        $hierarchy = [];
        $current = '';
        
        foreach ($segments as $segment) {
            $current .= $segment . '/';
            $hierarchy[] = rtrim($current, '/');
        }
        
        return $hierarchy;
    }
    
    /**
     * パスの深さを取得
     */
    public static function getDepth($path) {
        return count(self::splitPath($path));
    }
    
    /**
     * 共通のパスプレフィックスを取得
     */
    public static function getCommonPrefix($path1, $path2) {
        $segments1 = self::splitPath($path1);
        $segments2 = self::splitPath($path2);
        
        $common = [];
        $minLength = min(count($segments1), count($segments2));
        
        for ($i = 0; $i < $minLength; $i++) {
            if ($segments1[$i] === $segments2[$i]) {
                $common[] = $segments1[$i];
            } else {
                break;
            }
        }
        
        return implode('/', $common);
    }
}

// 使用例
$paths = [
    '/var/www/html/index.php',
    'C:\\Users\\Documents\\file.txt',
    'relative/path/to/file.pdf'
];

echo "=== パス分割 ===\n";
foreach ($paths as $path) {
    echo "\n{$path}:\n";
    $segments = PathTokenizer::splitPath($path);
    print_r($segments);
    echo "  深さ: " . PathTokenizer::getDepth($path) . "\n";
}

$path = '/var/www/html/images/photo.jpg';
echo "\n=== 階層構造 ===\n";
print_r(PathTokenizer::getHierarchy($path));

$path1 = '/var/www/html/images/photo.jpg';
$path2 = '/var/www/html/css/style.css';
echo "\n=== 共通プレフィックス ===\n";
echo PathTokenizer::getCommonPrefix($path1, $path2) . "\n";
// /var/www/html

例3: テキスト処理

class TextTokenizer {
    /**
     * 文を単語に分割
     */
    public static function tokenizeWords($text) {
        $words = [];
        
        // 空白、タブ、改行、句読点で分割
        $token = strtok($text, " \t\n\r.,!?;:");
        
        while ($token !== false) {
            $words[] = $token;
            $token = strtok(" \t\n\r.,!?;:");
        }
        
        return $words;
    }
    
    /**
     * 単語数をカウント
     */
    public static function countWords($text) {
        return count(self::tokenizeWords($text));
    }
    
    /**
     * 単語の頻度を計算
     */
    public static function wordFrequency($text) {
        $words = self::tokenizeWords($text);
        $frequency = [];
        
        foreach ($words as $word) {
            $word = strtolower($word);
            
            if (!isset($frequency[$word])) {
                $frequency[$word] = 0;
            }
            
            $frequency[$word]++;
        }
        
        arsort($frequency);
        
        return $frequency;
    }
    
    /**
     * 数値トークンを抽出
     */
    public static function extractNumbers($text) {
        $numbers = [];
        
        $token = strtok($text, " \t\n\r,;:!?");
        
        while ($token !== false) {
            if (is_numeric($token)) {
                $numbers[] = $token;
            }
            
            $token = strtok(" \t\n\r,;:!?");
        }
        
        return $numbers;
    }
}

// 使用例
$text = "The quick brown fox jumps over the lazy dog. The dog was very lazy!";

echo "=== テキスト解析 ===\n";
$words = TextTokenizer::tokenizeWords($text);
echo "単語:\n";
print_r($words);

echo "\n単語数: " . TextTokenizer::countWords($text) . "\n";

echo "\n単語頻度:\n";
$frequency = TextTokenizer::wordFrequency($text);
foreach ($frequency as $word => $count) {
    echo "  {$word}: {$count}\n";
}

$numText = "The price is 100 dollars, quantity 50, total 5000.";
echo "\n=== 数値抽出 ===\n";
print_r(TextTokenizer::extractNumbers($numText));

例4: ログ解析

class LogTokenizer {
    /**
     * Apache形式のログを解析
     */
    public static function parseApacheLog($logLine) {
        // 127.0.0.1 - - [01/Jan/2024:10:30:00 +0000] "GET /index.html HTTP/1.1" 200 1234
        
        $parts = [];
        
        // IPアドレス
        $token = strtok($logLine, ' ');
        $parts['ip'] = $token;
        
        // ハイフン2つをスキップ
        strtok(' ');
        strtok(' ');
        
        // タイムスタンプ([と]を含む)
        $timestamp = strtok('[');
        $timestamp = strtok(']');
        $parts['timestamp'] = $timestamp;
        
        // リクエスト("と"で囲まれている)
        $request = strtok('"');
        $request = strtok('"');
        $parts['request'] = $request;
        
        // ステータスコード
        $status = strtok(' ');
        $parts['status'] = $status;
        
        // バイト数
        $bytes = strtok(' ');
        $parts['bytes'] = $bytes;
        
        return $parts;
    }
    
    /**
     * カスタムログフォーマットを解析
     */
    public static function parseCustomLog($logLine, $delimiter = '|') {
        $fields = [];
        
        $token = strtok($logLine, $delimiter);
        
        while ($token !== false) {
            $fields[] = trim($token);
            $token = strtok($delimiter);
        }
        
        return $fields;
    }
    
    /**
     * キー・値形式のログを解析
     */
    public static function parseKeyValueLog($logLine) {
        $data = [];
        
        // スペースで分割
        $token = strtok($logLine, ' ');
        
        while ($token !== false) {
            // key=value 形式
            if (strpos($token, '=') !== false) {
                list($key, $value) = explode('=', $token, 2);
                $data[trim($key)] = trim($value, '"\'');
            }
            
            $token = strtok(' ');
        }
        
        return $data;
    }
}

// 使用例
$apacheLog = '127.0.0.1 - - [23/Feb/2024:10:30:00 +0000] "GET /index.html HTTP/1.1" 200 1234';

echo "=== Apacheログ解析 ===\n";
$parsed = LogTokenizer::parseApacheLog($apacheLog);
print_r($parsed);

$customLog = '2024-02-23|INFO|User logged in|user123';
echo "\n=== カスタムログ ===\n";
print_r(LogTokenizer::parseCustomLog($customLog));

$kvLog = 'timestamp="2024-02-23 10:30:00" level=ERROR message="Connection failed" user=admin';
echo "\n=== キー・値ログ ===\n";
print_r(LogTokenizer::parseKeyValueLog($kvLog));

例5: コマンドライン引数の解析

class CommandLineParser {
    /**
     * コマンドライン文字列を解析
     */
    public static function parse($commandLine) {
        $args = [];
        
        $token = strtok($commandLine, ' ');
        
        while ($token !== false) {
            $args[] = $token;
            $token = strtok(' ');
        }
        
        return $args;
    }
    
    /**
     * オプションとパラメータを分離
     */
    public static function parseOptions($commandLine) {
        $options = [];
        $params = [];
        
        $token = strtok($commandLine, ' ');
        
        while ($token !== false) {
            if (strpos($token, '--') === 0) {
                // 長いオプション
                $option = substr($token, 2);
                
                if (strpos($option, '=') !== false) {
                    list($key, $value) = explode('=', $option, 2);
                    $options[$key] = $value;
                } else {
                    $options[$option] = true;
                }
            } elseif (strpos($token, '-') === 0) {
                // 短いオプション
                $option = substr($token, 1);
                $options[$option] = true;
            } else {
                // パラメータ
                $params[] = $token;
            }
            
            $token = strtok(' ');
        }
        
        return [
            'options' => $options,
            'params' => $params
        ];
    }
    
    /**
     * クォートされた引数を処理
     */
    public static function parseQuoted($commandLine) {
        $args = [];
        $inQuote = false;
        $currentArg = '';
        
        $token = strtok($commandLine, ' "');
        
        while ($token !== false) {
            if ($inQuote) {
                $currentArg .= ' ' . $token;
            } else {
                if (!empty($currentArg)) {
                    $args[] = $currentArg;
                }
                $currentArg = $token;
            }
            
            $token = strtok(' "');
        }
        
        if (!empty($currentArg)) {
            $args[] = $currentArg;
        }
        
        return $args;
    }
}

// 使用例
$cmd = "git commit -m 'Initial commit' --author=John file1.txt file2.txt";

echo "=== コマンドライン解析 ===\n";
$args = CommandLineParser::parse($cmd);
print_r($args);

echo "\n=== オプションとパラメータ ===\n";
$parsed = CommandLineParser::parseOptions($cmd);
print_r($parsed);

例6: データフォーマットの解析

class DataFormatParser {
    /**
     * IPアドレスを解析
     */
    public static function parseIpAddress($ip) {
        $octets = [];
        
        $token = strtok($ip, '.');
        
        while ($token !== false) {
            $octets[] = (int)$token;
            $token = strtok('.');
        }
        
        return $octets;
    }
    
    /**
     * MACアドレスを解析
     */
    public static function parseMacAddress($mac) {
        $bytes = [];
        
        $token = strtok($mac, ':-');
        
        while ($token !== false) {
            $bytes[] = $token;
            $token = strtok(':-');
        }
        
        return $bytes;
    }
    
    /**
     * 日付を解析
     */
    public static function parseDate($date, $delimiter = '-') {
        $parts = [];
        
        $token = strtok($date, $delimiter . '/ ');
        
        while ($token !== false) {
            $parts[] = $token;
            $token = strtok($delimiter . '/ ');
        }
        
        if (count($parts) === 3) {
            return [
                'year' => $parts[0],
                'month' => $parts[1],
                'day' => $parts[2]
            ];
        }
        
        return null;
    }
    
    /**
     * バージョン番号を解析
     */
    public static function parseVersion($version) {
        $parts = [];
        
        $token = strtok($version, '.');
        
        while ($token !== false) {
            // ビルド情報を除外
            $token = strtok($token, '-+');
            $parts[] = (int)$token;
            $token = strtok('.');
        }
        
        return [
            'major' => $parts[0] ?? 0,
            'minor' => $parts[1] ?? 0,
            'patch' => $parts[2] ?? 0
        ];
    }
}

// 使用例
echo "=== IPアドレス解析 ===\n";
$ip = '192.168.1.1';
print_r(DataFormatParser::parseIpAddress($ip));

echo "\n=== MACアドレス解析 ===\n";
$mac1 = 'AA:BB:CC:DD:EE:FF';
$mac2 = 'AA-BB-CC-DD-EE-FF';
print_r(DataFormatParser::parseMacAddress($mac1));
print_r(DataFormatParser::parseMacAddress($mac2));

echo "\n=== 日付解析 ===\n";
$dates = ['2024-02-23', '2024/02/23', '2024 02 23'];
foreach ($dates as $date) {
    print_r(DataFormatParser::parseDate($date));
}

echo "\n=== バージョン解析 ===\n";
$versions = ['1.2.3', '2.10.5-alpha', '3.0.0+build.123'];
foreach ($versions as $version) {
    echo "{$version}: ";
    print_r(DataFormatParser::parseVersion($version));
}

例7: 設定ファイルの解析

class ConfigParser {
    /**
     * INI形式の行を解析
     */
    public static function parseIniLine($line) {
        $line = trim($line);
        
        // コメント行
        if (empty($line) || $line[0] === ';' || $line[0] === '#') {
            return null;
        }
        
        // セクション
        if ($line[0] === '[') {
            $section = trim($line, '[]');
            return ['type' => 'section', 'name' => $section];
        }
        
        // キー・値
        $token = strtok($line, '=');
        $key = trim($token);
        
        $value = strtok('');
        $value = trim($value);
        
        return ['type' => 'keyvalue', 'key' => $key, 'value' => $value];
    }
    
    /**
     * 環境変数形式を解析
     */
    public static function parseEnvLine($line) {
        $line = trim($line);
        
        if (empty($line) || $line[0] === '#') {
            return null;
        }
        
        $token = strtok($line, '=');
        $key = trim($token);
        
        $value = strtok('');
        $value = trim($value, '"\'');
        
        return [$key => $value];
    }
    
    /**
     * 複数行の設定を解析
     */
    public static function parseConfig($configText) {
        $config = [];
        $currentSection = 'default';
        
        $line = strtok($configText, "\n");
        
        while ($line !== false) {
            $parsed = self::parseIniLine($line);
            
            if ($parsed !== null) {
                if ($parsed['type'] === 'section') {
                    $currentSection = $parsed['name'];
                } elseif ($parsed['type'] === 'keyvalue') {
                    $config[$currentSection][$parsed['key']] = $parsed['value'];
                }
            }
            
            $line = strtok("\n");
        }
        
        return $config;
    }
}

// 使用例
$iniContent = <<<INI
; Database configuration

[database]

host=localhost port=3306 name=mydb

[cache]

enabled=true ttl=3600 INI; echo “=== 設定ファイル解析 ===\n”; $config = ConfigParser::parseConfig($iniContent); print_r($config); $envLine = ‘DATABASE_URL=”mysql://user:pass@localhost/db”‘; echo “\n=== 環境変数解析 ===\n”; print_r(ConfigParser::parseEnvLine($envLine));

explode()との違い

$text = "apple,,banana,,,orange";

// explode(): 空の要素も含む
$exploded = explode(',', $text);
print_r($exploded);
/*
Array (
    [0] => apple
    [1] =>
    [2] => banana
    [3] =>
    [4] =>
    [5] => orange
)
*/

// strtok(): 連続する区切り文字は無視
$tokens = [];
$token = strtok($text, ',');
while ($token !== false) {
    $tokens[] = $token;
    $token = strtok(',');
}
print_r($tokens);
/*
Array (
    [0] => apple
    [1] => banana
    [2] => orange
)
*/

注意点と制限事項

// 注意1: 内部状態を保持
$text1 = "apple,banana";
$text2 = "orange,grape";

$token1 = strtok($text1, ',');
echo $token1 . "\n";  // apple

// 新しい文字列を開始すると前の処理は中断される
$token2 = strtok($text2, ',');
echo $token2 . "\n";  // orange

// 注意2: 空のトークンは返されない
$text = "a,,b";
$token = strtok($text, ',');
while ($token !== false) {
    echo $token . "\n";
    $token = strtok(',');
}
// 出力: a, b(間の空は無視される)

// 注意3: 元の文字列は変更されない
$original = "apple,banana,orange";
$token = strtok($original, ',');
echo $original . "\n";  // apple,banana,orange(変更なし)

まとめ

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

できること:

  • 文字列を区切り文字で分割
  • トークンを順番に取得
  • 複数の区切り文字を指定可能
  • 連続する区切り文字を自動的に無視

使い方の特徴:

  • 最初の呼び出しでは文字列と区切り文字を指定
  • 2回目以降は区切り文字のみ指定
  • 内部状態を保持

推奨される使用場面:

  • CSVやTSVの解析
  • パスの分割
  • テキストのトークン化
  • ログファイルの解析
  • コマンドライン引数の処理

利点:

  • メモリ効率が良い(一度に全体を分割しない)
  • 連続する区切り文字を自動処理
  • 複数の区切り文字を一度に指定可能

注意点:

  • 内部状態を保持するため、並列処理には不向き
  • 空のトークンは返されない
  • 新しい文字列を開始すると前の処理が中断される

explode()との違い:

  • explode(): 一度に全て分割、空要素も含む
  • strtok(): 順番に取得、空要素は無視

関連関数:

  • explode(): 文字列を配列に分割
  • str_split(): 文字列を文字単位で分割
  • preg_split(): 正規表現で分割

strtok()は、大きなファイルや長い文字列を効率的に処理したい場合に便利です。内部状態を保持する独特な動作を理解して、適切な場面で活用しましょう!

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