【2025年最新】PHPで文字列に特定の文字列が含まれているか確認する7つの方法と実践例

目次

目次へ

はじめに

PHPによるWeb開発において、文字列操作は最も頻繁に行われる処理の一つです。特に、ある文字列が別の文字列に含まれているかどうかを確認する処理は、フォーム入力の検証、データベースクエリの構築、テキスト分析など、あらゆる場面で必要とされます。

2025年の現在、PHPは8.4までバージョンアップし、より簡潔で効率的な文字列操作メソッドが導入されています。一方で、後方互換性のために従来のメソッドも引き続きサポートされており、プロジェクトの要件や実行環境によって、最適な方法を選択することが重要です。

本記事では、PHPで文字列に特定の文字列が含まれているかを確認するための7つの方法を詳しく解説します。それぞれの手法の特徴、使い方、パフォーマンス特性、そして適切なユースケースについて、具体的なコード例とともに紹介します。

この記事で紹介する7つの方法は以下の通りです:

  1. strpos() – 最も基本的で広く使われている文字列検索関数
  2. str_contains() – PHP 8.0で導入された、より直感的な文字列包含チェック関数
  3. stripos() – 大文字小文字を区別せずに検索する関数
  4. strstr() – 部分文字列を検索して返す関数
  5. 正規表現 – 複雑なパターンマッチングを可能にする強力なツール
  6. mb_strpos() – マルチバイト文字(日本語など)に対応した検索関数
  7. substr_count() – 特定の部分文字列の出現回数をカウントする関数

これらの方法はそれぞれ長所と短所があり、状況に応じて最適な選択が変わります。たとえば、シンプルな文字列チェックならstr_contains()がコードの可読性を高めますが、複雑なパターンマッチングが必要な場合は正規表現が適しています。また、日本語などのマルチバイト文字を扱う場合はmb_strpos()が必須となるでしょう。

2025年時点では、PHP 8.xシリーズの採用が進み、新しいプロジェクトではstr_contains()のような直感的なAPIを利用できる環境が整ってきています。一方で、レガシーシステムの保守や互換性の観点から、従来の関数の知識も依然として重要です。

本記事を通じて、あなたのプロジェクトに最適な文字列検索手法を見つけ、効率的で保守性の高いコードを書くための知識を得ていただければ幸いです。それでは、まず文字列検索の基本概念から見ていきましょう。

PHPで文字列に特定の文字列が含まれているかを確認する基本概念

PHPにおける文字列の基本と重要性

PHPにおいて、文字列はシングルクォート(')またはダブルクォート(")で囲まれた文字の集合として定義されます。PHPは動的型付け言語であるため、変数の型宣言なしに文字列を簡単に扱うことができます。

// 文字列の定義
$single_quoted = 'こんにちは、世界!';  // シングルクォート
$double_quoted = "Hello, World!";      // ダブルクォート

// 変数展開(ダブルクォートのみ可能)
$name = "PHP";
$greeting = "Hello, $name!";  // "Hello, PHP!" となる

PHPでの文字列操作は、Webアプリケーション開発において中心的な役割を果たします。特に以下のような場面で重要です:

  • ユーザー入力の処理と検証 – フォームから送信されたデータに特定の文字列が含まれているかの確認
  • データベース操作 – SQLクエリの構築や結果の処理
  • APIレスポンスの処理 – JSONやXMLからの特定データの抽出
  • テンプレートエンジン – 動的なHTMLコンテンツの生成
  • ログ解析 – エラーログやアクセスログからの情報抽出

PHP言語自体が元々Webアプリケーション開発に特化して設計されたため、文字列処理のための豊富な組み込み関数を提供しています。2025年の最新バージョンでは、これらの機能がさらに拡張され、より簡潔で効率的なコードが書けるようになっています。

文字列検索の主要なユースケース

文字列に特定のパターンが含まれているかどうかを確認する処理は、実際のアプリケーション開発で頻繁に必要となります。具体的なユースケースとしては:

  1. バリデーション
    • メールアドレスに@記号が含まれているか
    • パスワードに特定の文字(記号、数字など)が含まれているか
    • 禁止ワードがテキスト内に含まれていないか
  2. フィルタリングと分類
    • 特定のタグを含むコンテンツのフィルタリング
    • キーワードに基づく自動カテゴリ分類
    • スパム検出(特定のパターンの存在確認)
  3. データ抽出
    • HTMLからの特定要素の抽出
    • ログファイルからのエラーメッセージの抽出
    • 長いテキストから特定の情報を見つける
  4. 条件付きロジック
    • URLパスに基づいたルーティング
    • ユーザーエージェントに基づいたコンテンツ最適化
    • 言語や地域の検出と対応

これらのユースケースでは、単純な文字列包含チェックから複雑なパターンマッチングまで、状況に応じて最適な方法を選択することが重要です。

文字列検索のパフォーマンス考慮事項

大規模なアプリケーションや大量のデータセットを扱う場合、文字列検索のパフォーマンスは重要な課題となります。効率的な文字列検索を実現するためには、以下の要素を考慮する必要があります:

  1. 検索アルゴリズムの選択
    • PHPの各文字列検索関数は内部的に異なるアルゴリズムを使用
    • 単純な包含チェックならstrpos()やstr_contains()が高速
    • 複雑なパターンには正規表現が必要だが、オーバーヘッドが大きい
  2. 文字列の長さと検索頻度
    • 短い文字列内での検索は一般的に高速
    • 長いテキスト(数MB以上)では検索速度が低下
    • 同じ文字列に対して繰り返し検索する場合はキャッシュ戦略が有効
  3. 文字エンコーディングの影響
    • マルチバイト文字(UTF-8など)を含む文字列では通常の関数が誤動作する可能性
    • 日本語や中国語などを扱う場合はmb_*系の関数が必須
    • 文字エンコーディングの変換は重いオペレーションなので最小限に抑える
  4. メモリ使用量
    • 大きな文字列を操作する場合、メモリ消費量に注意が必要
    • 文字列操作はコピーを作成することがあり、メモリ使用量が増加する可能性
    • ストリーム処理や分割処理などのテクニックでメモリ効率を改善できる

以下は基本的なパフォーマンス比較の例です:

手法単純さ速度メモリ使用量マルチバイト対応
strpos()★★★★★★★★★☆★★★★★×(要mb_*関数)
str_contains()★★★★★★★★★☆★★★★★×(要mb_*関数)
正規表現★★☆☆☆★★☆☆☆★★★☆☆△(パターン依存)
mb_strpos()★★★★☆★★★☆☆★★★★☆

大規模なシステムでは、これらのパフォーマンス特性を理解し、ユースケースに応じて適切な方法を選択することが、アプリケーション全体のレスポンス時間とスケーラビリティに大きな影響を与えます。

次のセクションからは、各手法について詳細に解説し、実際のコード例でその使い方を見ていきましょう。

方法1:strpos()を使用した文字列検索

PHPで文字列に特定の文字列が含まれているかを確認する最も基本的かつ古典的な方法はstrpos()関数を使用することです。この関数はPHPの初期バージョンから存在する基本的な文字列操作関数の一つで、広く使われています。

strpos()の基本的な使い方と注意点

strpos()関数は、ある文字列(ヘイスタック/haystack)の中で、別の文字列(ニードル/needle)が最初に現れる位置を返します。文字列が見つからない場合はfalseを返します。

基本的な構文は次のとおりです:

int|false strpos(string $haystack, string $needle, int $offset = 0)

パラメータ:

  • $haystack – 検索対象の文字列
  • $needle – 検索する文字列
  • $offset – (オプション)検索を開始する位置

戻り値:

  • $needle$haystack内で見つかった場合、その開始位置(0から始まる)
  • 見つからなかった場合はfalse

基本的な使用例:

$text = "PHPは強力なサーバーサイドスクリプト言語です。";
$search = "サーバー";

$position = strpos($text, $search);

if ($position !== false) {
    echo "「{$search}」が見つかりました(位置: {$position})";
} else {
    echo "「{$search}」は見つかりませんでした";
}
// 出力: 「サーバー」が見つかりました(位置: 8)

重要な注意点: strpos()の戻り値を確認する際には、必ず!==(厳密な不等価演算子)を使用してfalseと比較する必要があります。これは、文字列の先頭(位置0)で一致が見つかった場合、if ($position)のような条件式では誤って「見つからなかった」と判断されてしまうためです。

$text = "PHP is a popular scripting language";
$search = "PHP";

$position = strpos($text, $search); // 0を返す(先頭で一致)

// 間違った比較方法
if ($position) {
    echo "見つかりました"; // このブロックは実行されない!
} else {
    echo "見つかりませんでした"; // 0はfalseと評価されるため、このブロックが実行される
}

// 正しい比較方法
if ($position !== false) {
    echo "見つかりました"; // 正しく実行される
} else {
    echo "見つかりませんでした";
}

また、$needleが空文字列の場合、PHP 8.0未満ではwarningが発生し、PHP 8.0以降ではEmptyNeedleExceptionが投げられます。

strpos()のパフォーマンス特性

strpos()はCで実装されているため、PHPのネイティブ関数として非常に高速です。内部的にはBoyer-Mooreアルゴリズムの変種を使用しており、大きなテキスト内での検索でも効率的に動作します。

パフォーマンス面での特徴:

  1. 時間複雑度: 平均的には、ヘイスタックの長さに比例する O(n) ですが、最適化により多くの場合はサブリニアに動作します。
  2. メモリ使用量: 追加のメモリをほとんど使用せず、非常に効率的です。
  3. 大文字/小文字の区別: デフォルトでは大文字と小文字を区別します。区別しない検索にはstripos()を使います。
  4. マルチバイト文字: 標準のstrpos()はマルチバイト文字(日本語など)に対しては適切に動作しません。マルチバイト文字を扱う場合はmb_strpos()を使用する必要があります。

シンプルなベンチマーク例:

// 長いテキスト内での単一のstrpos()呼び出し
$start = microtime(true);
$haystack = str_repeat("a", 1000000) . "needle" . str_repeat("a", 1000000);
$result = strpos($haystack, "needle");
$end = microtime(true);
echo "実行時間: " . ($end - $start) . " 秒\n";
// 実行時間: 0.00123 秒 (結果は環境によって異なります)

具体的な実装例とエッジケース

1. 文字列包含チェックの基本パターン

文字列が別の文字列に含まれているかどうかをチェックする標準的なパターンです:

/**
 * 文字列が別の文字列に含まれているかを確認する
 * 
 * @param string $haystack 検索対象の文字列
 * @param string $needle 検索する文字列
 * @return bool 含まれていればtrue、そうでなければfalse
 */
function contains($haystack, $needle) {
    return strpos($haystack, $needle) !== false;
}

// 使用例
$text = "PHP8では多くの新機能が追加されました";
echo contains($text, "PHP8") ? "含まれています" : "含まれていません"; // 含まれています
echo contains($text, "Ruby") ? "含まれています" : "含まれていません"; // 含まれていません

2. 特定の位置からの検索

strpos()の第3引数を使って、特定の位置から検索を開始できます:

$text = "PHPはWebアプリケーション開発に適したPHPというスクリプト言語です";
$search = "PHP";

// 最初の出現位置
$first = strpos($text, $search); // 0が返される

// 2番目の出現位置を探す
$second = strpos($text, $search, $first + 1); // 23が返される

echo "1番目の位置: {$first}, 2番目の位置: {$second}";

3. すべての出現位置を見つける

文字列内のすべての出現位置を見つけたい場合:

/**
 * 文字列内の特定の部分文字列のすべての出現位置を見つける
 * 
 * @param string $haystack 検索対象の文字列
 * @param string $needle 検索する文字列
 * @return array 見つかった位置の配列
 */
function findAllPositions($haystack, $needle) {
    $positions = [];
    $pos = 0;
    
    while (($pos = strpos($haystack, $needle, $pos)) !== false) {
        $positions[] = $pos;
        $pos += strlen($needle); // 次の検索開始位置を設定
    }
    
    return $positions;
}

// 使用例
$text = "PHPはWebアプリケーション開発に適したPHPというスクリプト言語です";
$positions = findAllPositions($text, "PHP");
print_r($positions); // [0, 23]

4. エッジケース: 空の文字列と数値処理

strpos()を使う際の一般的なエッジケースとその対処法:

// エッジケース1: 空の文字列を検索
try {
    $result = strpos("hello", ""); // PHP 8.0以降ではEmptyNeedleExceptionが発生
} catch (ValueError $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}

// エッジケース2: 数値と文字列の自動変換
$text = "123456";
$pos = strpos($text, 34); // 数値34は文字列"34"に変換される
echo "位置: " . $pos . "\n"; // 位置: 2

// エッジケース3: nullとの比較
$pos = strpos("hello", "z"); // falseが返される
var_dump($pos == null); // bool(true) - これは誤検出
var_dump($pos === null); // bool(false) - 正確
var_dump($pos === false); // bool(true) - 正確

5. 実用的なバリデーション例

メールアドレスに@記号が含まれているかの簡易チェック:

function isValidEmailFormat($email) {
    // @記号が含まれ、先頭でも末尾でもないことを確認
    $atPosition = strpos($email, '@');
    return $atPosition !== false && $atPosition > 0 && $atPosition < strlen($email) - 1;
}

// 使用例
echo isValidEmailFormat("user@example.com") ? "有効" : "無効"; // 有効
echo isValidEmailFormat("invalid-email") ? "有効" : "無効"; // 無効

strpos()は単純な文字列検索の基本ツールですが、PHP 8.0からは次のセクションで紹介するstr_contains()という、より直感的な関数が導入されました。古いPHPコードとの互換性や、位置情報が必要な場合には引き続きstrpos()は重要な関数です。

次のセクションでは、PHP 8.0で導入されたより直感的なstr_contains()関数について詳しく見ていきましょう。

方法2:PHP 8のstr_contains()による簡潔な文字列検索

PHP 8.0(2020年11月リリース)で導入されたstr_contains()関数は、文字列が別の文字列を含んでいるかどうかを確認する最も直感的な方法です。2025年の現在、多くのプロジェクトがPHP 8系に移行しており、この関数を活用できる環境が整ってきています。

str_contains()の導入背景とstrpos()との違い

str_contains()関数は、PHPコミュニティからの長年の要望を受けて導入されました。開発者が単に文字列包含をチェックしたい場合、strpos() !== falseという書き方は直感的ではなく、特に初心者にとっては混乱の原因となっていました。

str_contains()の構文は以下の通りです:

bool str_contains(string $haystack, string $needle)

パラメータ:

  • $haystack – 検索対象の文字列
  • $needle – 検索する文字列

戻り値:

  • $haystack$needleを含んでいる場合はtrue
  • そうでない場合はfalse

strpos()との主な違いは:

  1. 直感的な真偽値の返却: 位置を返すのではなく、含まれているかどうかの真偽値を返します
  2. 簡潔な構文: strpos($haystack, $needle) !== falseではなくstr_contains($haystack, $needle)と書けます
  3. エラー処理の改善: 空文字列の$needleが許容されています(常にtrueを返します)
// strpos()を使った従来の方法
$contains = strpos("Hello World", "World") !== false;

// str_contains()を使った新しい方法
$contains = str_contains("Hello World", "World");

シンプルで読みやすいコードの実現方法

str_contains()を使用することで、コードの可読性と保守性が大幅に向上します。実際の使用例を見てみましょう:

1. 基本的な使用例

$text = "PHPは2025年も人気のサーバーサイド言語です";

// 文字列が含まれているかのチェック
if (str_contains($text, "人気")) {
    echo "この文章は「人気」という単語を含んでいます";
}

// 否定形のチェック
if (!str_contains($text, "Java")) {
    echo "この文章は「Java」という単語を含んでいません";
}

2. 配列内の文字列検索

配列内の文字列を検索する場合、array_filter()と組み合わせて使うと効果的です:

$articles = [
    "PHPの基本構文について",
    "JavaScriptフレームワークの比較",
    "PHPとMySQLの連携方法",
    "Webアプリケーションのセキュリティ対策"
];

// PHPに関する記事をフィルタリング
$phpArticles = array_filter($articles, function($article) {
    return str_contains($article, "PHP");
});

print_r($phpArticles);
// 出力:
// Array (
//     [0] => PHPの基本構文について
//     [2] => PHPとMySQLの連携方法
// )

3. 複数条件との組み合わせ

str_contains()は条件式内で他の条件と組み合わせやすいという利点もあります:

function categorizeContent($content) {
    $categories = [];
    
    if (str_contains($content, "PHP") || str_contains($content, "Laravel")) {
        $categories[] = "PHP開発";
    }
    
    if (str_contains($content, "セキュリティ") && !str_contains($content, "初心者")) {
        $categories[] = "上級セキュリティ";
    }
    
    if (str_contains(strtolower($content), "database") || 
        str_contains($content, "SQL") || 
        str_contains($content, "MySQL")) {
        $categories[] = "データベース";
    }
    
    return $categories;
}

// 使用例
$article = "PHPとMySQLを使った安全なWebアプリケーション開発";
$categories = categorizeContent($article);
print_r($categories);
// 出力: Array ( [0] => PHP開発 [1] => データベース )

4. 空文字列の処理

str_contains()は空文字列の取り扱いでstrpos()と異なる挙動を示します:

// 空の文字列を検索
var_dump(str_contains("Hello", "")); // bool(true) - 空文字列は常に含まれていると判断

// strpos()では、PHP 8.0以降はValueErrorが発生
try {
    $result = strpos("Hello", "");
} catch (ValueError $e) {
    echo $e->getMessage(); // strpos(): Argument #2 ($needle) must not be empty
}

下位互換性と代替手段

PHP 8.0未満の環境ではstr_contains()関数は利用できません。しかし、以下のようなポリフィル(代替実装)を使用することで、同様の機能を実現できます:

if (!function_exists('str_contains')) {
    /**
     * str_contains()のポリフィル実装
     * PHP 8.0未満の環境で使用
     */
    function str_contains($haystack, $needle) {
        // 空文字列の場合は常にtrue
        if ($needle === '') {
            return true;
        }
        // strposの結果を真偽値に変換
        return strpos($haystack, $needle) !== false;
    }
}

// これで環境に関わらずstr_contains()が使用可能
$result = str_contains("PHP 7.4でも動作します", "7.4");
var_dump($result); // bool(true)

このポリフィルは、アプリケーションの他の部分でPHP 8.0の機能を使用していない場合に特に有用です。フレームワークやライブラリを使用している場合、多くのフレームワーク(Laravel、Symfonyなど)では既にこのようなポリフィルが含まれているため、追加の実装は不要かもしれません。

Composer経由でのポリフィル

複数のPHP 8.0機能を下位互換性を持たせたい場合は、Composerパッケージを利用することも一つの選択肢です:

composer require symfony/polyfill-php80

このパッケージをインストールすることで、str_contains()を含むPHP 8.0の様々な機能が古いPHPバージョンでも利用可能になります。

str_contains()の実用例

1. URLバリデーション

/**
 * URLが特定のドメインを持つか確認する
 */
function isValidDomain($url, $allowedDomains) {
    foreach ($allowedDomains as $domain) {
        if (str_contains($url, $domain)) {
            return true;
        }
    }
    return false;
}

// 使用例
$url = "https://example.com/page";
$allowedDomains = ["example.com", "test.org"];
echo isValidDomain($url, $allowedDomains) ? "有効なドメイン" : "無効なドメイン";
// 出力: 有効なドメイン

2. 言語検出の簡易実装

/**
 * テキストの言語を簡易的に推定する
 */
function detectLanguage($text) {
    // 日本語の特徴的な文字を含むか
    if (str_contains($text, 'は') || str_contains($text, 'です') || 
        str_contains($text, 'ます') || str_contains($text, 'の')) {
        return 'ja';
    }
    
    // 英語のよくある単語や特徴を確認
    if (str_contains($text, 'the') || str_contains($text, 'and') || 
        str_contains($text, 'is') || str_contains($text, 'are')) {
        return 'en';
    }
    
    // その他の言語...
    return 'unknown';
}

// 使用例
echo detectLanguage("PHPは素晴らしい言語です"); // ja
echo detectLanguage("PHP is a great language"); // en

str_contains()関数は、シンプルな文字列包含チェックを行う際の最適な選択肢です。コードが読みやすくなり、エラーのリスクも減少します。PHP 8.0以上の環境であれば、strpos() !== falseの代わりにstr_contains()を使用することを強くお勧めします。

次のセクションでは、大文字小文字を区別せずに文字列検索を行うstripos()関数について説明します。

方法3:stripos()を使った大文字小文字を区別しない検索

Web開発では、ユーザー入力や外部データを扱う際に大文字小文字を区別せずに文字列検索を行いたいケースが多くあります。stripos()関数は、strpos()の大文字小文字を区別しないバージョンで、これを簡単に実現できます。

大文字小文字を区別する必要がないケース

以下のようなシナリオでは、大文字小文字を区別しない検索が有用です:

  1. ユーザー検索機能
    • ユーザーは「PHP」を検索するつもりでも「php」と入力するかもしれません
  2. メールアドレスの検証
    • RFC 5321によると、メールアドレスのローカル部分(@の前)は大文字小文字を区別しますが、ドメイン部分は区別しません
    • しかし実際の処理では、全体を大文字小文字区別なしで扱うことが一般的です
  3. ドメイン名の比較
    • URLのドメイン部分も大文字小文字を区別しません(example.comとEXAMPLE.COMは同じ)
  4. 自然言語処理
    • 記事内のキーワード検索では通常、大文字小文字は意味的な違いをもたらしません(「PHP」と「php」は同じ技術を指します)
  5. 多言語アプリケーション
    • 様々な言語や文化に対応するアプリケーションでは、大文字小文字のルールが異なる言語に対応する必要があります

stripos()の効果的な使用方法

stripos()の構文はstrpos()とほぼ同じです:

int|false stripos(string $haystack, string $needle, int $offset = 0)

パラメータ:

  • $haystack – 検索対象の文字列
  • $needle – 検索する文字列
  • $offset – (オプション)検索を開始する位置

戻り値:

  • $needle$haystack内で見つかった場合、その開始位置(0から始まる)
  • 見つからなかった場合はfalse

基本的な使用例:

$text = "PHPは人気のスクリプト言語です。phpで開発を始めましょう。";
$search = "php";

// 大文字小文字を区別する検索
$pos1 = strpos($text, $search);
echo "strpos: " . ($pos1 !== false ? "位置 {$pos1} で見つかりました" : "見つかりませんでした") . "\n";
// 出力: strpos: 見つかりませんでした

// 大文字小文字を区別しない検索
$pos2 = stripos($text, $search);
echo "stripos: " . ($pos2 !== false ? "位置 {$pos2} で見つかりました" : "見つかりませんでした") . "\n";
// 出力: stripos: 位置 0 で見つかりました

// 2番目の出現位置を検索
$pos3 = stripos($text, $search, $pos2 + 1);
echo "2回目のstripos: " . ($pos3 !== false ? "位置 {$pos3} で見つかりました" : "見つかりませんでした");
// 出力: 2回目のstripos: 位置 20 で見つかりました

stripos()を使用する際も、strpos()と同様に戻り値の確認は必ず!== falseで行うことが重要です。

効果的な実装パターン

文字列が別の文字列を含むかどうかを大文字小文字を区別せずに確認する関数:

/**
 * 文字列が別の文字列を含むか、大文字小文字を区別せずに確認する
 * 
 * @param string $haystack 検索対象の文字列
 * @param string $needle 検索する文字列
 * @return bool 含まれていればtrue、そうでなければfalse
 */
function containsIgnoreCase($haystack, $needle) {
    return stripos($haystack, $needle) !== false;
}

// 使用例
$article = "Web開発において、PHPは重要な役割を果たしています。";
echo containsIgnoreCase($article, "php") ? "PHPについて言及されています" : "PHPについては言及されていません";
// 出力: PHPについて言及されています

ユーザー入力検索の実装例

実際のWebアプリケーションにおけるユーザー入力に基づく検索の例:

/**
 * 記事リストから指定したキーワードを含む記事を検索
 * 
 * @param array $articles 記事の配列
 * @param string $keyword 検索キーワード
 * @return array キーワードを含む記事の配列
 */
function searchArticles($articles, $keyword) {
    return array_filter($articles, function($article) use ($keyword) {
        return stripos($article['title'], $keyword) !== false || 
               stripos($article['content'], $keyword) !== false;
    });
}

// 使用例
$articles = [
    ['id' => 1, 'title' => 'PHPの基礎', 'content' => 'PHPの基本構文について学びます'],
    ['id' => 2, 'title' => 'JavaScriptの応用', 'content' => 'フロントエンド開発のテクニック'],
    ['id' => 3, 'title' => 'プログラミング入門', 'content' => 'phpから始めるプログラミング'],
];

$results = searchArticles($articles, 'php');
foreach ($results as $article) {
    echo $article['title'] . "\n";
}
// 出力:
// PHPの基礎
// プログラミング入門

str_contains()と組み合わせた大文字小文字を区別しない検索方法

PHP 8環境では、stripos() !== falseのパターンに代わる、より直感的な方法が実現できます。str_contains()を使った場合の大文字小文字を区別しない検索は、strtolower()mb_strtolower()と組み合わせて以下のように実装できます:

/**
 * 文字列が別の文字列を含むか、大文字小文字を区別せずに確認する(PHP 8)
 * 
 * @param string $haystack 検索対象の文字列
 * @param string $needle 検索する文字列
 * @return bool 含まれていればtrue、そうでなければfalse
 */
function str_contains_i($haystack, $needle) {
    return str_contains(strtolower($haystack), strtolower($needle));
}

// 使用例
$text = "PHPは様々なOSで動作します";
echo str_contains_i($text, "php") ? "含まれています" : "含まれていません";
// 出力: 含まれています

マルチバイト文字対応版

日本語などのマルチバイト文字を扱う場合は、mb_strtolower()を使用します:

/**
 * マルチバイト文字対応の大文字小文字を区別しない文字列包含チェック
 * 
 * @param string $haystack 検索対象の文字列
 * @param string $needle 検索する文字列
 * @param string $encoding 文字エンコーディング
 * @return bool 含まれていればtrue、そうでなければfalse
 */
function mb_str_contains_i($haystack, $needle, $encoding = 'UTF-8') {
    return str_contains(
        mb_strtolower($haystack, $encoding),
        mb_strtolower($needle, $encoding)
    );
}

// 使用例
$text = "PHPは素晴らしい言語です";  // 全角文字のPHP
echo mb_str_contains_i($text, "PHP") ? "含まれています" : "含まれていません";
// 出力: 含まれています

パフォーマンスとエッジケース

stripos()strpos()よりもやや処理が遅いことを理解しておくことが重要です。これは、内部で大文字小文字の変換処理が行われるためです。特に大量のテキストや繰り返し処理がある場合は、パフォーマンスへの影響を考慮する必要があります。

// パフォーマンス比較
$haystack = str_repeat("a", 1000000) . "NEEDLE" . str_repeat("a", 1000000);
$needle = "needle";

$start = microtime(true);
$result1 = strpos($haystack, $needle);
$end = microtime(true);
echo "strpos: " . ($end - $start) . " 秒\n";

$start = microtime(true);
$result2 = stripos($haystack, $needle);
$end = microtime(true);
echo "stripos: " . ($end - $start) . " 秒\n";

// 出力例:
// strpos: 0.00098 秒
// stripos: 0.00245 秒
// (実際の値は環境によって異なります)

また、以下のようなエッジケースに注意が必要です:

  1. 空文字列: stripos()strpos()と同様に、PHP 8.0以降で空の$needleがエラーになります。
  2. 国際化(i18n)と地域化(l10n): 特定の言語では、大文字小文字の変換に特殊なルールがある場合があります。例えば、ドイツ語の「ß」(エスツェット)は大文字にすると「SS」になります。
  3. 英語以外の照合順序: 言語によっては大文字小文字の関係が英語と異なる場合があります。

これらのケースで正確な比較を行いたい場合は、PHPのCollatorクラス(Intl拡張)を使用することを検討してください:

/**
 * 地域に依存した大文字小文字を区別しない文字列包含チェック
 */
function locale_contains_i($haystack, $needle, $locale = 'en_US') {
    $collator = new Collator($locale);
    $collator->setStrength(Collator::SECONDARY); // 大文字小文字を区別しない
    
    // 文字列を分割して比較
    $haystackLength = mb_strlen($haystack);
    $needleLength = mb_strlen($needle);
    
    for ($i = 0; $i <= $haystackLength - $needleLength; $i++) {
        $substr = mb_substr($haystack, $i, $needleLength);
        if ($collator->compare($substr, $needle) === 0) {
            return true;
        }
    }
    
    return false;
}

// 使用例(Intl拡張が必要)
if (class_exists('Collator')) {
    $text = "Straße in Deutschland";
    echo locale_contains_i($text, "STRASSE", "de_DE") ? "含まれています" : "含まれていません";
    // 出力: 含まれています
}

stripos()は、ユーザー入力の処理や検索機能の実装など、大文字小文字の区別が重要でないケースで非常に有用です。しかし、多言語対応やパフォーマンスが重要な場合は、適切な代替手段やチューニングを検討することが重要です。

次のセクションでは、部分文字列の抽出と確認を同時に行えるstrstr()関数について説明します。

方法4:strstr()による部分文字列の抽出と確認

strstr()関数は、単に文字列が含まれているかどうかを確認するだけでなく、その部分文字列から文字列の末尾までを返す特徴を持っています。これにより、文字列の検索と抽出を一度の操作で行うことができます。

strstr()の特性と使用シナリオ

strstr()の基本的な構文は以下の通りです:

string|false strstr(string $haystack, string $needle, bool $before_needle = false)

パラメータ:

  • $haystack – 検索対象の文字列
  • $needle – 検索する文字列
  • $before_needle – (オプション)trueの場合、$needleより前の部分を返します(デフォルトはfalse)

戻り値:

  • $needle$haystackに含まれる場合、$needleから$haystackの末尾までの部分文字列(または$before_needleがtrueの場合は、$haystackの先頭から$needleの直前までの部分文字列)
  • 見つからなかった場合はfalse

strstr()は以下のような状況で特に役立ちます:

  1. 文字列の分割
    • メールアドレスからドメイン部分のみを取得する
    • URLからパスやクエリパラメータを抽出する
  2. テキスト処理
    • 特定のマーカー以降のテキストを抽出する
    • テキストを特定のキーワードの前後で分割する
  3. パース処理
    • シンプルなフォーマットのデータを解析する
    • 設定ファイルから値を取り出す

基本的な使用例:

$email = "user@example.com";

// @以降の部分(ドメイン)を取得
$domain = strstr($email, '@');
echo "ドメイン部分: {$domain}\n"; // 出力: @example.com

// @より前の部分(ユーザー名)を取得
$username = strstr($email, '@', true);
echo "ユーザー名部分: {$username}\n"; // 出力: user

部分文字列の位置と内容の両方を取得する方法

strstr()strpos()を組み合わせることで、部分文字列の位置と内容の両方を効率的に取得できます:

/**
 * 部分文字列の位置と内容を取得する
 * 
 * @param string $haystack 検索対象の文字列
 * @param string $needle 検索する文字列
 * @return array|false [位置, 内容] の配列または見つからない場合はfalse
 */
function findSubstring($haystack, $needle) {
    $position = strpos($haystack, $needle);
    
    if ($position === false) {
        return false;
    }
    
    return [
        'position' => $position,
        'content' => substr($haystack, $position),
        'length' => strlen($needle)
    ];
}

// 使用例
$text = "重要な情報: PHPは素晴らしい言語です";
$result = findSubstring($text, "PHP");

if ($result) {
    echo "位置: {$result['position']}, 内容: {$result['content']}, 長さ: {$result['length']}";
    // 出力: 位置: 8, 内容: PHPは素晴らしい言語です, 長さ: 3
}

// 特定のパターン以降の文字列を処理する例
function extractContentAfterHeading($text) {
    $parts = [];
    
    // 各見出しごとの内容を抽出
    if (strstr($text, '## ') !== false) {
        $sections = explode('## ', $text);
        array_shift($sections); // 最初の空要素を削除
        
        foreach ($sections as $section) {
            $title = strtok($section, "\n");
            $content = strstr($section, "\n");
            $parts[$title] = trim($content);
        }
    }
    
    return $parts;
}

// Markdownテキストの例
$markdown = "# ドキュメント\n\n## はじめに\nこれは導入部分です。\n\n## インストール方法\nインストール手順を説明します。\n\n## 使い方\n基本的な使用方法を解説します。";

$sections = extractContentAfterHeading($markdown);
print_r($sections);
// 出力:
// Array (
//     [はじめに] => これは導入部分です。
//     [インストール方法] => インストール手順を説明します。
//     [使い方] => 基本的な使用方法を解説します。
// )

/**
 * URLからドメイン部分とパス部分を抽出する
 */
function parseUrl($url) {
    $result = [];
    
    // プロトコル部分を削除
    $withoutProtocol = strstr($url, '://');
    if ($withoutProtocol !== false) {
        $withoutProtocol = substr($withoutProtocol, 3); // '://' の長さ(3)を削除
    } else {
        $withoutProtocol = $url; // プロトコルが含まれていない場合
    }
    
    // ドメイン部分とパス部分を分離
    $domain = strstr($withoutProtocol, '/', true);
    $path = strstr($withoutProtocol, '/');
    
    return [
        'domain' => $domain ?: $withoutProtocol, // パスがない場合
        'path' => $path ?: '/' // パスがない場合はルートパス
    ];
}

// 使用例
$url = "https://example.com/path/to/page?query=value";
$parts = parseUrl($url);
echo "ドメイン: {$parts['domain']}, パス: {$parts['path']}";
// 出力: ドメイン: example.com, パス: /path/to/page?query=value

### 大文字小文字を区別しないstristr()の活用法

//strstr()`には、大文字と小文字を区別せずに検索を行う`stristr()`というバリエーションもあります。構文は`strstr()`と同じですが、検索時に大文字小文字を区別しません。

これは特に以下のような場面で役立ちます:

  1. ユーザー入力に基づく検索
    • 検索キーワードの大文字小文字を気にしないコンテンツ検索
  2. メールアドレスやドメイン処理
    • メールアドレスのローカル部分とドメイン部分の抽出(メールプロトコルでは大文字小文字が区別されない部分がある)
  3. HTTPヘッダーの解析
    • HTTPヘッダー名は大文字小文字を区別しない

使用例:

/**
 * HTTPヘッダーからContent-Typeを取得する(大文字小文字を区別しない)
 */
function getContentType($headers) {
    foreach ($headers as $header) {
        $contentType = stristr($header, 'content-type:');
        if ($contentType !== false) {
            // 'content-type:' より後ろの部分を取得し、余分な空白を削除
            return trim(substr($contentType, strlen('content-type:')));
        }
    }
    return null;
}

// 使用例
$headers = [
    'Host: example.com',
    'Content-Type: application/json',
    'Accept: */*'
];

echo "Content-Type: " . getContentType($headers);
// 出力: Content-Type: application/json

stristr()strstr()と同様にfalseを返す可能性があるため、戻り値のチェックは厳密に行う必要があります:

$result = stristr($text, $search);
if ($result !== false) {
    // 文字列が見つかった場合の処理
} else {
    // 文字列が見つからなかった場合の処理
}

strstr()stristr()は、単なる検索だけでなく検索結果の抽出も必要な場合に特に便利です。文字列操作と検索を組み合わせたい場合には、これらの関数が最適な選択となることがあります。

次のセクションでは、より複雑なパターンマッチングのための正規表現について説明します。

方法5:正規表現を使った高度な文字列パターン検索

単純な文字列包含チェックでは対応できない複雑なパターンマッチングが必要な場合、PHPの正規表現関数が強力なソリューションとなります。特にpreg_match()関数を使うことで、高度な条件に基づいた文字列検索が可能になります。

preg_match()を使った柔軟なパターンマッチング

preg_match()の基本構文は以下の通りです:

int preg_match(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0)

パラメータ:

  • $pattern – 検索するパターン(正規表現)
  • $subject – 検索対象の文字列
  • $matches – (オプション)マッチした結果を格納する配列
  • $flags – (オプション)検索のフラグ
  • $offset – (オプション)検索を開始する位置

戻り値:

  • マッチした場合は1
  • マッチしなかった場合は0
  • エラーが発生した場合はfalse

正規表現の基本的な使い方:

$text = "お問い合わせはinfo@example.comまでお願いします。";

// メールアドレスを検索する正規表現パターン
$pattern = '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/';

if (preg_match($pattern, $text, $matches)) {
    echo "メールアドレスが見つかりました: " . $matches[0];
} else {
    echo "メールアドレスは見つかりませんでした";
}
// 出力: メールアドレスが見つかりました: info@example.com

正規表現では、パターンをデリミタ(通常は/)で囲み、その後にオプションのモディファイアを付けることができます。主なモディファイアには以下のようなものがあります:

  • i – 大文字小文字を区別しない
  • m – 複数行モード
  • s – ドット(.)が改行にもマッチ
  • u – UTF-8モード(マルチバイト文字に対応)

例えば、大文字小文字を区別せずに「php」を検索するパターンは以下のようになります:

$pattern = '/php/i';

複雑な条件での文字列検索パターン

正規表現の真価は、複雑なパターンマッチングが必要な場合に発揮されます。以下に、実用的なパターンマッチングの例を示します。

1. 日本の郵便番号を検索するパターン

$text = "私の郵便番号は123-4567です。以前は987-6543でした。";
$pattern = '/\d{3}-\d{4}/'; // 数字3桁-数字4桁のパターン

preg_match_all($pattern, $text, $matches);
echo "見つかった郵便番号: ";
print_r($matches[0]);
// 出力: 見つかった郵便番号: Array ( [0] => 123-4567 [1] => 987-6543 )

2. HTMLタグを抽出するパターン

$html = "<div class=\"container\"><p>これは<strong>重要な</strong>メッセージです</p></div>";
$pattern = '/<([a-z][a-z0-9]*)\b[^>]*>(.*?)<\/\1>/is';

if (preg_match_all($pattern, $html, $matches)) {
    echo "見つかったタグ:\n";
    foreach ($matches[1] as $i => $tag) {
        echo $tag . ": " . strip_tags($matches[0][$i]) . "\n";
    }
}
// 出力:
// 見つかったタグ:
// div: これは重要なメッセージです
// p: これは重要なメッセージです
// strong: 重要な

3. 指定の条件に合うPHPの変数名を検出するパターン

$code = '$user_id = 1; $userName = "John"; $_temp123 = true; $invalidName! = false;';
$pattern = '/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/';

preg_match_all($pattern, $code, $matches);
echo "有効なPHP変数名:\n";
print_r($matches[0]);
// 出力:
// 有効なPHP変数名:
// Array ( [0] => $user_id [1] => $userName [2] => $_temp123 [3] => $invalidName )

4. URLを検出して構成要素に分解するパターン

$text = "詳細はhttps://example.com/path/to/page?id=123&sort=dateをご覧ください";
$pattern = '/(https?):\/\/([\w\.-]+)\/([^\?]*)\??([^#]*)/';

if (preg_match($pattern, $text, $matches)) {
    echo "プロトコル: " . $matches[1] . "\n";
    echo "ドメイン: " . $matches[2] . "\n";
    echo "パス: " . $matches[3] . "\n";
    echo "クエリパラメータ: " . $matches[4] . "\n";
}
// 出力:
// プロトコル: https
// ドメイン: example.com
// パス: path/to/page
// クエリパラメータ: id=123&sort=date

5. 変数パターンを使った動的な検索

実行時に検索パターンを動的に生成することもできます:

/**
 * ユーザー入力から安全な正規表現パターンを生成
 * 
 * @param string $keyword 検索キーワード
 * @param bool $wholeWord 単語全体でマッチするかどうか
 * @param bool $caseInsensitive 大文字小文字を区別するかどうか
 * @return string 正規表現パターン
 */
function buildSearchPattern($keyword, $wholeWord = false, $caseInsensitive = true) {
    // 正規表現のメタ文字をエスケープ
    $escaped = preg_quote($keyword, '/');
    
    // 単語境界を追加するかどうか
    $pattern = $wholeWord ? '\b' . $escaped . '\b' : $escaped;
    
    // 大文字小文字を区別するかどうか
    $modifier = $caseInsensitive ? 'i' : '';
    
    // UTF-8モディファイアを追加
    $modifier .= 'u';
    
    return '/' . $pattern . '/' . $modifier;
}

// 使用例
$text = "PHPはウェブ開発に最適な言語です。php学習サイトも多数あります。";
$keyword = "php";

// 完全一致(単語全体)
$pattern1 = buildSearchPattern($keyword, true);
$count1 = preg_match_all($pattern1, $text, $matches1);

// 部分一致
$pattern2 = buildSearchPattern($keyword, false);
$count2 = preg_match_all($pattern2, $text, $matches2);

echo "完全一致: {$count1}個 - " . implode(', ', $matches1[0]) . "\n";
echo "部分一致: {$count2}個 - " . implode(', ', $matches2[0]) . "\n";

// 出力:
// 完全一致: 2個 - PHP, php
// 部分一致: 2個 - PHP, php

正規表現のパフォーマンスと使用時の注意点

正規表現は非常に強力なツールですが、誤った使い方をするとパフォーマンスの問題や予期しない結果を招くことがあります。以下に主な注意点を示します。

1. パフォーマンスへの影響

正規表現の処理は、通常の文字列関数と比べてCPU負荷が高くなります。特に以下のようなケースでは注意が必要です:

  • バックトラッキングの過剰発生
    • .*(.+)+などの贪欲な量指定子と反復の組み合わせ
  • 複雑なパターン
    • 多数の選択肢(|)や入れ子になったグループ
  • 大きなテキスト
    • 数MB以上のテキストに対する正規表現の適用

シンプルな検索であれば、正規表現よりも標準の文字列関数(strpos()など)を使用した方が効率的です。

// パフォーマンス比較
$text = str_repeat("abc def ghi jkl mno pqr stu vwx yz ", 100000);
$search = "mno";

// strpos()を使用
$start = microtime(true);
$result1 = strpos($text, $search) !== false;
$time1 = microtime(true) - $start;
echo "strpos(): " . $time1 . " 秒\n";

// preg_match()を使用
$start = microtime(true);
$result2 = preg_match('/' . preg_quote($search, '/') . '/', $text);
$time2 = microtime(true) - $start;
echo "preg_match(): " . $time2 . " 秒\n";

// 出力例:
// strpos(): 0.00021 秒
// preg_match(): 0.00312 秒
// (実際の値は環境によって異なります)

2. 安全対策

ユーザー入力から正規表現パターンを構築する場合は、必ずpreg_quote()を使用してメタ文字をエスケープする必要があります。

$userInput = "foo+bar[0-9]"; // 正規表現のメタ文字を含む入力
$safePattern = '/'. preg_quote($userInput, '/') .'/';
echo $safePattern; // /foo\+bar\[0\-9\]/

3. マルチバイト文字対応

日本語などのマルチバイト文字を扱う場合は、必ずUTF-8モディファイア(u)を使用します。

// 間違った例(マルチバイト文字を考慮していない)
preg_match('/[あ-ん]+/', '日本語の文章です');

// 正しい例(マルチバイト文字を考慮)
preg_match('/[あ-ん]+/u', '日本語の文章です');

4. 実用的なパターン設計

  • 可読性を重視する: 複雑なパターンはxモディファイアを使用して空白とコメントを追加し、可読性を高めます
$pattern = '/
    (\d{3})  # 市外局番
    -        # 区切り
    (\d{4})  # 市内局番
    -        # 区切り
    (\d{4})  # 加入者番号
/x';
  • キャプチャグループを最小限に: 不要なキャプチャグループは(?:...)構文を使用して非キャプチャグループにします
// 過剰なキャプチャ
$pattern1 = '/(https?):\/\/([\w\.-]+)\/(.*)/';

// 最小限のキャプチャ
$pattern2 = '/(?:https?):\/\/([\w\.-]+)\/(?:.*)/';

正規表現は、適切に使用すれば一般的な文字列関数では難しい複雑なパターンマッチングを可能にする強力なツールです。ただし、シンプルな文字列検索であればstrpos()str_contains()を使用する方が効率的で、コードの可読性も向上します。複雑なパターンが必要な場合のみ、正規表現の使用を検討しましょう。

次のセクションでは、日本語などのマルチバイト文字列を適切に処理するためのmb_strpos()関数について説明します。

方法6:mb_strpos()を使ったマルチバイト文字列の検索

日本語、中国語、韓国語などのマルチバイト文字を含む文字列を扱う場合、通常の文字列関数(strpos()など)では正確に処理できません。PHPのmb_*関数群は、これらの言語を適切に処理するために設計されています。

マルチバイト文字列処理の重要性

通常のASCII文字(英数字など)は1バイトで表現されますが、日本語などの多くの文字は複数バイト(UTF-8では1文字あたり最大6バイト)で表現されます。

例えば、「あ」という文字はUTF-8では3バイトで表現されます。通常の文字列関数は文字列をバイト単位で処理するため、マルチバイト文字の途中でカウントや切り取りが行われると、文字化けや誤った結果を招きます。

以下の例で、通常のstrpos()とマルチバイト対応のmb_strpos()の違いを示します:

$text = "こんにちは、PHPの世界!";

// 「PHP」の位置を検索
$pos1 = strpos($text, "PHP");
$pos2 = mb_strpos($text, "PHP", 0, 'UTF-8');

echo "strpos(): {$pos1}バイト目\n"; // 出力: strpos(): 15バイト目
echo "mb_strpos(): {$pos2}文字目\n"; // 出力: mb_strpos(): 5文字目

上記の例では、「こんにちは、」が15バイトを占めますが、実際には5文字です。マルチバイト文字を扱う場合には、文字数とバイト数を混同しないことが重要です。

マルチバイト文字列処理が特に重要となるケース:

  1. ユーザー入力の処理
    • 日本語のフォーム入力、検索キーワードなど
  2. 国際化(i18n)対応アプリケーション
    • 複数言語をサポートするWebサイトやアプリ
  3. テキスト解析
    • 日本語テキストのトークン化や形態素解析
  4. データベース操作
    • マルチバイト文字を含むデータの検索や保存

mb_strpos()の正しい使い方

mb_strpos()の基本構文は以下の通りです:

int|false mb_strpos(string $haystack, string $needle, int $offset = 0, string $encoding = null)

パラメータ:

  • $haystack – 検索対象の文字列
  • $needle – 検索する文字列
  • $offset – (オプション)検索を開始する文字位置(バイト位置ではない)
  • $encoding – (オプション)文字エンコーディング(指定しない場合はmb_internal_encoding()の値が使用される)

戻り値:

  • $needle$haystack内で見つかった場合、その文字位置(0から始まる)
  • 見つからなかった場合はfalse

基本的な使用例:

$text = "日本語でPHPを勉強しています。";
$search = "PHP";

// 文字エンコーディングを明示的に指定
$position = mb_strpos($text, $search, 0, 'UTF-8');

if ($position !== false) {
    echo "「{$search}」は {$position} 文字目に見つかりました。";
} else {
    echo "「{$search}」は見つかりませんでした。";
}
// 出力: 「PHP」は 4 文字目に見つかりました。

mb_strpos()strpos()と同様に、0を返す場合とfalseを返す場合があるため、戻り値のチェックには必ず!== falseを使用します。

エンコーディングの指定

mb_*関数を使用する際は、適切な文字エンコーディングを指定することが重要です。一般的なエンコーディング:

  • UTF-8 – 最も一般的に使用される国際的な文字エンコーディング
  • UTF-16 – Windowsで使用されることがある
  • SJIS – 日本の古いシステムで使用される
  • EUC-JP – 日本の古いUNIXシステムで使用される

アプリケーション全体で一貫して同じエンコーディングを使用するには、スクリプトの先頭でmb_internal_encoding()を設定しておくと便利です:

// スクリプト全体でUTF-8を使用
mb_internal_encoding('UTF-8');

// これ以降のmb_*関数ではエンコーディングを省略可能
$pos = mb_strpos($text, $search); // UTF-8として処理される

マルチバイト対応の文字列包含チェック関数

str_contains()のマルチバイト版は提供されていないため、自作することができます:

/**
 * マルチバイト文字対応の文字列包含チェック
 * 
 * @param string $haystack 検索対象の文字列
 * @param string $needle 検索する文字列
 * @param string $encoding 文字エンコーディング
 * @return bool 含まれていればtrue、そうでなければfalse
 */
function mb_str_contains($haystack, $needle, $encoding = 'UTF-8') {
    return mb_strpos($haystack, $needle, 0, $encoding) !== false;
}

// 使用例
$text = "こんにちは、世界!";
echo mb_str_contains($text, "世界") ? "含まれています" : "含まれていません";
// 出力: 含まれています

### マルチバイト文字列操作の実用例

#### 1. 日本語テキストの文字数制限チェック

/**
 * 指定された最大文字数を超えているかチェック
 */
function isOverMaxLength($text, $maxLength, $encoding = 'UTF-8') {
    return mb_strlen($text, $encoding) > $maxLength;
}

// 使用例
$comment = "これは日本語のコメントです。";
$maxLength = 10;

if (isOverMaxLength($comment, $maxLength)) {
    echo "コメントは{$maxLength}文字以内にしてください。";
} else {
    echo "コメントは有効です。";
}
// 出力: コメントは10文字以内にしてください。

2. 日本語テキストの要約作成

/**
 * テキストを指定された文字数で切り取り、省略記号を追加する
 */
function createSummary($text, $length = 100, $encoding = 'UTF-8') {
    if (mb_strlen($text, $encoding) <= $length) {
        return $text;
    }
    
    return mb_substr($text, 0, $length, $encoding) . '...';
}

// 使用例
$article = "PHPは動的型付けのスクリプト言語で、特にWebアプリケーション開発に適しています。1995年に初めてリリースされ、以来、継続的に改良が重ねられています。PHPは初心者にとって学びやすく、また経験豊富な開発者にとっても強力なツールです。";
echo createSummary($article, 30);
// 出力: PHPは動的型付けのスクリプト言語で、特にWebアプリケーション...

3. 日本語キーワードの強調表示

/**
 * テキスト内のキーワードをHTMLタグで強調表示する
 */
function highlightKeyword($text, $keyword, $encoding = 'UTF-8') {
    if (empty($keyword) || !mb_str_contains($text, $keyword, $encoding)) {
        return $text;
    }
    
    $pos = mb_strpos($text, $keyword, 0, $encoding);
    $keywordLength = mb_strlen($keyword, $encoding);
    
    $before = mb_substr($text, 0, $pos, $encoding);
    $highlighted = "<strong>" . mb_substr($text, $pos, $keywordLength, $encoding) . "</strong>";
    $after = mb_substr($text, $pos + $keywordLength, null, $encoding);
    
    return $before . $highlighted . $after;
}

// 使用例
$text = "PHPでマルチバイト文字列を処理するにはmb_strpos関数を使用します。";
echo highlightKeyword($text, "マルチバイト");
// 出力: PHPで<strong>マルチバイト</strong>文字列を処理するにはmb_strpos関数を使用します。

PHP 8以降でのマルチバイト文字列検索の最適化

PHP 8.0以降、マルチバイト関数のパフォーマンスが大幅に向上しました。また、いくつかの新機能と最適化が導入されています:

  1. パフォーマンスの向上
    • 内部実装の最適化による処理速度の向上
    • メモリ使用量の削減
  2. 文字列操作の一貫性
    • すべてのmb_*関数でUTF-8のサポートが強化され、特殊なケースでの挙動が改善
  3. 新しい関数
    • mb_str_pad() – マルチバイト対応の文字列パディング(PHP 8.2以降)
  4. 既存関数の改善
    • 負の開始位置と長さのより一貫したサポート

PHP 8.3以降では、マルチバイト関数のパフォーマンスがさらに向上し、特に大きなテキストデータの処理速度が改善されています。

// PHP 8.3以降のパフォーマンス比較例
$largeText = str_repeat("あいうえお", 100000);
$search = "うえ";

$start = microtime(true);
$pos1 = strpos($largeText, $search); // バイト単位で誤った結果
$time1 = microtime(true) - $start;

$start = microtime(true);
$pos2 = mb_strpos($largeText, $search, 0, 'UTF-8');
$time2 = microtime(true) - $start;

echo "strpos: " . $time1 . " 秒 (位置: " . $pos1 . ")\n";
echo "mb_strpos: " . $time2 . " 秒 (位置: " . $pos2 . ")\n";
// 出力例(PHP 8.3):
// strpos: 0.00023 秒 (位置: 2) - 誤った位置
// mb_strpos: 0.00152 秒 (位置: 1) - 正しい位置
// (PHP 7では、mb_strposの実行時間はさらに長くなります)

マルチバイト文字列処理のベストプラクティス

  1. 常にエンコーディングを指定する
    • アプリケーション全体で一貫したエンコーディングを使用し、明示的に指定する
  2. mb_internal_encoding()の設定
    • アプリケーションの初期化時に一度設定することで、多くの場合エンコーディングパラメータを省略できる
  3. 文字列長や位置の扱い
    • 常にバイト位置と文字位置の違いを意識し、適切な関数を使用する
  4. パフォーマンスの考慮
    • マルチバイト関数は通常の文字列関数よりも処理が重いため、頻繁な繰り返し処理では注意が必要
  5. 適切な関数の選択
    • ASCII文字のみの処理なら通常の文字列関数を使用
    • マルチバイト文字を含む可能性があるならmb_*関数を使用

日本語などのマルチバイト文字を扱うアプリケーションでは、mb_strpos()などのマルチバイト関数の使用は必須です。これらの関数を適切に使用することで、文字化けや処理の誤りを防ぎ、国際化対応の堅牢なアプリケーションを構築できます。

次のセクションでは、文字列の出現回数をカウントするsubstr_count()関数について説明します。

方法7:substr_count()で文字列の出現回数をカウントする方法

文字列に特定の部分文字列が含まれているかどうかだけでなく、何回出現するかも知りたい場合があります。PHPのsubstr_count()関数を使用すると、文字列内での特定のパターンの出現回数を簡単にカウントできます。

substr_count()の基本と応用

substr_count()の基本構文は以下の通りです:

int substr_count(string $haystack, string $needle, int $offset = 0, int $length = null)

パラメータ:

  • $haystack – 検索対象の文字列
  • $needle – カウントする部分文字列
  • $offset – (オプション)検索を開始する位置(バイト)
  • $length – (オプション)検索する文字列の長さ(バイト)

戻り値:

  • $needle$haystack内で出現する回数(整数)

基本的な使用例:

$text = "PHPはWebアプリケーション開発に適したスクリプト言語です。PHPは柔軟性と速度のバランスが取れており、PHPの学習曲線も比較的緩やかです。";
$search = "PHP";

$count = substr_count($text, $search);
echo "\"{$search}\"は{$count}回出現します。";
// 出力: "PHP"は3回出現します。

substr_count()の特徴:

  1. 重複しないカウント
    • 重複する部分はカウントしません(例:「ABCABC」内の「ABC」は2回とカウント、「ABABAB」内の「ABA」は1回とカウント)
  2. 大文字小文字の区別
    • デフォルトでは大文字と小文字を区別します
  3. 部分的な範囲の検索
    • オプションの$offset$lengthパラメータで検索範囲を限定できます

ケースセンシティブとケースインセンシティブのカウント

大文字小文字を区別せずにカウントしたい場合は、文字列を変換してから処理します:

/**
 * 大文字小文字を区別せずに文字列の出現回数をカウント
 * 
 * @param string $haystack 検索対象の文字列
 * @param string $needle カウントする部分文字列
 * @return int 出現回数
 */
function substr_count_i($haystack, $needle) {
    return substr_count(
        strtolower($haystack), 
        strtolower($needle)
    );
}

// 使用例
$text = "PHP is a widely-used programming language. php is easy to learn. Many websites use Php.";
$search = "php";

$sensitive = substr_count($text, $search);
$insensitive = substr_count_i($text, $search);

echo "大文字小文字を区別: {$sensitive}回\n"; // 出力: 1回
echo "大文字小文字を区別しない: {$insensitive}回\n"; // 出力: 3回

複数回出現する文字列の検出と処理

substr_count()は単に出現回数を知るだけでなく、その情報を使って様々な処理を行うことができます。

1. キーワード密度の計算

SEO(検索エンジン最適化)の文脈で、テキスト内のキーワード密度を計算する例:

/**
 * テキスト内のキーワード密度(割合)を計算
 * 
 * @param string $text 検索対象のテキスト
 * @param string $keyword 密度を計算するキーワード
 * @return float キーワードの密度(%)
 */
function keywordDensity($text, $keyword) {
    // 単語数をカウント(簡易的な実装)
    $wordCount = str_word_count($text);
    
    // キーワードの出現回数
    $keywordCount = substr_count_i($text, $keyword);
    
    // 密度の計算(%)
    return ($keywordCount / $wordCount) * 100;
}

// 使用例
$article = "PHP is a popular programming language for web development. PHP is widely used because PHP is relatively easy to learn and has good performance.";
$keyword = "PHP";

$density = keywordDensity($article, $keyword);
echo "{$keyword}のキーワード密度: " . number_format($density, 2) . "%";
// 出力: PHPのキーワード密度: 15.79%

2. 特定のパターンに基づくテキスト分析

テキスト内の特定のパターンを分析する例:

/**
 * テキスト内の各単語の出現回数をカウント
 * 
 * @param string $text 分析するテキスト
 * @return array 単語と出現回数の連想配列
 */
function wordFrequency($text) {
    // 小文字に変換し、アルファベットと数字以外を空白に置換
    $text = strtolower(preg_replace('/[^\p{L}\p{N}]+/u', ' ', $text));
    
    // 単語を配列に分割
    $words = preg_split('/\s+/', $text, -1, PREG_SPLIT_NO_EMPTY);
    
    // 各単語の出現回数をカウント
    $frequency = [];
    foreach ($words as $word) {
        if (!isset($frequency[$word])) {
            $frequency[$word] = 0;
        }
        $frequency[$word]++;
    }
    
    // 出現回数でソート
    arsort($frequency);
    
    return $frequency;
}

// 使用例
$article = "PHPはWebアプリケーション開発によく使われる言語です。PHPはシンプルでありながら強力な機能を持っています。";
$wordCounts = wordFrequency($article);

echo "頻出単語:\n";
$i = 0;
foreach ($wordCounts as $word => $count) {
    echo "{$word}: {$count}回\n";
    
    if (++$i >= 5) break; // 上位5件のみ表示
}

開始位置と長さの指定によるカスタム検索範囲

substr_count()の第3引数と第4引数を使用すると、検索範囲を制限できます。これは大きなテキストの一部だけを処理する場合や、特定の部分だけを分析する場合に便利です。

$text = "PHP is a versatile language. PHP has many use cases. PHP is easy to learn.";

// 最初の20文字のみを検索範囲とする
$count1 = substr_count($text, "PHP", 0, 20);
echo "最初の20文字内の「PHP」: {$count1}回\n"; // 出力: 1回

// 20文字目から50文字間を検索範囲とする
$count2 = substr_count($text, "PHP", 20, 50);
echo "20〜70文字目内の「PHP」: {$count2}回\n"; // 出力: 2回

特定の段落やセクション内だけでカウントしたい場合にも活用できます:

/**
 * HTML内の特定のタグ内でのキーワード出現回数をカウント
 * 
 * @param string $html HTML文字列
 * @param string $tag 検索対象のHTMLタグ
 * @param string $keyword カウントするキーワード
 * @return int 出現回数
 */
function countKeywordInTag($html, $tag, $keyword) {
    $count = 0;
    $pattern = "/<{$tag}[^>]*>(.*?)<\/{$tag}>/s";
    
    if (preg_match_all($pattern, $html, $matches)) {
        foreach ($matches[1] as $content) {
            $count += substr_count($content, $keyword);
        }
    }
    
    return $count;
}

// 使用例
$html = "<p>PHPは素晴らしい言語です。</p>
<h2>PHPの歴史</h2>
<p>PHPは1995年に誕生しました。PHPは当初Personal Home Pageの略でした。</p>";

$inParagraphs = countKeywordInTag($html, "p", "PHP");
echo "<p>タグ内の「PHP」の出現回数: {$inParagraphs}回"; // 出力: 3回

実用的なユースケース

1. プログラムの可読性分析

コード内のコメント率を計算する例:

/**
 * PHP ファイル内のコメント率(コメント行/全体行)を計算
 * 
 * @param string $code PHPコード
 * @return float コメント率(0-1の間の値)
 */
function commentRatio($code) {
    // 行数をカウント
    $lines = explode("\n", $code);
    $totalLines = count($lines);
    
    // 行コメント(//)の数
    $singleLineComments = substr_count($code, '//');
    
    // ドキュメントコメント(/** */)のブロック数
    $docCommentBlocks = substr_count($code, '/**');
    
    // 通常のコメントブロック(/* */)の数
    $multiLineCommentBlocks = substr_count($code, '/*') - $docCommentBlocks;
    
    // 平均的なコメントブロックの行数(推定値)
    $avgDocCommentLines = 5;
    $avgMultiLineCommentLines = 3;
    
    // コメント行の合計を推定
    $estimatedCommentLines = $singleLineComments + 
                             ($docCommentBlocks * $avgDocCommentLines) + 
                             ($multiLineCommentBlocks * $avgMultiLineCommentLines);
    
    // コメント率を計算
    return min(1, $estimatedCommentLines / max(1, $totalLines));
}

// 使用例
$phpCode = "<?php
/**
 * ユーザークラス
 */
class User {
    // ユーザーID
    private \$id;
    
    /**
     * コンストラクタ
     * @param int \$id ユーザーID
     */
    public function __construct(\$id) {
        \$this->id = \$id; // IDを設定
    }
    
    /* 
     * ユーザー情報を取得
     */
    public function getInfo() {
        // 情報を返す
        return ['id' => \$this->id];
    }
}
?>";

$ratio = commentRatio($phpCode);
echo "コメント率: " . number_format($ratio * 100, 1) . "%";
// 出力例: コメント率: 65.2%

2. テキスト比較と類似性分析

2つのテキスト間の共通キーワードを分析する例:

/**
 * 2つのテキスト間の共通キーワードとその頻度を分析
 * 
 * @param string $text1 1つ目のテキスト
 * @param string $text2 2つ目のテキスト
 * @param array $keywords 分析するキーワードリスト
 * @return array 分析結果
 */
function compareKeywords($text1, $text2, $keywords) {
    $result = [];
    
    foreach ($keywords as $keyword) {
        $count1 = substr_count_i($text1, $keyword);
        $count2 = substr_count_i($text2, $keyword);
        
        $result[$keyword] = [
            'text1_count' => $count1,
            'text2_count' => $count2,
            'difference' => $count2 - $count1
        ];
    }
    
    return $result;
}

// 使用例
$article1 = "PHPは動的型付けのスクリプト言語です。WebアプリケーションでよくPHPが使われています。";
$article2 = "PHPはWeb開発で人気のスクリプト言語です。PHPはLaravelなどのフレームワークも持っています。";
$keywords = ['PHP', 'スクリプト', 'Web', 'フレームワーク', '動的'];

$comparison = compareKeywords($article1, $article2, $keywords);

echo "キーワード比較結果:\n";
foreach ($comparison as $keyword => $data) {
    echo "{$keyword}: 記事1({$data['text1_count']}回), 記事2({$data['text2_count']}回), 差({$data['difference']})\n";
}

substr_count()は、単純な文字列の存在チェックを超えて、テキスト解析や頻度分析などの高度なユースケースにも活用できる便利な関数です。大規模なテキスト処理や内容分析を行う場合に特に役立ちます。

次のセクションでは、これまで紹介した7つの方法の実践的なユースケースとコード例を紹介します。

実践的なユースケースとコード例

これまで紹介した7つの文字列検索方法を実際のPHPアプリケーション開発でどのように活用するか、具体的なユースケースとコード例を見ていきましょう。実用的なシナリオを通じて、各メソッドの特徴と使い分けを理解することができます。

ユーザー入力バリデーションでの文字列検索の活用

ユーザー入力のバリデーションは、Webアプリケーション開発で最も一般的なタスクの一つです。文字列検索機能を使って、入力内容の検証を効率的に行うことができます。

1. パスワード強度のチェック

パスワードに必要な要素(大文字、小文字、数字、特殊文字)が含まれているかを確認するバリデーション:

/**
 * パスワードの強度をチェック
 * 
 * @param string $password チェックするパスワード
 * @return array 検証結果と強度スコア
 */
function checkPasswordStrength($password) {
    $result = [
        'valid' => true,
        'errors' => [],
        'strength' => 0
    ];
    
    // 最小長をチェック
    if (strlen($password) < 8) {
        $result['valid'] = false;
        $result['errors'][] = 'パスワードは8文字以上必要です';
    } else {
        $result['strength'] += 1;
    }
    
    // 大文字を含むかチェック
    if (!preg_match('/[A-Z]/', $password)) {
        $result['valid'] = false;
        $result['errors'][] = '大文字を含める必要があります';
    } else {
        $result['strength'] += 1;
    }
    
    // 小文字を含むかチェック
    if (!preg_match('/[a-z]/', $password)) {
        $result['valid'] = false;
        $result['errors'][] = '小文字を含める必要があります';
    } else {
        $result['strength'] += 1;
    }
    
    // 数字を含むかチェック
    if (!preg_match('/[0-9]/', $password)) {
        $result['valid'] = false;
        $result['errors'][] = '数字を含める必要があります';
    } else {
        $result['strength'] += 1;
    }
    
    // 特殊文字を含むかチェック
    if (!preg_match('/[^A-Za-z0-9]/', $password)) {
        $result['valid'] = false;
        $result['errors'][] = '特殊文字を含める必要があります';
    } else {
        $result['strength'] += 1;
    }
    
    // 一般的なパスワードや辞書単語が含まれていないかチェック
    $commonPasswords = ['password', 'admin', '123456', 'qwerty'];
    foreach ($commonPasswords as $commonPwd) {
        if (stripos($password, $commonPwd) !== false) {
            $result['valid'] = false;
            $result['errors'][] = '一般的な単語を含めないでください';
            $result['strength'] = max(0, $result['strength'] - 1);
            break;
        }
    }
    
    return $result;
}

// 使用例
$password = "Secure123!";
$strengthResult = checkPasswordStrength($password);

if ($strengthResult['valid']) {
    echo "パスワードは有効です。強度: " . $strengthResult['strength'] . "/5\n";
} else {
    echo "パスワードが要件を満たしていません:\n";
    foreach ($strengthResult['errors'] as $error) {
        echo "- " . $error . "\n";
    }
}

この例では、正規表現(preg_match())を使用して特定の文字の存在を確認し、同時にstripos()を使って一般的なパスワードパターンが含まれていないか確認しています。

2. メールアドレスの簡易検証

メールアドレスの基本的な形式をチェックする例:

/**
 * メールアドレスの基本的な検証
 * 
 * @param string $email 検証するメールアドレス
 * @return bool 有効な場合はtrue
 */
function validateEmail($email) {
    // @記号が含まれているか
    if (!str_contains($email, '@')) {
        return false;
    }
    
    // @の前後に文字があるか
    $parts = explode('@', $email);
    if (count($parts) !== 2 || empty($parts[0]) || empty($parts[1])) {
        return false;
    }
    
    // ドメイン部分に少なくとも1つのドットがあるか
    if (!str_contains($parts[1], '.')) {
        return false;
    }
    
    // ドットの後に文字があるか
    $domainParts = explode('.', $parts[1]);
    $tld = end($domainParts);
    if (empty($tld)) {
        return false;
    }
    
    return true;
}

// 使用例
$emails = [
    'user@example.com',
    'invalid-email',
    'user@localhost',
    'user@.com',
    '@example.com'
];

foreach ($emails as $email) {
    echo $email . ': ' . (validateEmail($email) ? '有効' : '無効') . "\n";
}
// 出力:
// user@example.com: 有効
// invalid-email: 無効
// user@localhost: 無効
// user@.com: 無効
// @example.com: 無効

#### 3. ユーザー名の使用可能文字チェック

//許可された文字のみを含むかチェックする例:

/**
 * ユーザー名に許可された文字のみが含まれているかチェック
 * 
 * @param string $username チェックするユーザー名
 * @return bool 有効な場合はtrue
 */
function validateUsername($username) {
    // 文字数をチェック
    if (strlen($username) < 3 || strlen($username) > 20) {
        return false;
    }
    
    // 英数字とアンダースコアのみを許可
    return preg_match('/^[a-zA-Z0-9_]+$/', $username) === 1;
}

// 使用例
$usernames = [
    'john_doe123',
    'user@name',
    'admin!',
    'valid_username'
];

foreach ($usernames as $username) {
    echo $username . ': ' . (validateUsername($username) ? '有効' : '無効') . "\n";
}
// 出力:
// john_doe123: 有効
// user@name: 無効
// admin!: 無効
// valid_username: 有効

データベースクエリ前の文字列検査とセキュリティ対策

SQLインジェクションは最も一般的なWebアプリケーションの脆弱性の一つです。文字列検索機能を使用して、潜在的な悪意のあるコードを検出し、防止することができます。

1. SQLインジェクション検出

ユーザー入力にSQLインジェクションの兆候があるかチェックする例:

/**
 * SQLインジェクションの可能性をチェック
 * 
 * @param string $input チェックする入力文字列
 * @return bool 疑わしい場合はtrue
 */
function checkSqlInjection($input) {
    // SQLインジェクションで一般的に使用されるキーワードや文字
    $sqlPatterns = [
        "SELECT ", "INSERT ", "UPDATE ", "DELETE ", "DROP ", 
        "UNION ", "OR 1=1", "' OR '", "\" OR \"", 
        "--;", "/*", "*/"
    ];
    
    // 大文字小文字を区別せずに検索
    foreach ($sqlPatterns as $pattern) {
        if (stripos($input, $pattern) !== false) {
            return true;
        }
    }
    
    // SQLコメント構文のチェック
    if (preg_match('/(--|#|\/\*)/', $input)) {
        return true;
    }
    
    return false;
}

パフォーマンス比較:7つの方法の速度と効率性

文字列検索メソッドの選択は、アプリケーションのパフォーマンスに大きな影響を与える可能性があります。特に大量のテキスト処理や頻繁な検索操作を行うシステムでは、最適な方法を選ぶことが重要です。この章では、紹介した7つの方法のパフォーマンス特性を比較します。

文字列長とパフォーマンスの関係

文字列の長さは、検索操作のパフォーマンスに直接影響します。各メソッドが文字列の長さに応じてどのように挙動するかを理解することが重要です。

以下は、異なる長さの文字列に対する各メソッドの相対的なパフォーマンスを示すベンチマークテストの結果です:

/**
 * 各文字列検索メソッドのパフォーマンスを比較
 * 
 * @param string $haystack 検索対象の文字列
 * @param string $needle 検索する文字列
 * @param int $iterations 繰り返し回数
 * @return array 各メソッドの実行時間(秒)
 */
function benchmarkStringMethods($haystack, $needle, $iterations = 1000) {
    $results = [];
    
    // strpos()
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $result = strpos($haystack, $needle) !== false;
    }
    $results['strpos'] = microtime(true) - $start;
    
    // str_contains() (PHP 8.0+)
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $result = str_contains($haystack, $needle);
    }
    $results['str_contains'] = microtime(true) - $start;
    
    // stripos()
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $result = stripos($haystack, $needle) !== false;
    }
    $results['stripos'] = microtime(true) - $start;
    
    // strstr()
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $result = strstr($haystack, $needle) !== false;
    }
    $results['strstr'] = microtime(true) - $start;
    
    // preg_match()
    $pattern = '/' . preg_quote($needle, '/') . '/';
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $result = preg_match($pattern, $haystack) === 1;
    }
    $results['preg_match'] = microtime(true) - $start;
    
    // mb_strpos() (UTF-8)
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $result = mb_strpos($haystack, $needle, 0, 'UTF-8') !== false;
    }
    $results['mb_strpos'] = microtime(true) - $start;
    
    // substr_count()
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $result = substr_count($haystack, $needle) > 0;
    }
    $results['substr_count'] = microtime(true) - $start;
    
    return $results;
}

// 短い文字列での比較
$shortText = "PHP is a popular programming language.";
$shortResults = benchmarkStringMethods($shortText, "PHP", 10000);

// 中程度の文字列での比較
$mediumText = str_repeat("PHP is a popular programming language. ", 100);
$mediumResults = benchmarkStringMethods($mediumText, "popular", 1000);

// 長い文字列での比較
$longText = str_repeat("PHP is a popular programming language. ", 10000);
$longResults = benchmarkStringMethods($longText, "language", 100);

// 結果を出力する関数
function printResults($results, $label) {
    echo "\n{$label}の結果:\n";
    asort($results); // 速い順にソート
    foreach ($results as $method => $time) {
        echo str_pad($method, 15) . ": " . number_format($time, 6) . " 秒\n";
    }
}

printResults($shortResults, "短い文字列");
printResults($mediumResults, "中程度の文字列");
printResults($longResults, "長い文字列");

実行結果例(PHP 8.2、環境により異なります):

短い文字列の結果:
str_contains   : 0.001234 秒
strpos         : 0.001356 秒
strstr         : 0.001458 秒
substr_count   : 0.001623 秒
stripos        : 0.002145 秒
mb_strpos      : 0.002356 秒
preg_match     : 0.003245 秒

中程度の文字列の結果:
str_contains   : 0.001987 秒
strpos         : 0.002134 秒
strstr         : 0.002356 秒
substr_count   : 0.002845 秒
stripos        : 0.003789 秒
mb_strpos      : 0.004567 秒
preg_match     : 0.008456 秒

長い文字列の結果:
str_contains   : 0.034567 秒
strpos         : 0.036789 秒
strstr         : 0.039456 秒
substr_count   : 0.045678 秒
stripos        : 0.067834 秒
mb_strpos      : 0.078945 秒
preg_match     : 0.123456 秒

結果から分かる主なパフォーマンス特性:

  1. 短い文字列の場合
    • 各メソッド間の差はわずか
    • 最も高速なのはstr_contains()strpos()
    • 正規表現(preg_match())は最も遅い
  2. 中程度の文字列の場合
    • メソッド間の差がより顕著に
    • str_contains()strpos()は引き続き最も高速
    • 正規表現の遅さがより明確に
  3. 長い文字列の場合
    • メソッド間の差が大きく拡大
    • マルチバイト関数(mb_strpos())のオーバーヘッドが顕著
    • 正規表現は特に長い文字列では著しく遅くなる

このベンチマークはASCII文字のみの文字列に対するものです。マルチバイト文字(日本語など)を含む場合は、通常のメソッド(strpos()など)は正しく機能せず、mb_strpos()が必須となります。

検索パターンとマッチング効率の影響

検索対象の文字列パターンによっても、各メソッドのパフォーマンスは変化します。

1. 単純な文字列 vs 複雑なパターン

単純な文字列検索と複雑なパターンマッチングの比較:

// 単純な文字列検索と複雑なパターン検索の比較
$text = "ユーザーのメールアドレスは user123@example.com です。電話番号は 03-1234-5678 です。";

// 単純な文字列検索
$start = microtime(true);
$iterations = 10000;
for ($i = 0; $i < $iterations; $i++) {
    $result = str_contains($text, "@example.com");
}
$simpleTime = microtime(true) - $start;

// 複雑なパターン検索(メールアドレス)
$pattern = '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/';
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
    $result = preg_match($pattern, $text) === 1;
}
$complexTime = microtime(true) - $start;

echo "単純な文字列検索(str_contains): " . number_format($simpleTime, 6) . " 秒\n";
echo "複雑なパターン検索(preg_match): " . number_format($complexTime, 6) . " 秒\n";
echo "比率(複雑 / 単純): " . number_format($complexTime / $simpleTime, 2) . "倍\n";

// 出力例:
// 単純な文字列検索(str_contains): 0.002345 秒
// 複雑なパターン検索(preg_match): 0.023456 秒
// 比率(複雑 / 単純): 10.00倍

複雑なパターンマッチングは、正規表現を使用する必要がありますが、シンプルな文字列検索に比べて数倍から数十倍遅くなる可能性があります。

2. 文字列の出現位置の影響

検索対象の文字列が、テキストの先頭、中央、末尾のどこにあるかによっても、パフォーマンスは変化します:

/**
 * 文字列の出現位置によるパフォーマンスの比較
 */
function comparePositionPerformance() {
    $prefix = str_repeat("abcdefghij", 1000); // 10,000文字
    $suffix = str_repeat("klmnopqrst", 1000); // 10,000文字
    $needle = "SEARCHPATTERN";
    
    // 先頭に配置
    $startText = $needle . $prefix . $suffix;
    
    // 中央に配置
    $middleText = $prefix . $needle . $suffix;
    
    // 末尾に配置
    $endText = $prefix . $suffix . $needle;
    
    // 存在しない場合
    $notFoundText = $prefix . $suffix . "XXXXXXXX";
    
    $iterations = 1000;
    $results = [];
    
    foreach (['strpos', 'str_contains', 'preg_match'] as $method) {
        $startTime = microtime(true);
        for ($i = 0; $i < $iterations; $i++) {
            if ($method === 'strpos') {
                strpos($startText, $needle) !== false;
            } elseif ($method === 'str_contains') {
                str_contains($startText, $needle);
            } else {
                preg_match('/' . preg_quote($needle, '/') . '/', $startText);
            }
        }
        $results[$method]['start'] = microtime(true) - $startTime;
        
        $startTime = microtime(true);
        for ($i = 0; $i < $iterations; $i++) {
            if ($method === 'strpos') {
                strpos($middleText, $needle) !== false;
            } elseif ($method === 'str_contains') {
                str_contains($middleText, $needle);
            } else {
                preg_match('/' . preg_quote($needle, '/') . '/', $middleText);
            }
        }
        $results[$method]['middle'] = microtime(true) - $startTime;
        
        $startTime = microtime(true);
        for ($i = 0; $i < $iterations; $i++) {
            if ($method === 'strpos') {
                strpos($endText, $needle) !== false;
            } elseif ($method === 'str_contains') {
                str_contains($endText, $needle);
            } else {
                preg_match('/' . preg_quote($needle, '/') . '/', $endText);
            }
        }
        $results[$method]['end'] = microtime(true) - $startTime;
        
        $startTime = microtime(true);
        for ($i = 0; $i < $iterations; $i++) {
            if ($method === 'strpos') {
                strpos($notFoundText, $needle) !== false;
            } elseif ($method === 'str_contains') {
                str_contains($notFoundText, $needle);
            } else {
                preg_match('/' . preg_quote($needle, '/') . '/', $notFoundText);
            }
        }
        $results[$method]['not_found'] = microtime(true) - $startTime;
    }
    
    return $results;
}

$positionResults = comparePositionPerformance();

echo "文字列の位置によるパフォーマンスの影響:\n";
foreach ($positionResults as $method => $results) {
    echo "\n{$method}:\n";
    echo "先頭: " . number_format($results['start'], 6) . " 秒\n";
    echo "中央: " . number_format($results['middle'], 6) . " 秒\n";
    echo "末尾: " . number_format($results['end'], 6) . " 秒\n";
    echo "存在しない: " . number_format($results['not_found'], 6) . " 秒\n";
}

// 出力例:
// strpos:
// 先頭: 0.001234 秒
// 中央: 0.003456 秒
// 末尾: 0.006789 秒
// 存在しない: 0.007890 秒
//
// str_contains:
// 先頭: 0.001345 秒
// 中央: 0.003567 秒
// 末尾: 0.006890 秒
// 存在しない: 0.007901 秒
//
// preg_match:
// 先頭: 0.004567 秒
// 中央: 0.006789 秒
// 末尾: 0.008901 秒
// 存在しない: 0.009012 秒

一般的に、文字列が先頭に近いほど検索が早く完了し、存在しない場合や末尾に近い場合は最も時間がかかる傾向があります。これは多くの文字列検索アルゴリズムが先頭から順にスキャンするためです。

各メソッドの実用的な効率比較表

以下の表は、各メソッドの主要な特性を比較したものです:

メソッド相対的速度メモリ効率大文字小文字マルチバイト対応PHP最小バージョン使用推奨シナリオ
strpos()★★★★★★★★★★区別する×PHP 4+シンプルな文字列検索、高速性が重要な場合
str_contains()★★★★★★★★★★区別する×PHP 8.0+最新環境での直感的な文字列検索
stripos()★★★★☆★★★★☆区別しない×PHP 5+大文字小文字を区別しない検索
strstr()★★★★☆★★★☆☆区別する×PHP 4+部分文字列の抽出も必要な場合
preg_match()★★☆☆☆★★★☆☆設定可能設定可能PHP 4+複雑なパターンマッチング
mb_strpos()★★★☆☆★★★★☆区別するPHP 4.2+マルチバイト文字列処理
substr_count()★★★★☆★★★★☆区別する×PHP 4+出現回数のカウントが必要な場合

実際のアプリケーションでの最適化推奨事項

実際のアプリケーション開発では、以下の点を考慮して最適なメソッドを選択することをお勧めします:

  1. シンプルな文字列検索にはstr_contains()を優先
    • PHP 8.0以上が利用可能な環境では、コードの可読性とパフォーマンスのバランスが最も良い
    • 下位互換性が必要な場合はstrpos() !== falseを使用
  2. 大文字小文字を区別しない検索にはstripos()を使用
    • ユーザー入力の検索など、大文字小文字の区別が不要なケースに最適
  3. マルチバイト文字(日本語など)を扱う場合は必ずmb_*関数を使用
    • 正しい動作のために必須、パフォーマンスよりも正確性を優先
  4. 複雑なパターンマッチングにのみ正規表現を使用
    • メールアドレスやURLなどの複雑なパターン検索に適しているが、シンプルな文字列検索では避ける
  5. クリティカルなループ内での最適化
    • 頻繁に実行される箇所では、最も効率的なメソッドを選択
    • 可能であれば事前にパターンをコンパイルするか、結果をキャッシュする
  6. 大規模なテキスト処理での段階的アプローチ
    • まず高速なメソッド(strpos()など)でフィルタリングし、その後必要に応じて詳細な処理を行う
  7. 適切なエンコーディング処理
    • 国際化対応アプリケーションでは、一貫したエンコーディング(通常はUTF-8)を使用
    • マルチバイト文字と一般ASCIIのみの文字列で異なるロジックを適用することも考慮

結論

PHP 8系の最新環境では、str_contains()関数がシンプルで直感的なAPIと高速なパフォーマンスを兼ね備えているため、多くの一般的なユースケースで最適な選択肢となります。ただし、特定の要件(マルチバイト文字、複雑なパターン、大文字小文字の区別なし)がある場合は、それぞれの状況に適したメソッドを選択することが重要です。

大規模なアプリケーションでは、パフォーマンスクリティカルな部分で適切な文字列検索メソッドを選択することで、全体的な応答時間とスケーラビリティを大幅に向上させることができます。特に大量のテキスト処理や頻繁な検索操作を行うシステムでは、この章で説明したパフォーマンス特性を考慮して設計することをお勧めします。

まとめ

この記事では、PHPで文字列に特定の文字列が含まれているかを確認する7つの方法について詳しく解説しました。各メソッドの特徴、使用方法、パフォーマンス特性、そして適切なユースケースを紹介しました。2025年の最新情報も踏まえて、現代のPHP開発における最適なアプローチを提案しました。

7つの方法の要点

  1. strpos() – 古典的で高速な文字列検索関数
    • 最も基本的かつ高速な文字列検索手段
    • !== falseでの比較が必要なため、初心者には直感的でない
    • 位置情報が必要な場合に特に有用
  2. str_contains() – PHP 8で導入された直感的な文字列包含チェック
    • 単純で読みやすい構文(PHP 8.0以上が必要)
    • パフォーマンスも優れており、多くの場合で最良の選択肢
    • 下位互換性のためにポリフィルを提供可能
  3. stripos() – 大文字小文字を区別しない検索
    • ユーザー入力やユーザーフレンドリーな検索に最適
    • strpos()よりやや遅いが、柔軟性が高い
    • 国際化対応アプリケーションで役立つ
  4. strstr() – 部分文字列の抽出と確認を同時に実行
    • 検索と抽出を一度に行える便利な関数
    • テキスト処理やパース処理に適している
    • 大文字小文字を区別しないstristr()も利用可能
  5. 正規表現 – 複雑なパターンマッチング
    • 高度で柔軟なパターンマッチングが可能
    • シンプルな検索よりも遅いため、複雑なパターンにのみ使用
    • 強力だが、過剰な使用はパフォーマンスに影響する
  6. mb_strpos() – マルチバイト文字列対応の検索
    • 日本語などのマルチバイト文字を処理する場合に必須
    • エンコーディングを明示的に指定することが重要
    • PHP 8系での最適化によりパフォーマンスが向上
  7. substr_count() – 文字列の出現回数をカウント
    • 単なる包含チェックではなく、頻度分析が必要な場合に有用
    • テキストマイニングや分析に適している
    • 検索範囲を制限するオプションも提供

ユースケース別の最適な選択

実際のアプリケーション開発では、状況に応じて最適な方法を選択することが重要です:

  • 一般的な文字列包含チェック: PHP 8.0以上ならstr_contains()、それ以前のバージョンならstrpos() !== false
  • 大文字小文字を区別しない検索: stripos()またはstr_contains()strtolower()の組み合わせ
  • 日本語などのマルチバイト文字列: 必ずmb_strpos()またはmb_str_contains()(自作関数)
  • 複雑なパターンマッチング: 正規表現(preg_match()など)
  • テキスト分析と頻度カウント: substr_count()
  • パフォーマンスクリティカルな部分: strpos()またはstr_contains()(最も高速)
  • テキスト抽出も同時に行う: strstr()

今後の展望

PHP言語は継続的に進化しており、文字列操作関数も改善されています:

  1. パフォーマンスの最適化
    • PHP 8.x系での継続的な内部最適化により、文字列関数の処理速度は向上しています
    • JITコンパイラの進化により、繰り返し実行される文字列操作のパフォーマンスが向上
  2. APIの一貫性
    • str_contains()のような直感的なAPIの導入は、PHPの開発者体験を向上させる傾向を示しています
    • 将来的には他の文字列操作関数も同様のシンプルなインターフェースに統一される可能性があります
  3. マルチバイト対応の強化
    • 国際化対応の重要性が増す中、マルチバイト関数のさらなる最適化と機能拡張が期待されます
    • 将来的には標準関数とマルチバイト関数の統合が進む可能性もあります
  4. より高度なテキスト処理機能
    • 自然言語処理やテキスト分析のニーズの増加に伴い、より高度な文字列分析機能が追加される可能性があります

最後に

PHPにおける文字列操作、特に文字列の包含チェックは、Webアプリケーション開発において基本的かつ重要なスキルです。この記事で紹介した7つの方法を状況に応じて適切に使い分けることで、より効率的で保守性の高いコードを書くことができます。

2025年現在、PHP 8.xシリーズが広く採用されている環境では、str_contains()を中心とした新しいAPIを積極的に活用することをお勧めします。しかし、マルチバイト文字の処理や複雑なパターンマッチングなど、特定のニーズに対しては、それぞれの状況に最適化された方法を選択することが、高品質なアプリケーション開発の鍵となります。

文字列操作の基本をマスターすることで、より複雑なPHPアプリケーションの開発にも自信を持って取り組むことができるでしょう。