【PHP】 空白文字で文字列を分割して検索用キーワードの配列を作る


これは何?

データベースの LIKE 検索用途とかでキーワードを抽出するやつ。
例えば

PHP Qiita Laravel

と入力したら,

PHP Qiita Laravel

として取り出す関数を作ります。

実装

実装したい関数の形を示し, 2 つの異なるアプローチでの実装例を記載します。

関数シグネチャ

$input に入力文字列を渡し, $limit にキーワード数制限を指定します。 -1 を指定した場合は無制限になり,これをデフォルトとします。

function extractKeywords(string $input, int $limit = -1): array

手法の比較

※ 「手法」の名称は一般的に提唱されているものではありません

手法 説明 よく使われる関数
空白分割法 空白文字で分割し,残った部分をキーワードとする。キーワード数制限を超過する場合,最後の1つの中に空白文字を含む残りのすべてが含まれる。 preg_split()
explode()
非空白抽出法 空白文字ではない部分だけを直接キーワードとして抽出する。キーワード数制限を超過する場合,単に超過分は無視される。 preg_match_all()
preg_replace_callback()

実装が簡単なのは空白分割法のほうですが,キーワード数制限超過時の挙動がそれぞれ異なるので注意してください。例えば a b c d e において $limit = 3 で抽出する場合,

  • 空白分割法の場合, a b c d e となる
  • 非空白抽出法の場合, a b c となる

という違いがあります。

空白分割法

半角スペースで分割

一番シンプルな実装。 explode() を使う手法もありますが, $limit の適用や空文字列の除外を考えると,こちらのほうが基本的に上位互換です。

function extractKeywords(string $input, int $limit = -1): array
{
    return preg_split('/ ++/', $input, $limit, PREG_SPLIT_NO_EMPTY);
}

あらゆる空白文字で分割

半角スペースの他に全角スペース,改行,タブ,ノーブレークスペースなどあらゆる空白系の制御文字を対象とする場合はこちら。 \p{Z} は ASCII 範囲にある制御文字の集合, \p{Cc} は Unicode 範囲にある制御文字の集合を表しています。またこれらを適用するためには u フラグが必須になります。

function extractKeywords(string $input, int $limit = -1): array
{
    return preg_split('/[\p{Z}\p{Cc}]++/u', $input, $limit, PREG_SPLIT_NO_EMPTY);
}

非空白抽出法

半角スペース以外を抽出

function extractKeywords(string $input, int $limit = -1): array
{
    $matches = [];
    preg_replace_callback(
        '/[^ ]++/',
        function (array $match) use (&$matches) {
            $matches[] = $match[0];
        },
        $input,
        $limit,
        $_,
        PREG_SET_ORDER
    );
    return $matches;
}

「なんで preg_match_all() じゃないんだ!?」

が素直な感想だと思います。 preg_replace_callback() で参照代入を使うみたいな変なことをやっているのは, $limit 適用に対応するためです。 preg_match_all() にはマッチング回数を制限する仕組みがありません。

あらゆる空白文字以外を抽出

function extractKeywords(string $input, int $limit = -1): array
{
    $matches = [];
    preg_replace_callback(
        '/[^\p{Z}\p{Cc}]++/u',
        function (array $match) use (&$matches) {
            $matches[] = $match[0];
        },
        $input,
        $limit,
        $_,
        PREG_SET_ORDER
    );
    return $matches;
}

パターンの部分を変えるだけで全体的な体裁は同じですね。

ダブルクオーテーションで括った部分は保持したまま,あらゆる空白文字以外の部分を抽出

こちらは,非空白抽出法でしか実装できません。

Laravel "Taylor Otwell" PHP と入力したら Laravel Taylor Otwell PHP として欲しい場合はこちら。 厳密にイコールではないですが,ある程度 Google 検索っぽい動きになります。

function extractKeywords(string $input, int $limit = -1): array
{
    $matches = [];
    preg_replace_callback(
        '/""(*SKIP)(*FAIL)|"([^"]++)"|([^"\p{Z}\p{Cc}]++)/u',
        function (array $match) use (&$matches) {
            $matches[] = $match[2] ?? $match[1];
        },
        $input,
        $limit,
        $_,
        PREG_SET_ORDER
    );
    return $matches;
}

ダブルクオーテーションで括られた空文字列は, PCRE 特有の機能である (*SKIP)(*FAIL) でマッチングを強制終了して $limit 適用の際に 1 キーワードとしてカウントしない,など少し気を利かせています。

補足

重複を削除する

もし重複したキーワードを削除する場合は, array_unique() + array_values() の処理を組み合わせてください。以下に使用率の高そうな,適用版の一部を記載しておきます。

あらゆる空白文字で分割し,重複を除外する
function extractKeywords(string $input, int $limit = -1): array
{
    return array_values(array_unique(preg_split('/[\p{Z}\p{Cc}]++/u', $input, $limit, PREG_SPLIT_NO_EMPTY)));
}
ダブルクオーテーションで括った部分は保持したまま,あらゆる空白文字以外の部分を抽出し,重複を除外する
function extractKeywords(string $input, int $limit = -1): array
{
    $matches = [];
    preg_replace_callback(
        '/""(*SKIP)(*FAIL)|"([^"]++)"|([^"\p{Z}\p{Cc}]++)/u',
        function (array $match) use (&$matches) {
            $matches[] = $match[2] ?? $match[1];
        },
        $input,
        $limit,
        $_,
        PREG_SET_ORDER
    );
    return array_values(array_unique($matches));
}