正規表現の基本と頻出パターン 20 選

コードエディタに正規表現パターンが表示されている Photo: Unsplash

ログからメールアドレスだけ抽出したい。フォームの電話番号バリデーションを書きたい。CSV の特定列を切り出したい。正規表現は毎日のように必要なのに、いざ書こうとすると「あのメタ文字何だっけ」と検索し直してしまう——そんな経験はないでしょうか。本記事では、現場で頻出する基本構文 8 つ・文字クラスのショートハンド・フラグの意味を整理したうえで、コピペで動く頻出パターン 20 選をひとつずつ丁寧に解説します。パターンごとに「何にマッチするか/何にマッチしないか」の具体例と、実務でよくある落とし穴もセットで掲載しました。「グリーディ vs レイジー」や「破滅的バックトラック」など、少し踏み込んだトピックも後半に収録しています。手元でパターンを試しながら読むと理解が格段に速まります。ベンリーの正規表現チェッカーをサイドバイサイドで開いておくと便利です。

正規表現の歴史

正規表現の理論は 1956 年に数学者 Stephen Kleene が「正規言語」として定式化したのが起源です。実装として広く使われるようになったのは 1968 年、Ken Thompson が Unix の qed エディタに正規表現エンジンを組み込んでから。その後 grep(1973)や sedawk に引き継がれました。1987 年に Perl が後方参照・非貪欲マッチ・名前付きキャプチャなどを大幅に拡張した「Perl 正規表現」を導入し、これが事実上の業界標準になります。この方言を PCRE(Perl Compatible Regular Expressions)と呼び、PHP・Python・Ruby など多くの言語が採用しています。ECMAScript(JavaScript)も PCRE に近い構文を持ちますが細部で差異があります。POSIX 標準には BRE(基本正規表現)と ERE(拡張正規表現)があり、Unix コマンドラインツールで今でも使われています。

基本構文 8 つ

以下の 8 つを覚えるだけで、日常の正規表現の大部分は書けます。各項目に「何にマッチするか」の具体例を添えました。

. — 任意の 1 文字(改行を除く)
パターン c.tcatcutc3t にマッチします。c\nt(c と t の間に改行)にはデフォルトでマッチしません。改行も含めるには s フラグ(後述)を使います。
* — 直前の要素を 0 回以上繰り返し
パターン go*glegglegoglegooglegoooooogle のすべてにマッチします。「0 回以上」なので、対象の文字が 1 つもなくてもマッチします。
+ — 直前の要素を 1 回以上繰り返し
パターン go+glegoglegoogle にマッチしますが、ggle(o が 0 回)にはマッチしません。「1 回以上」という制約が * との違いです。
? — 直前の要素を 0 回または 1 回
パターン colou?rcolor(米式)と colour(英式)の両方にマッチします。? は「あってもなくてもよい」ものを表すのに便利です。繰り返し量指定子(*/+/{n,})の直後に ? を付けると「レイジー(非貪欲)」の意味に変わります(後述)。
^ — 行頭(または文字列の先頭)
パターン ^Error は行の先頭が Error で始まる行にのみマッチします。文字列中の Error にはマッチしません。m フラグを付けると各行の先頭に対してマッチします。
$ — 行末(または文字列の末尾)
パターン \.png$ はファイル名が .png で終わる文字列にマッチします。m フラグ付きでは各行の末尾に対してマッチします。
[abc] — 文字クラス:a、b、c のいずれか 1 文字
パターン [aeiou] は母音 1 文字にマッチします。[a-z] のように範囲指定も使えます。[^abc] と先頭に ^ を付けると「a・b・c 以外の 1 文字」という否定クラスになります。
(abc) — キャプチャグループ
パターン (foo)+foofoofoofoofoofoo にマッチし、最後の foo をグループ 1 としてキャプチャします。置換処理では $1(JavaScript/PCRE)や \1(sed/Vim)でキャプチャした内容を参照できます。キャプチャを必要としない場合は (?:abc)(非キャプチャグループ)を使うとパフォーマンスがわずかに改善します。

文字クラスのショートハンド(\d \w \s \b)

よく使う文字クラスはショートハンドで短く書けます。大文字は各ショートハンドの「否定」です。

\d — 数字 1 文字([0-9] と同等)
JavaScript・PCRE では Unicode モードでない限り ASCII 数字のみ。Python の re モジュールはデフォルトで ASCII モードですが、re.UNICODE フラグを付けると全角数字(0〜9)にもマッチします。\D は数字以外の 1 文字。
\w — 単語構成文字([A-Za-z0-9_] と同等)
英数字とアンダースコアのみ。日本語文字にはマッチしません(後述の「落とし穴」参照)。\W は単語構成文字以外の 1 文字。
\s — 空白文字
半角スペース(U+0020)・タブ(\t)・改行(\n)・キャリッジリターン(\r)・フォームフィード(\f)・垂直タブ(\v)を含みます。全角スペース(U+3000)は含まれません。\S は空白以外の 1 文字。
\b — 単語境界(ゼロ幅アサーション)
文字を消費せずに「単語の境界」という位置にマッチします。例えば \bcat\bcat(単独の単語)にはマッチしますが、catalogtomcat にはマッチしません。\B は単語境界以外の位置。ログ中の特定のキーワードだけを抽出したいときに欠かせません。ただし日本語には単語境界の概念がないため、\b は日本語テキストでほぼ機能しません。

フラグ(g / i / m / s / u / y)の意味

正規表現の末尾に付けるフラグで、マッチの挙動を大きく変えられます。JavaScript では /pattern/flags の形式で、Python では re.compile(pattern, re.FLAG) で指定します。

g(global)— 全体マッチ
フラグなしだと最初のマッチだけを返します。g フラグを付けると全マッチを返します。JavaScript の String.replace()matchAll()replaceAll() では g フラグが必須です。Python の re.findall() は常に全マッチを返すため、g フラグ相当の概念はありません。
i(insensitive)— 大文字小文字を無視
/hello/ihelloHelloHELLO のすべてにマッチします。Python では re.IGNORECASE または re.I。Unicode モードと組み合わせると、アクセント付き文字(ä/Ä など)も同一視されます。
m(multiline)— 複数行モード
^$ が文字列全体の先頭・末尾だけでなく、各行の先頭・末尾にもマッチするようになります。複数行のログを一度にパースするときに必須。Python では re.MULTILINE
s(dotall)— ドットが改行にもマッチ
デフォルトで . は改行文字(\n)にマッチしません。s フラグを付けると改行を含む任意の文字にマッチします。Python では re.DOTALL。JavaScript では ES2018 以降で利用可能。
u(unicode)— Unicode モード
サロゲートペア(絵文字など)を 1 文字として正しく扱います。JavaScript で u フラグなしだと、絵文字 1 文字が 2 つのコードユニットとして扱われ、. が絵文字 1 文字にマッチしないことがあります。現代の JS 正規表現では u フラグを常に付けることが推奨されています。
y(sticky)— 粘着モード
正規表現の lastIndex が指す位置からのみマッチします。パーサやトークナイザを手書きするときに、文字列を逐次スキャンする用途で使います。g フラグとの違いは「前回のマッチ終了位置から再開する」点が同じですが、y はその位置でしかマッチしません。

頻出パターン 20 選

各パターンにはマッチ例・非マッチ例・実務での注意点を添えました。パターンをそのままコピーして使えますが、用途によって適宜調整してください。

No. 01

メールアドレス(簡易版)

^[\w._%+-]+@[\w.-]+\.[A-Za-z]{2,}$

マッチ例: user@example.comfirst.last+tag@sub.domain.co.jpabc123@my-company.org

非マッチ例: @example.com(ローカル部なし)、user@(ドメインなし)、user@.com(ドメイン名なし)

正規表現でメールアドレスを完全に検証することは現実的ではありません。RFC 5322 に準拠したパターンは数百文字に膨れ上がり、それでもすべての有効アドレスをカバーできません。実務では「@ が 1 つあり、TLD が 2 文字以上」という簡易チェックで十分なケースが大半です。最終的な正当性は確認メールの送受信で担保しましょう。なお \w は ASCII 英数字とアンダースコアのみなので、国際化ドメイン名(IDN)や Unicode アドレスには対応していません。

現場のヒント

HTML の <input type="email"> はブラウザ組み込みの検証を持っており、多くの場合これだけで十分です。正規表現バリデーションはサーバーサイドの二重チェックとして使うのが定石です。

No. 02

URL(http/https)

https?://[\w./?=&%#@:,;!~*'()\[\]+-]+

マッチ例: https://example.comhttp://api.example.com/v1/users?page=2&limit=10https://sub.domain.co.jp/path/to/page

非マッチ例: ftp://files.example.com(ftp は含まない)、//example.com(プロトコル相対 URL)

URL の完全な仕様(RFC 3986)を正規表現で再現するのは難しく、特にクエリパラメータやフラグメントに使える文字の範囲は広大です。テキストからリンクを抽出するユースケースでは上のパターンで概ね対応できますが、バリデーション用途では URL コンストラクタ(JavaScript)や urllib.parse(Python)などの専用パーサを使う方が確実です。URL 末尾の句読点(ピリオドや括弧)が意図せず含まれることがあるため、トリムしてから使うことを推奨します。

No. 03

日本の郵便番号

^\d{3}-?\d{4}$

マッチ例: 100-0001(ハイフンあり)、1000001(ハイフンなし)

非マッチ例: 1234567(7桁だがハイフンなし位置が正しくない……ように見えてこれはマッチします)、12-3456(前半 2 桁のみ)、1234-567(ハイフン位置が逆)

日本の郵便番号は「3 桁 + ハイフン + 4 桁」の計 7 桁(ハイフンを除く)です。-? でハイフンをオプションにしているため、入力フォームでユーザーがハイフンを省略しても受け付けられます。実在する郵便番号かどうかの確認(例: 存在しない番号を弾く)は正規表現では不可能なため、郵便番号 API と組み合わせるのが実務的なアプローチです。

No. 04

日本の電話番号(固定/携帯)

^0\d{1,4}-?\d{1,4}-?\d{4}$

マッチ例: 03-1234-5678(東京固定)、090-1234-5678(携帯)、0120-123-456(フリーダイヤル)、09012345678(ハイフンなし)

非マッチ例: 1234-5678(先頭の 0 なし)、090-12-345678(桁数が不自然)

日本の電話番号は市外局番の桁数が地域によって異なる(東京 03、地方は 0123 など 4 桁)ため、固定長パターンでは対応しきれません。このパターンはゆるめの検証で「それっぽい日本の電話番号か」を判断する用途に適しています。IP 電話(050 始まり)やフリーダイヤル(0120/0800)にも対応しています。国際プレフィックス(+81)を含む形式は別途対応が必要です。

No. 05

ISO 8601 日付(YYYY-MM-DD)

^\d{4}-\d{2}-\d{2}$

マッチ例: 2026-04-142000-01-011999-12-31

非マッチ例: 26-04-14(年が 2 桁)、2026/04/14(スラッシュ区切り)、20260414(区切りなし)

このパターンは形式のチェックのみで、値の妥当性(例: 2026-13-99 のような存在しない日付)は検証しません。日付の妥当性まで確認するには、マッチ後に言語のDate/dateutil ライブラリでパースして例外をキャッチするのが確実です。タイムゾーン付きの ISO 8601(例: 2026-04-14T10:00:00+09:00)を扱う場合は別のパターンが必要です。

No. 06

IPv4 アドレス(簡易版)

^(?:\d{1,3}\.){3}\d{1,3}$

マッチ例: 192.168.1.110.0.0.1255.255.255.0

非マッチ例: 256.1.1.1(数値 255 超……だがこのパターンではマッチしてしまう)、192.168.1(オクテットが 3 つ)

このパターンは各オクテットが 1〜3 桁の数字であることしか確認しません。256.999.999.999 のような無効な IP にもマッチしてしまいます。厳密に 0〜255 を検証するには各オクテットを (?:25[0-5]|2[0-4]\d|[01]?\d\d?) のように書く必要があります(可読性が大きく下がります)。ログフィルタや検索用途なら簡易版で十分。ネットワーク機器設定やセキュリティ用途では厳密版を使いましょう。

No. 07

16 進カラーコード

^#(?:[0-9a-fA-F]{3}){1,2}$

マッチ例: #fff(3 桁省略形)、#ffffff(6 桁)、#1A2B3C(大文字混在)

非マッチ例: #ffff(4 桁)、#gggggg(16進以外の文字)、ffffff(# なし)

CSS では #RRGGBB(6桁)と #RGB(3桁省略形)が広く使われます。CSS Colors Level 4 で追加された 4 桁(#RGBA)・8 桁(#RRGGBBAA)のアルファチャンネル付き表記が必要な場合は {1,2}{1,4}(?:(?<=\#.{3})(?=.{0}$)|(?<=\#.{6})(?=.{0}$))... のように複雑になるため、正規表現ではなくライブラリで処理することを検討してください。i フラグを付けると大文字小文字を統一できます。

No. 08

半角英数字のみ

^[A-Za-z0-9]+$

マッチ例: hello123ABCa1b2c3

非マッチ例: hello world(スペースあり)、café(アクセント付き文字)、hello_world(アンダースコア)

ユーザー名やパスワードのポリシーチェック、API キーの形式確認などで頻繁に使います。アンダースコアを許容するなら [A-Za-z0-9_]+\w+ と同等)、ハイフンも許容するなら [A-Za-z0-9_-]+ と追加します。大文字小文字を無視する場合は /^[a-z0-9]+$/ii フラグで簡略化できます。

No. 09

ひらがなのみ

^[ぁ-ん]+$

マッチ例: ひらがなあいうえお(小文字ひらがな)

非マッチ例: ひらがナ(カタカナ混在)、hiragana(ローマ字)、漢字

Unicode のひらがなブロックは U+3041(ぁ)〜 U+3096(ゖ)です。[ぁ-ん] は実用上問題ありませんが、厳密には [ぁ-ゖ] で「ゕ」「ゖ」(小書き)も含めた全ひらがなをカバーできます。振り仮名入力フォームのバリデーションや、読み仮名フィールドの検証によく使われます。長音符「ー」はひらがなではなくカタカナブロックに含まれる点に注意。

No. 10

カタカナのみ(全角)

^[ァ-ヶー]+$

マッチ例: カタカナアイウエオコーヒー(長音符を含む)

非マッチ例: カタカナ(半角カタカナ)、katakana(ローマ字)、かたかな(ひらがな)

半角カタカナ(U+FF65〜U+FF9F)も許容する場合は [ァ-ヶーヲ-゚]+ のように半角カタカナのレンジを追加します。全角カタカナのみを受け付けるフォームで半角カタカナが入力される場合はバリデーション前に String.normalize('NFKC')(JavaScript)や unicodedata.normalize('NFKC', s)(Python)で正規化するのが定石です。「ヴ」(U+30F4)は ァ-ヶ のレンジ内に含まれています。

No. 11

漢字のみ

^[一-龯々]+$

マッチ例: 漢字日本語(同の字点)

非マッチ例: 漢字ひらがな(ひらがな混在)、漢字A(ローマ字混在)、𠮷野家(JIS X 0208 外の漢字)

Unicode の CJK 統合漢字ブロック(U+4E00〜U+9FFF)をカバーします。(U+3005・繰り返し記号)を加えているのは「刻々」のような単語に対応するためです。注意点として、U+20000 以降の CJK 統合漢字拡張(𠮷 など JIS 第三水準以降の文字)はこのレンジに含まれません。「𠮷野家」の「𠮷」(U+20BB7)は拡張漢字なのでマッチしません。氏名フィールドで拡張漢字も考慮するなら Unicode カテゴリを使える環境(Python の \p{Han}、Perl、Java)を検討してください。

No. 12

改行で分割された行

[^\n]+

マッチ例: テキスト apple\nbanana\ncherry から applebananacherry の 3 つにマッチ(g フラグ使用時)

非マッチ例: 空行(改行のみの行)にはマッチしません

「改行以外の文字が 1 つ以上続く」というパターンで、実質的に空行を除いた各行を抽出します。m フラグと組み合わせて ^.+$/m でも同じ結果を得られます。空行も含めて行単位で処理したい場合は、split('\n') などの文字列分割関数の方がシンプルです。Windows 形式の改行(CRLF、\r\n)を扱う場合は [^\r\n]+ にしてキャリッジリターンも除外しましょう。

No. 13

重複する空白を 1 つに

\s+

使い方: "hello world".replace(/\s+/g, ' ')"hello world"

マッチ例: 連続するスペース、タブ、改行のすべてにマッチします

これは単体でマッチするというより「置換」と組み合わせて使うパターンです。スクレイピングで取得した HTML テキストや、ユーザー入力のクリーニングに使います。\s+ は改行もマッチするため、段落をまたいで空白が潰れることに注意。改行を保持したいなら [ \t]+(スペースとタブのみ)を使いましょう。

No. 14

HTML タグの除去(簡易)

<[^>]+>

マッチ例: <p><a href="url"></div><br />

非マッチ例: テキストノード(タグに囲まれていない文字列そのもの)

HTML の正規表現パースには根本的な限界があります(後述の FAQ 参照)。ただし「プレーンテキスト抽出のためにタグを除去する」という用途に限れば、このシンプルなパターンで大抵のケースに対応できます。replace(/<[^>]+>/g, '') で HTML をストリップしてテキストだけ残せます。<script><style> タグの中身ごと除去したい場合は別のパターンが必要です。信頼できないソースの HTML をこの方法で「サニタイズ」するのは XSS の温床になるため、必ず DOMParser や専用ライブラリを使ってください。

セキュリティ注意

正規表現による HTML 処理をサニタイズ目的に使わないでください。<scri pt> のような意図的なスペース挿入など、攻撃者はフィルタを回避する無数の手法を持っています。XSS 対策には DOMPurify(ブラウザ)や sanitize-html(Node.js)などの専用ライブラリを使ってください。

No. 15

ファイル名から拡張子を取る

\.([^.]+)$

マッチ例: image.png → キャプチャグループ 1 = pngarchive.tar.gzgz(最後の拡張子のみ)

非マッチ例: Makefile(拡張子なし)、.gitignore(ドットで始まるが拡張子なし)

\.([^.]+)$ は「最後のドット以降を末尾まで」キャプチャします。archive.tar.gz なら gz のみが取れます(tar.gz 全体を取るには別の対応が必要)。実務では多くの言語・環境にパス操作のユーティリティが用意されています(Node.js の path.extname()、Python の pathlib.Path.suffix など)。正規表現を使うのは、これらが利用できない文字列処理の文脈に限定する方がバグを減らせます。

No. 16

文字列の前後の空白を取る(trim)

^\s+|\s+$

使い方: " hello world ".replace(/^\s+|\s+$/g, '')"hello world"

|(OR)を使って「先頭の空白」または「末尾の空白」にマッチさせ、空文字列に置換します。現代の JavaScript には String.trim()trimStart()trimEnd() が標準搭載されており、trim 目的だけなら正規表現より .trim() を使う方がシンプルで高速です。正規表現が活きるのは、trim を他の置換処理とまとめて行うか、全角スペース(U+3000)も除去したい場合です。全角スペースも除去するには ^[\s\u3000]+|[\s\u3000]+$ とします。

No. 17

CSV 行(簡易)

([^,]+)(?:,|$)

マッチ例: apple,banana,cherry から applebananacherry を順番に抽出

非マッチ例(処理できないケース): "field,with,comma"(ダブルクォートで囲まれたフィールド内のカンマ)、"line\nbreak"(フィールド内の改行)

RFC 4180 に準拠した CSV はフィールドをダブルクォートで囲むことができ、その中ではカンマや改行が使えます。このシンプルなパターンはそれらに対応していません。実務では csv(Python)や papaparse(JavaScript)などの専用パーサを使うことを強く推奨します。単純な「カンマ区切りで囲み文字なし」形式のログや設定ファイルの解析には使えます。

No. 18

UUID v4

^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$

マッチ例: 550e8400-e29b-41d4-a716-446655440000f47ac10b-58cc-4372-a567-0e02b2c3d479

非マッチ例: 550e8400-e29b-11d4-a716-446655440000(v1 UUID: 3 番目のグループが 1xxx)、xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx(x はマッチしない)

UUID v4 の仕様(RFC 4122)では 3 番目のグループの先頭が 4(バージョン番号)、4 番目のグループの先頭が 89ab(バリアント識別子)と決まっています。このパターンはそれを正確に反映しています。大文字混在(550E8400-...)を許容するなら末尾に i フラグを付けるか、[0-9a-fA-F] と書きます。データベースで UUID を扱う場合、DB 側の UUID 型への変換前にこのパターンで形式チェックしておくと不正データの混入を防げます。

No. 19

クレジットカード番号(簡易・空白とハイフン許容)

^(?:\d[ -]?){13,19}$

マッチ例: 4111111111111111(Visa 16 桁)、4111 1111 1111 1111(スペース区切り)、4111-1111-1111-1111(ハイフン区切り)

非マッチ例: 1234567890(10 桁、短すぎる)、41111111111111111111111(23 桁、長すぎる)

クレジットカード番号の桁数はカードブランドによって異なります(Visa/Mastercard: 16 桁、American Express: 15 桁、Diners Club: 14 桁)。このパターンは 13〜19 桁の数字(空白・ハイフン区切り可)を受け付けます。有効なカード番号かどうかの検証には Luhn アルゴリズムを実装する必要があります(正規表現では不可能)。本番環境のカード情報処理には必ず PCI DSS 準拠の決済サービス(Stripe、Square 等)を使ってください。正規表現によるバリデーションは UI の入力補助に留めます。

No. 20

ハッシュタグ

(?:^|\s)#([\w\u3040-\u30ff\u4e00-\u9faf]+)

マッチ例: #JavaScript(英語)、#日本語タグ(日本語)、テキスト #タグ 続き → キャプチャ: タグ

非マッチ例: example@domain.com#(前の文字が空白でない)、# スペースあり(# 直後にスペース)

Twitter/X や Instagram 風のハッシュタグ抽出パターンです。(?:^|\s) で「行頭か空白の後」という条件を付けることで、URL のアンカー(example.com/path#section)などをタグとして誤検出しません。Unicode レンジ \u3040-\u30ff でひらがな・カタカナを、\u4e00-\u9faf で漢字を許容しています。キャプチャグループ 1 に # を除いたタグ本体が入ります。g フラグと組み合わせて matchAll() すると複数タグを一括抽出できます。

グリーディ vs レイジー(欲張り vs 怠惰)

正規表現の量指定子(*+?{n,})はデフォルトでグリーディ(欲張り)です。できる限り長くマッチしようとします。量指定子の直後に ? を付けるとレイジー(怠惰)になり、できる限り短くマッチします。

具体例で確認しましょう。テキスト <b>太字</b> と <i>斜体</i> から各タグを抽出したいとします。

グリーディ(意図しない結果)
<.*>

マッチ: <b>太字</b> と <i>斜体</i> — 全体が 1 つのマッチになってしまう

レイジー(意図通りの結果)
<.*?>

マッチ: <b></b><i></i> — 各タグを個別に抽出

グリーディの <.*> が最初の < から最後の > まで貪欲に飲み込んでしまうのに対し、レイジーの <.*?> は最初の > が現れた時点でマッチを終了します。

レイジーが活きるもう一つの例として、JavaScript のテンプレートリテラル内のプレースホルダー抽出があります。テキスト ${name} は ${age} 歳 から各プレースホルダーを取るには \$\{.*?\}(レイジー)を使います。グリーディの \$\{.*\} では ${name} は ${age} 歳 全体がマッチしてしまいます。

パフォーマンスの注意点: レイジー量指定子はグリーディより遅くなる場合があります。それぞれ異なるバックトラックパターンを持つためです。特定のケースでは否定文字クラスを使った方が高速です。例えば <.*?> の代わりに <[^>]*>> 以外の文字を 0 回以上)と書くと、バックトラックが発生せず高速になります。

よくある落とし穴

1. メタ文字のエスケープ忘れ

.*+?()[]{}^$|\ は正規表現のメタ文字です。これらを文字リテラルとしてマッチさせたいときはバックスラッシュでエスケープが必要です。

例えばファイル名の report.csv にマッチさせるつもりで report.csv と書くと、. が「任意の 1 文字」として働き、reportXcsv にもマッチしてしまいます。正しくは report\.csv とドットをエスケープします。

2. ドットが改行にマッチしない問題

複数行にまたがるテキストを .* でまとめて取ろうとして失敗するケースです。デフォルトでは . が改行(\n)にマッチしないため、<div>.*</div><div></div> が同じ行にあるときしかマッチしません。解決策は s フラグ(dotall モード)を使うことです。JavaScript では /<div>.*<\/div>/s、Python では re.DOTALL フラグで有効になります。

3. 日本語環境での \w の罠

\w[A-Za-z0-9_] と同等であり、日本語文字にはマッチしません。「単語文字」という名前から「日本語も含む」と誤解されがちですが、PCRE・JavaScript の標準動作では ASCII のみです。日本語を含む「単語」にマッチさせたいなら、Unicode プロパティエスケープをサポートする環境(JavaScript の u フラグ付き \p{L}、Python の re.UNICODE\w)か、明示的な Unicode レンジ([ぁ-ん] 等)を組み合わせてください。Python は re.UNICODE がデフォルトで有効なため \w が日本語にもマッチしますが、JavaScript の u フラグなしでは ASCII のみです。

4. 後方参照の \1 vs 置換文字列の $1

キャプチャグループの参照記法は、パターン内で使う後方参照置換文字列で使う参照で記法が異なります。パターン内では \1(例: (\w+)\s+\1 で「同じ単語の繰り返し」にマッチ)、JavaScript の replace() の置換文字列では $1、Python の re.sub() では \1 を使います。処理系によって混在するため、移植時に必ず確認しましょう。また名前付きキャプチャグループの場合、パターン内は \k<name>(JavaScript)、置換文字列は $<name>(JavaScript)や \g<name>(Python)と記法がさらに分かれます。

パフォーマンス:破滅的バックトラック

正規表現エンジンの多く(PCRE、JavaScript の V8 など)はバックトラッキング型の NFA(非決定性有限オートマトン)です。通常は問題ないのですが、パターンの組み方によっては入力が長くなるにつれてマッチ時間が指数的に増加する「破滅的バックトラック(catastrophic backtracking)」が起きます。

典型的なパターンは ネストされた繰り返し です。

危険なパターン(指数的に遅くなる)
(a+)+b

入力: aaaaaaaaaaaaaaaaac(末尾が b でない)に対して、エンジンは全ての可能な組み合わせを試し尽くして失敗するため、a の個数 n に対して O(2ⁿ) の時間がかかります。

安全なパターン(線形時間)
a+b

同じ意味を持つより単純なパターン。ネストを排除することでバックトラックが線形になります。

同様の問題が起きる他のパターン例:(a*)*(\w|\d)+\w\d が含まれるため重複)。

回避策:

  • 繰り返しのネストを避け、可能な限りフラット化する
  • アトミックグループ(?>、PCRE/Java でサポート)または所有格量指定子(a++、PCRE/Java)でバックトラックを禁止する
  • JavaScript・Python で長い入力を検証する場合は re2(線形時間保証の正規表現エンジン)の利用を検討する
  • ユーザー入力を正規表現に直接渡すコードでは必ずタイムアウトを設ける

外部入力(フォームデータ、APIレスポンスなど)に対して複雑なパターンを適用する場合は、ReDoS(Regular Expression Denial of Service)攻撃のリスクを念頭に置いてください。

よくある質問

JavaScript の正規表現と PCRE はどう違いますか?

JavaScript の正規表現(ECMAScript 仕様)と PCRE(PHP・Python・Ruby などが採用)は多くの構文を共有していますが、いくつか重要な差異があります。主な違いは、後読みアサーションの扱い(JavaScript ES2018 以前は後読みを持たなかった。現在は可変長後読みも対応)、所有格量指定子(a++)とアトミックグループ(?>の非サポート(JavaScript には未実装)、そして Unicode プロパティエスケープ(\p{L}(JavaScript は u フラグ付きで ES2018 から対応、PCRE は /u オプションで対応)です。POSIX 正規表現(grep/sed)はさらに別物で、\d のショートハンドを持たず BRE と ERE で構文が分かれています。

\d は日本の全角数字(0〜9)にマッチしますか?

処理系によって異なります。JavaScript では u フラグなしの \d は ASCII 数字(0〜9)のみにマッチし、全角数字(0〜9、U+FF10〜U+FF19)にはマッチしません。Pythonre モジュールは Python 3 以降、re.UNICODE(デフォルトで有効)のときに \d が Unicode の数字カテゴリ全体にマッチするため、全角数字や他言語の数字にもマッチします。実務では正規化(全角→半角変換)してからバリデーションする方が一貫性が高くなります。

正規表現が遅すぎるときの対策は?

まず「どのパターンが遅いか」を特定してください。次の観点で見直します。①破滅的バックトラックの排除:ネストされた繰り返し((a+)+ など)がないか確認。②文字クラスの簡略化(\w|\d)+\w\d を含むため冗長。\w+ に簡略化できます。③アンカーの追加^$ を付けることでエンジンが探索範囲を絞れます。④より速いエンジンへの移行:Node.js では re2 パッケージ(Google の RE2 エンジン)が線形時間を保証します。ユーザー入力由来の正規表現を動的に実行する場合は必ずタイムアウトを実装してください。

正規表現で HTML/XML をパースしてはいけないと聞きましたが本当ですか?

本当です。HTML と XML は正規言語ではなく文脈自由言語であるため、理論的に正規表現では完全にパースできません。具体的な問題として、①入れ子のタグ(<div><p>...</p></div>)の対応、②属性値内の >、③コメントや CDATA セクション、④自己終了タグの正しい処理——これらを正規表現で全て正確に扱うことは不可能です。ただし「特定の単純なパターンを抽出する」用途に限れば実用上問題ないケースもあります(このガイドの HTML タグ除去パターンはその例)。本格的な HTML パースには JavaScript の DOMParser、Python の BeautifulSoup、Node.js の cheerio などを使ってください。

名前付きキャプチャグループはどう使いますか?

名前付きキャプチャグループは番号の代わりに名前でグループを参照できる機能です。JavaScript では (?<name>pattern)、Python では (?P<name>pattern) と書きます。例えば日付のパース: (?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2}) とすると、JavaScript では match.groups.yearmatch.groups.monthmatch.groups.day で各部分にアクセスできます。番号で管理するより読みやすく、グループの順序が変わっても名前でアクセスできるため保守性が上がります。置換時は JavaScript で $<name>、Python で \g<name> を使います。

複数行にまたがるテキストをマッチさせるにはどうすればよいですか?

2 つのフラグが関係します。まず m(multiline)フラグは ^$ の意味を「各行の先頭・末尾」に変えるだけで、. の動作は変わりません。複数行にまたがって . でマッチさせるには s(dotall)フラグが必要です。例えば <div> から </div> までを複数行にまたがって取るには /<div>[\s\S]*?<\/div>/[\s\S] で改行を含む任意文字を表現、レイジーで最短マッチ)か /<div>.*?<\/div>/ss フラグ使用)を使います。[\s\S]s フラグをサポートしない古い環境でのワークアラウンドとしてよく使われます。

正規表現を実際に試してみましょう

この記事で紹介したパターンは、ベンリーの正規表現チェッカーで今すぐ試せます。パターンを貼り付けてテスト文字列を入力するだけで、マッチ結果をリアルタイムで確認できます。

正規表現チェッカーを使う