[PHP]substr_count関数を完全解説!部分文字列の出現回数をカウントする方法

PHP

こんにちは!今回は、PHPの標準関数であるsubstr_count()について詳しく解説していきます。文字列内で特定の部分文字列が何回出現するかを高速にカウントできる便利な関数です!

substr_count関数とは?

substr_count()関数は、文字列内で指定した部分文字列が出現する回数をカウントする関数です。

重複しない出現をカウントするため、検索文字列が重なり合う場合は1回としてカウントされます。テキスト解析、データ検証、統計処理などで活躍します!

基本的な構文

substr_count(
    string $haystack,
    string $needle,
    int $offset = 0,
    ?int $length = null
): int
  • $haystack: 検索対象の文字列
  • $needle: カウントする部分文字列
  • $offset: 検索開始位置(オプション)
  • $length: 検索する長さ(オプション)
  • 戻り値: 出現回数(整数)

基本的な使用例

シンプルなカウント

$text = "apple banana apple orange apple";

// "apple"の出現回数
$count = substr_count($text, "apple");
echo $count . "\n";
// 出力: 3

// "banana"の出現回数
$count = substr_count($text, "banana");
echo $count . "\n";
// 出力: 1

// 存在しない文字列
$count = substr_count($text, "grape");
echo $count . "\n";
// 出力: 0

1文字のカウント

$text = "Hello World";

// "l"の出現回数
$count = substr_count($text, "l");
echo $count . "\n";
// 出力: 3

// "o"の出現回数
$count = substr_count($text, "o");
echo $count . "\n";
// 出力: 2

重複する部分文字列

$text = "aaaa";

// 重複しない"aa"の出現回数
$count = substr_count($text, "aa");
echo $count . "\n";
// 出力: 2("aa|aa"として数える)

$text = "ababa";
$count = substr_count($text, "aba");
echo $count . "\n";
// 出力: 1(重複しない)

オフセットと長さの使用

$text = "apple banana apple orange apple";

// 位置10から検索
$count = substr_count($text, "apple", 10);
echo $count . "\n";
// 出力: 2

// 位置0から20文字分で検索
$count = substr_count($text, "apple", 0, 20);
echo $count . "\n";
// 出力: 2

// 位置10から15文字分で検索
$count = substr_count($text, "apple", 10, 15);
echo $count . "\n";
// 出力: 1

大文字小文字の区別

$text = "Apple apple APPLE";

// 大文字小文字を区別する
$count = substr_count($text, "apple");
echo $count . "\n";
// 出力: 1

// 大文字小文字を区別しない場合は変換が必要
$count = substr_count(strtolower($text), strtolower("apple"));
echo $count . "\n";
// 出力: 3

実践的な使用例

例1: テキスト分析

class TextAnalyzer {
    /**
     * 単語の出現回数をカウント
     */
    public static function wordCount($text, $word) {
        // 単語境界を考慮
        $pattern = '/\b' . preg_quote($word, '/') . '\b/i';
        return preg_match_all($pattern, $text);
    }
    
    /**
     * 文字の出現頻度
     */
    public static function characterFrequency($text) {
        $frequency = [];
        $length = strlen($text);
        
        for ($i = 0; $i < $length; $i++) {
            $char = $text[$i];
            
            if (!isset($frequency[$char])) {
                $frequency[$char] = substr_count($text, $char);
            }
        }
        
        arsort($frequency);
        return $frequency;
    }
    
    /**
     * 句読点をカウント
     */
    public static function countPunctuation($text) {
        $punctuations = ['.', ',', '!', '?', ';', ':'];
        $total = 0;
        
        foreach ($punctuations as $punct) {
            $total += substr_count($text, $punct);
        }
        
        return $total;
    }
    
    /**
     * 改行をカウント
     */
    public static function countLines($text) {
        return substr_count($text, "\n") + 1;
    }
    
    /**
     * 空白をカウント
     */
    public static function countSpaces($text) {
        return substr_count($text, ' ');
    }
    
    /**
     * 特定のパターンの出現回数
     */
    public static function countPattern($text, $pattern) {
        return substr_count($text, $pattern);
    }
    
    /**
     * テキスト統計を取得
     */
    public static function getStatistics($text) {
        return [
            'length' => strlen($text),
            'lines' => self::countLines($text),
            'spaces' => self::countSpaces($text),
            'punctuation' => self::countPunctuation($text),
            'words' => str_word_count($text)
        ];
    }
}

// 使用例
$text = "Hello World! This is a test. Hello again, World!";

echo "=== テキスト分析 ===\n";
echo "'Hello'の出現回数: " . TextAnalyzer::wordCount($text, "Hello") . "\n";
echo "句読点の数: " . TextAnalyzer::countPunctuation($text) . "\n";
echo "スペースの数: " . TextAnalyzer::countSpaces($text) . "\n";

echo "\n=== 文字頻度 ===\n";
$frequency = TextAnalyzer::characterFrequency($text);
foreach (array_slice($frequency, 0, 5) as $char => $count) {
    echo "'{$char}': {$count}\n";
}

echo "\n=== テキスト統計 ===\n";
$stats = TextAnalyzer::getStatistics($text);
print_r($stats);

例2: データ検証

class DataValidator {
    /**
     * パスワード強度をチェック
     */
    public static function checkPasswordStrength($password) {
        $strength = [
            'length' => strlen($password),
            'uppercase' => 0,
            'lowercase' => 0,
            'numbers' => 0,
            'special' => 0,
            'repeated' => self::hasRepeatedChars($password)
        ];
        
        // 大文字のカウント
        for ($i = 0; $i < 26; $i++) {
            $char = chr(65 + $i); // A-Z
            if (substr_count($password, $char) > 0) {
                $strength['uppercase']++;
            }
        }
        
        // 小文字のカウント
        for ($i = 0; $i < 26; $i++) {
            $char = chr(97 + $i); // a-z
            if (substr_count($password, $char) > 0) {
                $strength['lowercase']++;
            }
        }
        
        // 数字のカウント
        for ($i = 0; $i < 10; $i++) {
            if (substr_count($password, (string)$i) > 0) {
                $strength['numbers']++;
            }
        }
        
        // 特殊文字のカウント
        $specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?';
        for ($i = 0; $i < strlen($specialChars); $i++) {
            if (substr_count($password, $specialChars[$i]) > 0) {
                $strength['special']++;
            }
        }
        
        return $strength;
    }
    
    /**
     * 繰り返し文字をチェック
     */
    private static function hasRepeatedChars($text) {
        $maxRepeat = 0;
        $length = strlen($text);
        
        for ($i = 0; $i < $length - 1; $i++) {
            $char = $text[$i];
            $nextChar = $text[$i + 1];
            
            if ($char === $nextChar) {
                $repeat = 1;
                for ($j = $i + 1; $j < $length && $text[$j] === $char; $j++) {
                    $repeat++;
                }
                $maxRepeat = max($maxRepeat, $repeat);
            }
        }
        
        return $maxRepeat;
    }
    
    /**
     * 括弧のバランスをチェック
     */
    public static function checkBracketBalance($text) {
        $brackets = [
            '(' => ')',
            '[' => ']',
            '{' => '}'
        ];
        
        $balanced = [];
        
        foreach ($brackets as $open => $close) {
            $openCount = substr_count($text, $open);
            $closeCount = substr_count($text, $close);
            
            $balanced[$open . $close] = [
                'open' => $openCount,
                'close' => $closeCount,
                'balanced' => $openCount === $closeCount
            ];
        }
        
        return $balanced;
    }
    
    /**
     * SQLインジェクションのリスクをチェック
     */
    public static function checkSqlInjectionRisk($input) {
        $dangerousPatterns = [
            "'" => substr_count($input, "'"),
            '"' => substr_count($input, '"'),
            '--' => substr_count($input, '--'),
            ';' => substr_count($input, ';'),
            'OR' => substr_count(strtoupper($input), 'OR'),
            'AND' => substr_count(strtoupper($input), 'AND')
        ];
        
        $totalRisk = array_sum($dangerousPatterns);
        
        return [
            'risk_level' => $totalRisk > 3 ? 'high' : ($totalRisk > 0 ? 'medium' : 'low'),
            'patterns' => $dangerousPatterns,
            'total' => $totalRisk
        ];
    }
}

// 使用例
echo "=== パスワード強度 ===\n";
$passwords = ['password', 'Pass123!', 'MyP@ssw0rd', 'aaa111'];
foreach ($passwords as $password) {
    echo "\nパスワード: {$password}\n";
    $strength = DataValidator::checkPasswordStrength($password);
    print_r($strength);
}

echo "\n=== 括弧バランス ===\n";
$expressions = ['(a + b) * [c - d]', '((a + b)', '{a, b, c}'];
foreach ($expressions as $expr) {
    echo "\n{$expr}:\n";
    $balance = DataValidator::checkBracketBalance($expr);
    foreach ($balance as $bracket => $info) {
        $status = $info['balanced'] ? 'OK' : 'NG';
        echo "  {$bracket}: {$status} (開:{$info['open']}, 閉:{$info['close']})\n";
    }
}

echo "\n=== SQLインジェクションリスク ===\n";
$inputs = [
    "John Doe",
    "admin' OR '1'='1",
    "user@example.com"
];
foreach ($inputs as $input) {
    echo "\n{$input}:\n";
    $risk = DataValidator::checkSqlInjectionRisk($input);
    echo "  リスクレベル: {$risk['risk_level']}\n";
    echo "  検出パターン: {$risk['total']}\n";
}

例3: ログ解析

class LogAnalyzer {
    /**
     * ログレベル別の件数をカウント
     */
    public static function countByLevel($logContent) {
        $levels = ['ERROR', 'WARN', 'INFO', 'DEBUG'];
        $counts = [];
        
        foreach ($levels as $level) {
            $counts[$level] = substr_count($logContent, "[{$level}]");
        }
        
        return $counts;
    }
    
    /**
     * 特定のIPアドレスの出現回数
     */
    public static function countIpOccurrences($logContent, $ip) {
        return substr_count($logContent, $ip);
    }
    
    /**
     * HTTPステータスコード別の件数
     */
    public static function countHttpStatus($logContent) {
        $statusCodes = ['200', '404', '500', '301', '302', '403'];
        $counts = [];
        
        foreach ($statusCodes as $code) {
            $counts[$code] = substr_count($logContent, " {$code} ");
        }
        
        return $counts;
    }
    
    /**
     * エラーメッセージの出現回数
     */
    public static function countErrorMessages($logContent, $errorMessages) {
        $counts = [];
        
        foreach ($errorMessages as $message) {
            $counts[$message] = substr_count($logContent, $message);
        }
        
        arsort($counts);
        return $counts;
    }
    
    /**
     * ユーザーアクションのカウント
     */
    public static function countUserActions($logContent) {
        $actions = ['login', 'logout', 'upload', 'download', 'delete'];
        $counts = [];
        
        foreach ($actions as $action) {
            $counts[$action] = substr_count(strtolower($logContent), $action);
        }
        
        return $counts;
    }
}

// 使用例
$logContent = <<<LOG
[2024-02-23 10:00:00] [INFO] User login: 192.168.1.1
[2024-02-23 10:05:00] [ERROR] Database connection failed
[2024-02-23 10:10:00] [WARN] High memory usage
[2024-02-23 10:15:00] [INFO] User logout: 192.168.1.1
[2024-02-23 10:20:00] [ERROR] Database connection failed
200 OK
404 Not Found
500 Internal Server Error
LOG;

echo "=== ログレベル別件数 ===\n";
$levelCounts = LogAnalyzer::countByLevel($logContent);
print_r($levelCounts);

echo "\n=== HTTPステータス件数 ===\n";
$statusCounts = LogAnalyzer::countHttpStatus($logContent);
print_r($statusCounts);

echo "\n=== IP出現回数 ===\n";
echo "192.168.1.1: " . LogAnalyzer::countIpOccurrences($logContent, '192.168.1.1') . "\n";

echo "\n=== エラーメッセージ ===\n";
$errorMessages = ['Database connection failed', 'High memory usage'];
$errorCounts = LogAnalyzer::countErrorMessages($logContent, $errorMessages);
print_r($errorCounts);

例4: HTML/XMLパース

class HtmlAnalyzer {
    /**
     * 特定のHTMLタグの数をカウント
     */
    public static function countTags($html, $tag) {
        $openTag = "<{$tag}";
        $closeTag = "</{$tag}>";
        
        return [
            'open' => substr_count($html, $openTag),
            'close' => substr_count($html, $closeTag)
        ];
    }
    
    /**
     * すべてのタグをカウント
     */
    public static function countAllTags($html) {
        $tags = ['div', 'p', 'span', 'a', 'img', 'table', 'tr', 'td'];
        $counts = [];
        
        foreach ($tags as $tag) {
            $tagCounts = self::countTags($html, $tag);
            $counts[$tag] = $tagCounts['open'];
        }
        
        arsort($counts);
        return $counts;
    }
    
    /**
     * 画像の数をカウント
     */
    public static function countImages($html) {
        return substr_count($html, '<img');
    }
    
    /**
     * リンクの数をカウント
     */
    public static function countLinks($html) {
        return substr_count($html, '<a ');
    }
    
    /**
     * CSSクラスの使用回数
     */
    public static function countClass($html, $className) {
        return substr_count($html, "class=\"{$className}\"") +
               substr_count($html, "class='{$className}'");
    }
    
    /**
     * コメントの数をカウント
     */
    public static function countComments($html) {
        return substr_count($html, '<!--');
    }
}

// 使用例
$html = <<<HTML
<!DOCTYPE html>
<html>
<head>
    <title>Test Page</title>
</head>
<body>
    <div class="container">
        <p>Paragraph 1</p>
        <p>Paragraph 2</p>
        <a href="#">Link</a>
        <img src="image.jpg" alt="Image">
    </div>
    <!-- Comment -->
</body>
</html>
HTML;

echo "=== HTMLタグ分析 ===\n";
$allTags = HtmlAnalyzer::countAllTags($html);
foreach ($allTags as $tag => $count) {
    if ($count > 0) {
        echo "{$tag}: {$count}\n";
    }
}

echo "\n=== 要素カウント ===\n";
echo "画像: " . HtmlAnalyzer::countImages($html) . "\n";
echo "リンク: " . HtmlAnalyzer::countLinks($html) . "\n";
echo "コメント: " . HtmlAnalyzer::countComments($html) . "\n";

例5: コード分析

class CodeAnalyzer {
    /**
     * 関数呼び出しをカウント
     */
    public static function countFunctionCalls($code, $functionName) {
        return substr_count($code, $functionName . '(');
    }
    
    /**
     * 変数の使用回数
     */
    public static function countVariableUsage($code, $variableName) {
        // $を含める
        if ($variableName[0] !== '$') {
            $variableName = '$' . $variableName;
        }
        
        return substr_count($code, $variableName);
    }
    
    /**
     * インデントレベルを分析
     */
    public static function analyzeIndentation($code) {
        return [
            'spaces' => substr_count($code, '    '), // 4スペース
            'tabs' => substr_count($code, "\t")
        ];
    }
    
    /**
     * セミコロンの数(文の数の目安)
     */
    public static function countStatements($code) {
        return substr_count($code, ';');
    }
    
    /**
     * コメント行をカウント
     */
    public static function countComments($code) {
        return [
            'single_line' => substr_count($code, '//'),
            'multi_line_start' => substr_count($code, '/*'),
            'multi_line_end' => substr_count($code, '*/')
        ];
    }
    
    /**
     * 括弧のペアをカウント
     */
    public static function countBrackets($code) {
        return [
            'curly_open' => substr_count($code, '{'),
            'curly_close' => substr_count($code, '}'),
            'paren_open' => substr_count($code, '('),
            'paren_close' => substr_count($code, ')'),
            'square_open' => substr_count($code, '['),
            'square_close' => substr_count($code, ']')
        ];
    }
}

// 使用例
$code = <<<'CODE'
<?php
function calculateTotal($items) {
    $total = 0;
    foreach ($items as $item) {
        $total += $item['price'];
    }
    return $total;
}

$items = [
    ['price' => 100],
    ['price' => 200]
];

$total = calculateTotal($items);
echo $total;
CODE;

echo "=== コード分析 ===\n";
echo "calculateTotal呼び出し: " . CodeAnalyzer::countFunctionCalls($code, 'calculateTotal') . "\n";
echo "\$total使用回数: " . CodeAnalyzer::countVariableUsage($code, 'total') . "\n";
echo "文の数: " . CodeAnalyzer::countStatements($code) . "\n";

echo "\n=== インデント ===\n";
$indent = CodeAnalyzer::analyzeIndentation($code);
print_r($indent);

echo "\n=== 括弧 ===\n";
$brackets = CodeAnalyzer::countBrackets($code);
print_r($brackets);

例6: URL分析

class UrlAnalyzer {
    /**
     * URLのパラメータ数をカウント
     */
    public static function countParameters($url) {
        $queryStart = strpos($url, '?');
        
        if ($queryStart === false) {
            return 0;
        }
        
        $queryString = substr($url, $queryStart + 1);
        
        // フラグメントを除去
        $fragmentPos = strpos($queryString, '#');
        if ($fragmentPos !== false) {
            $queryString = substr($queryString, 0, $fragmentPos);
        }
        
        return substr_count($queryString, '&') + 1;
    }
    
    /**
     * パス階層の深さをカウント
     */
    public static function countPathDepth($url) {
        // プロトコルを除去
        $url = preg_replace('#^[a-z]+://#', '', $url);
        
        // クエリとフラグメントを除去
        $url = preg_replace('#[?#].*$#', '', $url);
        
        // 先頭のドメイン部分を除去
        $slashPos = strpos($url, '/');
        if ($slashPos !== false) {
            $path = substr($url, $slashPos);
            return substr_count($path, '/');
        }
        
        return 0;
    }
    
    /**
     * サブドメインの数をカウント
     */
    public static function countSubdomains($url) {
        // プロトコルを除去
        $url = preg_replace('#^[a-z]+://#', '', $url);
        
        // パス以降を除去
        $slashPos = strpos($url, '/');
        if ($slashPos !== false) {
            $url = substr($url, 0, $slashPos);
        }
        
        // ドットの数 - 1 = サブドメイン数
        $dotCount = substr_count($url, '.');
        
        return max(0, $dotCount - 1);
    }
}

// 使用例
$urls = [
    'https://example.com/path/to/page?id=1&name=test&sort=asc',
    'https://sub1.sub2.example.com/deep/path/structure/',
    'http://test.org?single=param'
];

echo "=== URL分析 ===\n";
foreach ($urls as $url) {
    echo "\n{$url}:\n";
    echo "  パラメータ数: " . UrlAnalyzer::countParameters($url) . "\n";
    echo "  パス深さ: " . UrlAnalyzer::countPathDepth($url) . "\n";
    echo "  サブドメイン数: " . UrlAnalyzer::countSubdomains($url) . "\n";
}

例7: 文字列パターン検出

class PatternDetector {
    /**
     * 繰り返しパターンを検出
     */
    public static function detectRepeatingPattern($text, $pattern) {
        $count = substr_count($text, $pattern);
        
        return [
            'pattern' => $pattern,
            'count' => $count,
            'percentage' => $count > 0 ? 
                (strlen($pattern) * $count / strlen($text)) * 100 : 0
        ];
    }
    
    /**
     * 最も頻出する2文字パターン
     */
    public static function findMostCommonBigram($text) {
        $bigrams = [];
        $length = strlen($text);
        
        for ($i = 0; $i < $length - 1; $i++) {
            $bigram = substr($text, $i, 2);
            
            if (!isset($bigrams[$bigram])) {
                $bigrams[$bigram] = substr_count($text, $bigram);
            }
        }
        
        arsort($bigrams);
        
        return array_slice($bigrams, 0, 5, true);
    }
    
    /**
     * 空白の連続をチェック
     */
    public static function checkMultipleSpaces($text) {
        return [
            'double' => substr_count($text, '  '),
            'triple' => substr_count($text, '   '),
            'quad' => substr_count($text, '    ')
        ];
    }
}

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

echo "=== パターン検出 ===\n";
$pattern = DetectorDetector::detectRepeatingPattern($text, "the");
print_r($pattern);

echo "\n=== 頻出2文字 ===\n";
$bigrams = PatternDetector::findMostCommonBigram($text);
foreach ($bigrams as $bigram => $count) {
    echo "'{$bigram}': {$count}\n";
}

大文字小文字を区別しないカウント

// 大文字小文字を区別しない場合
$text = "Apple apple APPLE";

// 小文字に統一してカウント
$count = substr_count(strtolower($text), strtolower("apple"));
echo $count . "\n";  // 3

// または専用の関数を作成
function substr_count_i($haystack, $needle) {
    return substr_count(strtolower($haystack), strtolower($needle));
}

echo substr_count_i($text, "APPLE") . "\n";  // 3

パフォーマンステスト

// 大量のテキストで出現回数をカウント
$text = str_repeat("apple banana orange ", 100000);
$needle = "apple";

// substr_count()
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
    substr_count($text, $needle);
}
$time1 = microtime(true) - $start;

// preg_match_all()
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
    preg_match_all('/' . preg_quote($needle, '/') . '/', $text);
}
$time2 = microtime(true) - $start;

// 手動カウント
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
    $count = 0;
    $pos = 0;
    while (($pos = strpos($text, $needle, $pos)) !== false) {
        $count++;
        $pos += strlen($needle);
    }
}
$time3 = microtime(true) - $start;

echo "substr_count(): {$time1}秒\n";
echo "preg_match_all(): {$time2}秒\n";
echo "手動カウント: {$time3}秒\n";

// substr_count()が最も高速

まとめ

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

できること:

  • 部分文字列の出現回数を高速カウント
  • 範囲を指定したカウント
  • 重複しない出現のカウント

推奨される使用場面:

  • テキスト分析・統計
  • ログ解析
  • データ検証
  • HTML/XMLパース
  • コード分析
  • パターン検出

利点:

  • 非常に高速(最適化済み)
  • シンプルで使いやすい
  • バイナリセーフ

注意点:

  • 大文字小文字を区別する
  • 重複するパターンは1回としてカウント
  • 空文字列を検索すると警告

重複のカウント方法:

$text = "aaaa";

// 重複しない"aa"の数
substr_count($text, "aa");  // 2

// 重複を含む"aa"の数
$count = 0;
$pos = 0;
while (($pos = strpos($text, "aa", $pos)) !== false) {
    $count++;
    $pos++;  // 1文字進める(重複を許可)
}
echo $count;  // 3

関連関数:

  • str_word_count(): 単語数をカウント
  • strpos(): 最初の出現位置を取得
  • preg_match_all(): 正規表現でマッチ数をカウント

よく使うパターン:

// 文字の出現回数
substr_count($text, 'a')

// 単語の出現回数(大文字小文字無視)
substr_count(strtolower($text), strtolower($word))

// 特定範囲内でのカウント
substr_count($text, $needle, $offset, $length)

// 改行のカウント
substr_count($text, "\n")

substr_count()は、文字列内のパターン出現回数を数える最も効率的な方法です。テキスト分析、ログ解析、データ検証など、様々な場面で活躍する関数なので、しっかり使いこなしましょう!

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