preg_matchとは、文字列が正規表現のパターンに合うかを調べるPHPの関数で、マッチすれば1、しなければ0、エラーのときはfalseを返します。 第3引数に変数を渡すと、マッチした部分やカッコで囲んだ部分(キャプチャ)を配列で受け取れます。置換は preg_replace、すべてのマッチを集めるのは preg_match_all、分割は preg_split と、目的ごとに関数が分かれています。

この記事では、preg系の6関数の引数と戻り値を一覧表にしたうえで、「0とfalseを取り違えると入力チェックをすり抜ける」バグを再現します。また、日本語で u 修飾子を付けないと起きる文字化けと誤マッチ、ひらがな・カタカナ・漢字・全角英数の判定パターン、郵便番号・電話番号・メール・日付の定番パターン、名前付きキャプチャ、PHP 8系での挙動、preg_last_error_msg() でのエラー確認までを扱います。掲載したコードと出力は、すべて PHP 8.4.21(PCRE2 10.42)で実行して確かめたものです。戻り値やフラグの仕様は、PHP公式マニュアルの preg_match と preg_last_error_msg のページを今回開いて確認しました。

preg系関数の早見表

関数 用途 主な引数 成功時の戻り値 エラー時
preg_match 最初の1件にマッチするか調べる $pattern, $subject, &$matches, $flags, $offset マッチ 1/マッチなし 0 false
preg_match_all すべてのマッチを集める $pattern, $subject, &$matches, $flags, $offset マッチした件数(0以上の整数) false
preg_replace パターンに合う部分を置換する $pattern, $replacement, $subject, $limit, &$count 置換後の文字列(配列を渡すと配列) null
preg_replace_callback 関数の戻り値で置換する $pattern, $callback, $subject, $limit, &$count, $flags 置換後の文字列(配列を渡すと配列) null
preg_split パターンで分割する $pattern, $subject, $limit, $flags 分割後の配列 false
preg_quote 正規表現の記号をエスケープする $str, $delimiter エスケープ後の文字列 (エラーなし)
preg_grep 配列からパターンに合う要素を取り出す $pattern, $array, $flags 合った要素の配列(キー維持) false
preg_last_error_msg 直前のpreg関数のエラー内容を返す なし "No error" などの文字列 ―

覚えておきたいのは「マッチなしは0、エラーはfalse(置換系はnull)」という区別です。判定は === 1 や === false のように厳密比較で書くと、取り違えが起きません。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PHP preg_matchの使い方|preg_replaceと日本語対応」

preg_matchの基本の使い方

書式と最小のコード

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

パターンは / などの区切り文字(デリミタ)で囲み、その後ろに修飾子を書きます。

<?php
$r = preg_match('/(\d{4})-(\d{2})-(\d{2})/', '発売日は2026-10-03です', $m);
var_dump($r);
print_r($m);

var_dump(preg_match('/\d+/', 'abc'));

実行結果:

int(1)
Array
(
    [0] => 2026-10-03
    [1] => 2026
    [2] => 10
    [3] => 03
)
int(0)

$m[0] にマッチ全体、$m[1] 以降にカッコで囲んだ部分が順に入ります。マッチしなかったときは int(0) です。

マッチしたかどうかの書き方

書き方 判定 補足
if (preg_match($p, $s) === 1) マッチしたときだけ真 いちばん誤解のない書き方
if (preg_match($p, $s)) マッチしたときだけ真 0 と false がどちらも偽になる
if (!preg_match($p, $s)) マッチなしまたはエラーで真 次の項のバグにつながる
if (preg_match($p, $s) === 0) マッチなしのときだけ真 エラーは別扱いにできる

0とfalseを取り違えるバグを再現する

falseが返る3つの場面

preg_match が false を返す代表的な場面を実行して確かめました。

<?php
// 1. パターンの書き間違い(カッコが閉じていない)
$r = @preg_match('/(abc/', 'abc');
var_dump($r);
echo preg_last_error(), ' ', preg_last_error_msg(), "\n";

// 2. バックトラックの上限超過(JITを切った状態)
ini_set('pcre.jit', '0');
$r = preg_match('/(?:\D+|<\d+>)*[!?]/', 'foobar foobar foobar');
var_dump($r);
echo preg_last_error(), ' ', preg_last_error_msg(), "\n";

// 3. u修飾子つきのパターンに、UTF-8として正しくない文字列を渡した
$bad = "abc\xff";
$r = preg_match('/./u', $bad);
var_dump($r);
echo preg_last_error(), ' ', preg_last_error_msg(), "\n";

実行結果:

bool(false)
1 Internal error
bool(false)
2 Backtrack limit exhausted
bool(false)
4 Malformed UTF-8 characters, possibly incorrectly encoded

1 は @ を外すと Warning: preg_match(): Compilation failed: missing closing parenthesis at offset 4 という警告も出ます。3 は、外部から届いた文字列(フォーム入力、CSV、他システムのデータ)に別の文字コードが混ざっているだけで起きるため、もっとも出会いやすい場面です。

入力チェックをすり抜ける例

「< や > を含まなければ合格」という関数を、否定の ! で書いた例です。

<?php
// 直す前:マッチしなければ合格、のつもり
function isValidBuggy(string $s): bool
{
    return !preg_match('/[<>]/u', $s);
}

var_dump(isValidBuggy("ok"));        // 含まない
var_dump(isValidBuggy("<b>"));       // 含む
var_dump(isValidBuggy("<b>\xff"));   // 含む+正しくないUTF-8

実行結果:

bool(true)
bool(false)
bool(true)

3つめは < を含んでいるのに true(合格)になりました。preg_match がエラーで false を返し、!false が true になったためです。「マッチなし(0)」と「調べられなかった(false)」を同じ扱いにしたことが原因です。

<?php
// 直した後:falseは例外にし、0のときだけ合格にする
function isValidFixed(string $s): bool
{
    $r = preg_match('/[<>]/u', $s);
    if ($r === false) {
        throw new RuntimeException(preg_last_error_msg());
    }
    return $r === 0;
}

try {
    var_dump(isValidFixed("<b>\xff"));
} catch (Exception $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
}

実行結果:

RuntimeException: Malformed UTF-8 characters, possibly incorrectly encoded

入力の文字コードを先に確かめる方法も有効です。mb_check_encoding($s, 'UTF-8') が false なら、正規表現にかける前に入力エラーとして返せます。

置換・分割系のエラー時の戻り値

同じ「正しくないUTF-8」を渡したときの各関数の戻り値です。

呼び出し 戻り値 preg_last_error_msg()
preg_match('/./u', $bad) false Malformed UTF-8 characters, possibly incorrectly encoded
preg_replace('/a/u', 'b', $bad) NULL 同上
preg_split('/,/u', $bad) false 同上
続けて preg_match('/a/', 'abc') 1 No error

preg_replace はエラーのとき null を返すため、結果をそのまま変数に上書きすると元の文字列が消えます。 $s = preg_replace(...) ?? $s; と書くか、null のときに例外を投げると安全です。エラー情報は次のpreg関数の呼び出しで上書きされるので、確認は直後に行います。

preg_last_errorのエラー一覧

定数 値 preg_last_error_msg() 主な原因
PREG_NO_ERROR 0 No error 正常
PREG_INTERNAL_ERROR 1 Internal error パターンの文法誤りなど
PREG_BACKTRACK_LIMIT_ERROR 2 Backtrack limit exhausted 入れ子の繰り返しで試行回数が上限(pcre.backtrack_limit)に到達
PREG_RECURSION_LIMIT_ERROR 3 Recursion limit exhausted 再帰の上限に到達
PREG_BAD_UTF8_ERROR 4 Malformed UTF-8 characters, possibly incorrectly encoded u 修飾子で不正なUTF-8
PREG_BAD_UTF8_OFFSET_ERROR 5 The offset did not correspond to the beginning of a valid UTF-8 code point $offset が文字の途中
PREG_JIT_STACKLIMIT_ERROR 6 JIT stack limit exhausted 長い入力でJITのスタックが不足

値0〜2・4〜6のメッセージは今回の実行で確認したもので、3は未実行です(一般に知られた定数として載せています)。5は preg_match('/語/u', '日本語', $m, 0, 1) のように開始位置を文字の途中にすると再現できます。6は preg_match('/^(?:a|b)*$/', str_repeat('a', 100000)) で再現でき、false が返りました。preg_last_error_msg() は PHP 8.0 以降で使えます。

日本語を扱うときはu修飾子を付ける

u修飾子がないと「1文字」が1バイトになる

UTF-8の日本語は1文字が3バイトです。u 修飾子を付けないと、正規表現はバイト単位で動きます。

<?php
var_dump(preg_match('/^.{3}$/',  '日本語'));  // 3「文字」のつもり
var_dump(preg_match('/^.{3}$/u', '日本語'));
var_dump(preg_match('/^.{9}$/',  '日本語'));  // 実際は9バイト

preg_match('/^./', '日本語', $m);
var_dump($m[0], bin2hex($m[0]));

preg_match('/^./u', '日本語', $m);
var_dump($m[0]);

実行結果:

int(0)
int(1)
int(1)
string(1) "�"
string(2) "e6"
string(3) "日"

u なしでは「日」の先頭1バイト(e6)だけが取り出され、表示すると文字化けします。文字数の制限や先頭文字の取得は、u を付けて初めて意図どおりになります。

文字クラスが別の文字にマッチする

さらに注意したいのが、[あ] のような文字クラスです。

<?php
var_dump(preg_match('/[あ]/',  'い'));   // 「あ」は含まれていない
var_dump(preg_match('/[あ]/u', 'い'));
var_dump(preg_match('/[あ]/',  '「'));
echo bin2hex('あ'), ' ', bin2hex('い'), ' ', bin2hex('「'), "\n";

var_dump(preg_replace('/[あ]/',  '', 'あいう'));
var_dump(preg_replace('/[あ]/u', '', 'あいう'));

実行結果:

int(1)
int(0)
int(1)
e38182 e38184 e3808c
string(2) "��"
string(6) "いう"

u なしの [あ] は「e3 81 82 のどれか1バイト」という意味になり、同じバイトを含む「い」や「「」にもマッチします。置換では文字の一部のバイトだけが消えて、残りが文字化けしました。日本語を含むパターン・日本語を対象にする処理では、常に u を付けるのが基本です。

u修飾子で変わる動き一覧

パターン 対象 u なし u あり 解説
/^.{3}$/ 日本語 0 1 . が1文字を表すようになる
/[あ]/ い 1 0 文字クラスが文字単位になる
/^\w+$/ 日本語 0 1 \w が日本語の文字も含むようになる
/\s/ 全角 空白 0 1 \s が全角スペースも含むようになる
/^\d+$/ 123 0 1 \d が全角数字も含むようになる
/é/i É 0 1 大文字小文字の無視がASCII以外にも効く
preg_split('//', 'あい') ― 6個のバイト ["あ","い"] 1文字ずつの分割

u を付けると \d が全角数字にもマッチする点は見落としやすいところです。半角数字だけを通したいときは \d ではなく [0-9] と書きます(preg_match('/^[0-9]+$/u', '123') は 0 でした)。

マッチ位置はバイト数で返る

PREG_OFFSET_CAPTURE で得られる位置は、u を付けてもバイト単位です。

<?php
preg_match('/語/u', '日本語', $m, PREG_OFFSET_CAPTURE);
echo $m[0][1], "\n";                                   // 6(バイト)
echo mb_strlen(substr('日本語', 0, $m[0][1])), "\n";   // 2(文字)

文字数での位置が必要なときは、上のように substr で切り出してから mb_strlen で数え直します。

ひらがな・カタカナ・漢字・全角英数の判定パターン集

そのままコピーして使える形にまとめました。\A と \z は文字列の先頭と末尾を表します(^ $ との違いは後述)。

<?php
const JP_PATTERNS = [
    'ひらがなのみ'           => '/\A[ぁ-ゖー]+\z/u',
    'ひらがなのみ(Unicode)' => '/\A[\p{Hiragana}ー]+\z/u',
    '全角カタカナのみ'       => '/\A[ァ-ヶー]+\z/u',
    'カタカナ(半角含む)'   => '/\A[\p{Katakana}ー]+\z/u',
    '半角カタカナのみ'       => '/\A[ヲ-゚]+\z/u',
    '漢字のみ'               => '/\A\p{Han}+\z/u',
    '全角英数字のみ'         => '/\A[A-Za-z0-9]+\z/u',
    '全角数字のみ'           => '/\A[0-9]+\z/u',
    '日本語の文字を含む'     => '/[\p{Hiragana}\p{Katakana}\p{Han}]/u',
    '全角文字のみ'           => '/\A[^\x01-\x7Eヲ-゚]+\z/u',
];

foreach (JP_PATTERNS as $name => $pattern) {
    echo $name, ': ', preg_match($pattern, 'やまだ'), "\n";
}

各パターンに10種類の文字列を渡した実行結果です(1=マッチ、0=マッチなし)。

パターン ひらがな カタカナ ハンカク 漢字 佐々木 ABC123 abc123 らーめん コーヒー
ひらがなのみ 1 0 0 0 0 0 0 1 0
ひらがなのみ(Unicode) 1 0 0 0 0 0 0 1 0
全角カタカナのみ 0 1 0 0 0 0 0 0 1
カタカナ(半角含む) 0 1 1 0 0 0 0 0 1
半角カタカナのみ 0 0 1 0 0 0 0 0 0
漢字のみ 0 0 0 1 1 0 0 0 0
全角英数字のみ 0 0 0 0 0 1 0 0 0
日本語の文字を含む 1 1 1 1 1 0 0 1 1
全角文字のみ 1 1 0 1 1 1 0 1 1

読み取りのポイントは次の3つです。

  • 長音記号「ー」は \p{Hiragana} にも \p{Katakana} にも含まれないため、「らーめん」「コーヒー」を通すには文字クラスに ー を足す
  • \p{Katakana} は半角カタカナにもマッチする。全角だけに絞るなら [ァ-ヶー] を使う
  • 「々」は \p{Han} に含まれるため、「佐々木」は漢字のみのパターンで通る(PCRE2 10.42 での結果)

全角英数や半角カタカナを受け取ったあとで形式をそろえたい場合は、判定で弾くより mb_convert_kana で変換するほうが入力する人にやさしい作りになります。

<?php
echo mb_convert_kana('ABC123 カタカナ', 'asKV');  // ABC123 カタカナ

定番パターン表(郵便番号・電話番号・メール・日付)

コピペ用のパターン

<?php
const PATTERNS = [
    '郵便番号(ハイフン必須)'   => '/\A\d{3}-\d{4}\z/',
    '郵便番号(ハイフン任意)'   => '/\A\d{3}-?\d{4}\z/',
    '固定電話(ハイフンあり)'   => '/\A0\d{1,4}-\d{1,4}-\d{4}\z/',
    '携帯電話'                   => '/\A0[789]0-?\d{4}-?\d{4}\z/',
    '電話(数字10〜11桁)'       => '/\A0\d{9,10}\z/',
    'メール(簡易)'             => '/\A[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+\z/',
    '日付 YYYY-MM-DD'            => '/\A\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])\z/',
    '日付(区切り混在)'         => '/\A(\d{4})[-\/.](\d{1,2})[-\/.](\d{1,2})\z/',
    '時刻 HH:MM'                 => '/\A(?:[01]\d|2[0-3]):[0-5]\d\z/',
    'URL(http/https)'         => '/\Ahttps?:\/\/[\w.-]+(?::\d+)?(?:\/[\w\-.\/?%&=~#+]*)?\z/',
    '半角英数字のみ'             => '/\A[A-Za-z0-9]+\z/',
    '英字と数字を含む8字以上'    => '/\A(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}\z/',
    '整数'                       => '/\A-?(?:0|[1-9]\d*)\z/',
    '小数'                       => '/\A-?\d+(?:\.\d+)?\z/',
    'カンマ区切りの数値'         => '/\A\d{1,3}(?:,\d{3})*\z/',
];

これらは u を付けていないので、\d は半角数字だけにマッチします。

実行して確かめた結果

パターン マッチした例(1) マッチしなかった例(0)
郵便番号(ハイフン必須) 100-0001 1000001、100-00011
郵便番号(ハイフン任意) 100-0001、1000001 100-0001(全角)
固定電話(ハイフンあり) 03-1234-5678、0123-45-6789、090-1234-5678 0312345678
携帯電話 090-1234-5678、08012345678 060-1234-5678
電話(数字10〜11桁) 0312345678、09012345678 12345
メール(簡易) taro@example.com、a.b+c@mail.example.co.jp taro@example、@example.com
日付 YYYY-MM-DD 2026-10-03、2026-02-31 2026-13-01、2026/10/03
日付(区切り混在) 2026/10/3、2026.1.5 20261003
時刻 HH:MM 09:30、23:59 24:00
URL https://example.com/a?b=1、http://localhost:8080 ftp://example.com
半角英数字のみ abc123 abc-123、abc
英字と数字を含む8字以上 abc12345 abcdefgh、a1b2
整数 0、-12 012、1.5
小数 1.5、10 1.、.5
カンマ区切りの数値 1,234,567、999 12,34

正規表現だけで決めきれないもの

日付のパターンは 2026-02-31 にもマッチしました。 形式は正しくても、実在する日かどうかは正規表現では判定しきれません。形式を正規表現で、実在を checkdate() で確かめる2段構えにします。

<?php
function parseDate(string $s): ?array
{
    $r = preg_match('/\A(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})\z/', $s, $m);
    if ($r === false) {
        throw new RuntimeException(preg_last_error_msg());
    }
    if ($r === 0) {
        return null;
    }
    return checkdate((int)$m['m'], (int)$m['d'], (int)$m['y'])
        ? [(int)$m['y'], (int)$m['m'], (int)$m['d']]
        : null;
}

echo json_encode([parseDate('2026-10-03'), parseDate('2026-02-31'), parseDate('abc')]);

実行結果:

[[2026,10,3],null,null]

メールアドレスも同様で、PHPには filter_var($s, FILTER_VALIDATE_EMAIL) が用意されています(taro@example は false、taro@example.com はそのまま返ることを確認しました)。自前の正規表現は「明らかな打ち間違いを早めに知らせる」用途に留め、最終的な確認は専用の関数や確認メールで行うと堅実です。

^ $ と \A \z の違い

入力チェックでは ^...$ より \A...\z をおすすめします。$ は「末尾の改行の直前」にもマッチするためです。

<?php
var_dump(preg_match('/^\d{3}$/',   "123\n"));  // int(1) 改行つきでも通る
var_dump(preg_match('/\A\d{3}\z/', "123\n"));  // int(0)
var_dump(preg_match('/^\d{3}$/D',  "123\n"));  // int(0) D修飾子でも同じ効果

名前付きキャプチャで読みやすくする

(?<名前>...) と書くと、番号に加えて名前でも結果を取り出せます。

<?php
preg_match('/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/', '2026-10-03', $m);
print_r($m);

['year' => $y, 'month' => $mo] = $m;
echo "$y / $mo\n";

実行結果:

Array
(
    [0] => 2026-10-03
    [year] => 2026
    [1] => 2026
    [month] => 10
    [2] => 10
    [day] => 03
    [3] => 03
)
2026 / 10

名前のキーと番号のキーの両方が入ります。 カッコを追加して番号がずれても、名前で取り出しているコードはそのまま動くのが利点です。名前付きだけを結果に残したい場合は、PHP 8.2 以降で使える n 修飾子(NO_AUTO_CAPTURE)を付けると、名前のないカッコはキャプチャされなくなります。

<?php
preg_match('/(?<w>a)(b)/n', 'ab', $m);
print_r($m);   // [0] => ab, [w] => a, [1] => a ((b) は入らない)

preg_match_allですべてのマッチを集める

<?php
$s = '備品A-101 3個、備品B-205 12個';

var_dump(preg_match_all('/([A-Z])-(\d+)/', $s, $m));
echo json_encode($m), "\n";

preg_match_all('/([A-Z])-(\d+)/', $s, $m, PREG_SET_ORDER);
echo json_encode($m), "\n";

実行結果:

int(2)
[["A-101","B-205"],["A","B"],["101","205"]]
[["A-101","A","101"],["B-205","B","205"]]
フラグ $m の並び 向いている使い方
PREG_PATTERN_ORDER(既定) $m[キャプチャ番号][何件目] 「型番だけの配列」が欲しいとき
PREG_SET_ORDER $m[何件目][キャプチャ番号] foreach で1件ずつ処理するとき

マッチが1件もないときの戻り値は int(0) で、$m は [[]](空の配列を1つ持つ配列)になります。preg_match_all の戻り値は件数なので、=== 1 ではなく > 0 や === false で判定します。

preg_replaceで置換する

<?php
// キャプチャした部分を $1 $2 で参照する
echo preg_replace('/(\d{4})-(\d{2})-(\d{2})/', '$1年$2月$3日', '2026-10-03'), "\n";

// 連続する空白(全角含む)を半角スペース1つにする
echo preg_replace('/\s+/u', ' ', "a  b \t c"), "\n";

// 参照の直後に数字を続けるときは ${1} と書く
echo preg_replace('/(\d)/', '${1}0', 'a1b2'), "\n";

// パターンと置換文字列を配列で渡す
echo preg_replace(['/apple/', '/orange/'], ['りんご', 'みかん'], 'apple and orange'), "\n";

// 回数の上限と、置換した件数の取得
echo preg_replace('/\d/', '#', 'a1b2c3', 2, $count), " count=$count\n";

実行結果:

2026年10月03日
a b c
a10b20
りんご and みかん
a#b#c3 count=2
書き方 意味
$0 マッチ全体
$1、\1 1番目のカッコ
${1}0 1番目のカッコの直後に 0 を続ける($10 と区別するため)
第4引数 $limit 置換する最大回数(-1 で無制限)
第5引数 &$count 実際に置換した回数が入る

マッチする箇所がなければ、元の文字列がそのまま返ります(preg_replace('/x/', 'y', 'abc') は "abc")。固定の文字列を置き換えるだけなら、正規表現を使わない str_replace のほうが速く、記号のエスケープも不要です。

preg_replace_callbackで計算しながら置換する

置換後の内容を計算で決めたいときは、コールバック関数を渡します。関数には preg_match の $matches と同じ形の配列が渡されます。

<?php
// 数値を2倍にする
echo preg_replace_callback('/\d+/', fn($m) => $m[0] * 2, '在庫 3 個、予約 12 個'), "\n";

// 名前付きキャプチャと組み合わせる
echo preg_replace_callback(
    '/(?<y>\d{4})-(?<m>\d{2})/',
    fn(array $m) => "{$m['y']}年" . (int)$m['m'] . '月',
    '2026-04'
), "\n";

// スネークケースをキャメルケースにする
echo preg_replace_callback('/_([a-z])/', fn($m) => strtoupper($m[1]), 'user_first_name'), "\n";

// パターンごとに別の関数を使う
echo preg_replace_callback_array([
    '/\d+/'    => fn($m) => "[$m[0]]",
    '/[a-z]+/' => fn($m) => strtoupper($m[0]),
], 'abc 123'), "\n";

実行結果:

在庫 6 個、予約 24 個
2026年4月
userFirstName
ABC [123]

以前あった e 修飾子(置換文字列をコードとして実行する機能)は現在のPHPにはなく、指定すると Warning: preg_match(): Unknown modifier 'e' となって false が返ります。同じことは preg_replace_callback で書けます。

preg_splitで分割する

<?php
// 複数の区切り文字(空白・カンマ・読点・全角空白)で分割
echo json_encode(preg_split('/[\s,、]+/u', 'りんご, みかん、ぶどう もも'), JSON_UNESCAPED_UNICODE), "\n";

// 空の要素を残す/除く
echo json_encode(preg_split('/,/', 'a,,b,')), "\n";
echo json_encode(preg_split('/,/', 'a,,b,', -1, PREG_SPLIT_NO_EMPTY)), "\n";

// 区切り文字も結果に含める
echo json_encode(preg_split('/(\d)/', 'a1b2c', -1, PREG_SPLIT_DELIM_CAPTURE)), "\n";

// 分割数の上限
echo json_encode(preg_split('/,/', 'a,b,c', 2)), "\n";

// 日本語を1文字ずつに分ける
echo json_encode(preg_split('//u', '日本語', -1, PREG_SPLIT_NO_EMPTY), JSON_UNESCAPED_UNICODE), "\n";

実行結果:

["りんご","みかん","ぶどう","もも"]
["a","","b",""]
["a","b"]
["a","1","b","2","c"]
["a","b,c"]
["日","本","語"]
フラグ 働き
PREG_SPLIT_NO_EMPTY 空文字列の要素を取り除く
PREG_SPLIT_DELIM_CAPTURE カッコで囲んだ区切り部分も結果に入れる
PREG_SPLIT_OFFSET_CAPTURE 各要素の開始位置(バイト)も返す

区切り文字が固定の1種類なら explode で十分です。1文字ずつの分割は mb_str_split('日本語') でも同じ結果が得られます。

preg_quoteで記号をエスケープする

ユーザーが入力した検索語をパターンに埋め込むときは、preg_quote を通します。記号が正規表現として解釈されるのを防ぐためです。

<?php
echo preg_quote('1.5+2=3.5 (税込) #1'), "\n";
echo preg_quote('https://example.com/a?b=1', '/'), "\n";

$kw = 'C++ (入門)';
var_dump(preg_match('/' . $kw . '/u', 'C++ (入門) の本'));                    // そのまま埋め込む
var_dump(preg_match('/' . preg_quote($kw, '/') . '/u', 'C++ (入門) の本'));  // エスケープする

実行結果:

1\.5\+2\=3\.5 \(税込\) \#1
https\:\/\/example\.com\/a\?b\=1
int(0)
int(1)

そのまま埋め込んだ場合、エラーにはならず 0(マッチなし)が返りました。+ が繰り返し、( ) がグループとして解釈され、別の意味のパターンになったためです。エラーが出ないので気付きにくい不具合になります。

第2引数にデリミタ(ここでは /)を渡すのを忘れないようにします。省略すると / がエスケープされず、URLなどを埋め込んだときにパターンが途中で終わってしまいます。

PHP 8系での挙動

PHP 8.4.21 で確認した動きをまとめます。

未マッチのキャプチャは空文字列かnullか

省略可能なカッコがマッチしなかったとき、既定では空文字列 "" が入ります。PREG_UNMATCHED_AS_NULL を付けると null になります。

<?php
preg_match('/(a)(b)?(c)/', 'ac', $m);
var_dump($m[2]);                       // string(0) ""

preg_match('/(a)(b)?(c)/', 'ac', $m, PREG_UNMATCHED_AS_NULL);
var_dump($m[2]);                       // NULL

// 末尾のカッコがマッチしなかった場合
preg_match('/(a)(b)?/', 'a', $m);
var_dump(count($m));                   // int(2) … $m[2] は存在しない

preg_match('/(a)(b)?/', 'a', $m, PREG_UNMATCHED_AS_NULL);
var_dump(count($m), $m[2]);            // int(3) NULL
状況 既定 PREG_UNMATCHED_AS_NULL
途中のカッコが未マッチ "" null
末尾のカッコが未マッチ キー自体が無い null(キーがある)

既定の動作では「空文字列にマッチした」と「マッチしなかった」の区別が付かず、末尾ではキーが無くて Undefined array key の警告につながります。 PREG_UNMATCHED_AS_NULL を付けておけば、どの位置でも $m[2] === null や $m[2] ?? '既定値' で統一して書けます。preg_replace_callback でも第6引数に同じフラグを渡せます。

引数の型が厳しくなっている

呼び出し PHP 8.4.21 での結果
preg_match('/a/', null) Deprecated: Passing null to parameter #2 ($subject) of type string is deprecated
preg_match('/a/', []) TypeError: preg_match(): Argument #2 ($subject) must be of type string, array given
preg_match('', 'a') Warning: preg_match(): Empty regular expression、戻り値 false
preg_match('abc', 'a') Warning: preg_match(): Delimiter must not be alphanumeric, backslash, or NUL byte、戻り値 false

フォームの値が未入力で null になり得るときは、preg_match($p, $value ?? '') のように文字列にしてから渡します。

よく使う修飾子

修飾子 働き 例と実行結果
i 大文字小文字を区別しない /php/i と PHP → 1
u パターンと対象をUTF-8として扱う /^.{3}$/u と 日本語 → 1
m ^ $ が各行の先頭・末尾にマッチ /^b/m と "a\nb" → 1
s . が改行にもマッチ /a.b/s と "a\nb" → 1
x パターン中の空白を無視(コメントを書ける) /a b/x と ab → 1
D $ が末尾の改行の前にマッチしなくなる /^\d{3}$/D と "123\n" → 0
U 繰り返しを最短マッチにする /<.+>/U と <a><b> → <a>
n 名前のないカッコをキャプチャしない(PHP 8.2以降) /(?<w>a)(b)/n → w だけ入る

最短マッチは、修飾子 U より +? や *? と個別に書くほうが、どこが最短なのかが読み取りやすくなります(/<.+?>/ も <a> を返しました)。

つまずきやすいポイントのチェックリスト

確認すること こう書くとよい
戻り値を ! や == で判定していないか === 1、=== 0、=== false で書き分ける
日本語を扱うのに u を付け忘れていないか パターンの末尾に u
u を付けたうえで \d に全角数字を通していないか 半角だけなら [0-9]
入力チェックに ^...$ を使っていないか \A...\z
ユーザー入力をそのままパターンに連結していないか preg_quote($kw, '/')
preg_replace の結果をそのまま上書きしていないか null のときの扱いを決める
省略可能なカッコの結果を $m[2] で直接読んでいないか PREG_UNMATCHED_AS_NULL と ??
エラーの原因を確認できるようにしているか 直後に preg_last_error_msg()

よくある質問

Q. preg_matchの戻り値が0のときとfalseのときは、どう違いますか? A. 0は「調べた結果、マッチしなかった」、falseは「エラーで調べられなかった」です。パターンの文法誤り、u 修飾子つきで不正なUTF-8を渡した場合、バックトラックの上限超過などで false になります。=== false で分けて、preg_last_error_msg() で原因を確認します。

Q. 日本語を含む文字列では、必ずu修飾子が必要ですか? A. 対象やパターンに日本語が含まれるなら付けておくのが確実です。付けないと . や文字クラスがバイト単位で動き、今回の実行でも [あ] が「い」にマッチし、置換結果が文字化けしました。なお u を付けると不正なUTF-8で false が返るようになるため、戻り値の確認もセットで行います。

Q. preg_matchとstr_containsはどう使い分けますか? A. 決まった文字列が含まれるかを調べるだけなら str_contains(PHP 8.0以降)や str_starts_with が簡潔で高速です。「数字が3桁続く」「どちらかの語を含む」のようにパターンで表す必要があるときに preg_match を使います。

Q. mb_ereg系の関数とpreg系はどちらを使えばよいですか? A. 文字コードがUTF-8であれば、u 修飾子を付けた preg 系で日本語を正しく扱えます。preg 系は関数や修飾子が豊富で、名前付きキャプチャやコールバック置換も使えるため、新しく書くコードでは preg 系を軸にすると情報も探しやすくなります。

まとめ

  • preg_match は「マッチ1/マッチなし0/エラーfalse」を返す。判定は === 1 などの厳密比較で書く
  • !preg_match(...) はエラー時にも真になる。不正なUTF-8を渡すと入力チェックをすり抜けることを実行結果で確認した
  • 日本語には u 修飾子。付けないと . や [あ] がバイト単位になり、誤マッチや文字化けが起きる
  • u を付けると \d は全角数字にもマッチする。半角だけなら [0-9]、入力チェックの両端は \A と \z
  • 置換は preg_replace、計算が必要なら preg_replace_callback、分割は preg_split、ユーザー入力の埋め込みには preg_quote($s, '/')
  • 名前付きキャプチャ (?<name>...) と PREG_UNMATCHED_AS_NULL で、結果の取り出しが読みやすく安全になる
  • 原因がわからないときは preg_last_error_msg() を直後に呼ぶ

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「PHP preg_matchの使い方|preg_replaceと日本語対応」

株式会社RJCでは、PHPの正規表現や文字列処理の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?