正規表現がいつも本番で抜ける人へ:Claude Codeで「落としたい例」から組む書き方
正規表現が本番で抜ける原因は許可と拒否が曖昧なまま書くこと。Claude Codeでメール・電話・ログ抽出を、失敗例とテスト付きで安全に組む手順を実コードで。
「メールのバリデーション、regexでサッと書いといて」
そう頼まれて10分で書いた正規表現が、自分のテストでは全部通った。[email protected] もOK、空文字もちゃんと弾く。よし完璧、とリリースしました。
3日後、サポートから連絡が来ます。「[email protected] で登録できないと苦情が」。会計用にプラス記号でメールを振り分けている、わりとよくいるユーザーでした。僕のregexは + を許可していなかったんです。
正規表現で抜けが出る原因は、記号を覚えていないことじゃありません。「何を通して、何を落とすか」を決めないまま書き始めることです。自分の頭にある2、3個の例だけ見て書くと、本番の入力で必ず穴が出ます。
この記事では、Claude Codeを使って正規表現を組み立てる流れを、僕が実際にやっている手順で書きます。コツはひとつ。AIに記号を丸投げするんじゃなく、通したい例と落としたい例を先に渡すこと。正規表現とは、文字列の形を条件で書く小さな言語のことです。最初にそれだけ押さえれば大丈夫です。
この記事の要点
- 正規表現は「通したい例」より**「落としたい例」を3つ以上**先に決めると、本番での抜けが激減する
- Claude Codeには「完全な仕様に準拠して」ではなく、具体的な許可例・拒否例とテストをセットで渡す
- 取り出す目的があるグループは名前付きキャプチャ(
(?<requestId>...))にすると、後から正規表現を直しても列がずれない - 検証用と検索用は分ける。
gフラグ付きを使い回すと内部状態で事故る - 最大の地雷は壊滅的バックトラッキング(ReDoS)。曖昧な繰り返しの入れ子は短い入力でも処理が固まる
なぜ「落としたい例」から始めるのか
正規表現を書くとき、ほとんどの人は通したい例から考えます。[email protected] が通ればOK、と。でもこれが穴の入り口です。
通したい例だけ渡すと、AIも人間も広すぎる正規表現を作ってしまいます。.+@.+ でも [email protected] は通りますよね。でもこれは @@@@@@ すら通します。通る例だけ見ていると、緩すぎることに気づけないんです。
だから僕は順番を逆にします。先に「これは絶対に落としたい」を並べる。メールなら、[email protected](ドメインがおかしい)、[email protected](ドットが連続)、@example.com(ローカル部がない)。この3つを落とせる、と決めてから書くと、正規表現は自然と引き締まります。
Claude Codeに頼むときも同じです。「メールのregexを書いて」ではなく、許可例と拒否例を箇条書きで渡す。そうすると、AIが「とりあえず通る広いやつ」に逃げられなくなります。テストケースが、いわば実務上の契約になるわけです。
Claude Codeの基本操作がまだの人は Claude Code入門ガイド を、依頼文の組み立て方は Claude Code/Codexプロンプト入門 を先に読むとつながりが良くなります。仕様そのものは MDNのJavaScript正規表現リファレンス が公式の入口です。Claude Codeは便利ですが、仕様を置き換えるものではありません。
Claude Codeへの最初の依頼
最初のプロンプトは短くていいです。ただし用途と合格条件は必ず入れます。
メールアドレス、日本の電話番号、アプリケーションログを扱う正規表現ヘルパーを作ってください。
条件:
- Node.jsでそのまま動くJavaScriptにする
- メールは [email protected] を許可する
- メールは [email protected] と [email protected] を拒否する
- 電話番号は 090-1234-5678, 03-1234-5678, 05012345678 を許可する
- ログは timestamp, level, service, requestId, message を名前付きキャプチャで取り出す
- node:test のテストも作る
- 正規表現でやりすぎている点があればコメントで説明する
ここで「メールの完全な仕様に準拠して」とは絶対に言いません。メールアドレスの正式な仕様(RFC 5322)はとんでもなく広くて、"john doe"@example.com のような引用符付きまで許します。登録フォームでそんなものを通したら、たいていは入力ミスです。実務では厳密すぎても緩すぎても困る。だから「フォームでよく使う実用的な範囲」に最初から絞ります。
実例:メール・電話・ログ抽出を1ファイルに
ここが本体です。次のコードを regex-helper.mjs として保存すれば、node regex-helper.mjs でそのまま動きます。メール検証、電話番号検証、本文からの連絡先抽出、名前付きキャプチャによるログ抽出を1つにまとめています。
import { fileURLToPath } from "node:url";
// メール検証用: 先頭でドット連続(..)を否定先読みで弾き、ドメインは各ラベルを丁寧に
export const emailRegex =
/^(?!.*\.\.)[A-Z0-9_%+-]+(?:\.[A-Z0-9_%+-]+)*@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,63}$/i;
// 本文からメールを拾う検索用(検証用とは別に持つ)
export const emailSearchRegex =
/[A-Z0-9_%+-]+(?:\.[A-Z0-9_%+-]+)*@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,63}/gi;
// 正規化後(記号を抜いた状態)の電話番号を判定する
export const normalizedJapanesePhoneRegex = /^0(?:[5789]0\d{8}|[1-9]\d{8,9})$/;
// 本文から電話番号らしき塊を拾う検索用
export const looseJapanesePhoneSearchRegex =
/0\d{1,4}[-\s]?\d{1,4}[-\s]?\d{3,4}/g;
// アプリログ1行を分解する。取り出す列はすべて名前付きキャプチャ
export const appLogRegex =
/^\[(?<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z)\]\s+(?<level>INFO|WARN|ERROR)\s+(?<service>[a-z][a-z0-9-]*)\s+requestId=(?<requestId>[A-Za-z0-9_-]+)\s+message="(?<message>[^"]*)"$/;
export function isEmail(input) {
return emailRegex.test(input.trim());
}
// 比較しやすい形にそろえる(ハイフン・空白・括弧を除去)
export function normalizePhone(input) {
return input.replace(/[()\s-]/g, "");
}
export function isJapanesePhone(input) {
return normalizedJapanesePhoneRegex.test(normalizePhone(input));
}
export function extractContacts(text) {
const emails = [...text.matchAll(emailSearchRegex)]
.map((match) => match[0])
.filter(isEmail); // 拾った後に厳密な検証で二段構え
const phones = (text.match(looseJapanesePhoneSearchRegex) ?? []).filter(
isJapanesePhone,
);
return {
emails: [...new Set(emails)], // 重複を落とす
phones: [...new Set(phones)],
};
}
export function parseLogLine(line) {
const match = line.match(appLogRegex);
if (!match?.groups) return null; // パースできない行はnullで返す(捨てない判断は呼び出し側で)
return {
timestamp: match.groups.timestamp,
level: match.groups.level,
service: match.groups.service,
requestId: match.groups.requestId,
message: match.groups.message,
};
}
// このファイルを直接実行したときだけ動くサンプル
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const text = "連絡先: [email protected] / 090-1234-5678";
const log =
'[2026-06-02T10:15:30.000Z] ERROR billing-api requestId=req_123 message="payment failed"';
console.log(extractContacts(text));
console.log(parseLogLine(log));
}
実装で押さえてほしいのは3か所だけです。
ひとつ目、電話番号は先に正規化しています。正規化とは、比較しやすい形にそろえること。090-1234-5678 と 090 1234 5678 を別物として正規表現で両方カバーしようとすると、パターンがどんどん長くなります。先にハイフンと空白を抜いてしまえば、判定する正規表現は短く保てます。
ふたつ目、検証用と検索用を分けています。emailRegex(^...$ で全体一致)と emailSearchRegex(g フラグで本文から拾う)は役割が違うので別の変数にしました。1本で兼用すると後で必ず混乱します。
みっつ目、ログ抽出は名前付きキャプチャを使っています。match[1]、match[2] のような番号で取り出すと、後で正規表現の途中に () を足したとき列がずれて、静かにバグります。match.groups.requestId と名前で読めば、Claude Codeのレビューでも人間のレビューでも意図が一目で追えます。
テストで「直したら戻った」を防ぐ
正規表現は1文字変えただけで挙動が変わります。+ を1個足したつもりが、別のケースを巻き込んで壊す。これがテストなしだと気づけません。だから回帰テストを付けます。回帰テストとは、一度直した不具合がまた戻っていないか確認するテストのことです。
次を regex-helper.test.mjs として保存し、node --test regex-helper.test.mjs で実行します。
import test from "node:test";
import assert from "node:assert/strict";
import {
extractContacts,
isEmail,
isJapanesePhone,
parseLogLine,
} from "./regex-helper.mjs";
test("validates practical email addresses", () => {
// 通したい例
assert.equal(isEmail("[email protected]"), true);
assert.equal(isEmail("[email protected]"), true);
// 落としたい例(ここが本番の抜けを防ぐ)
assert.equal(isEmail("[email protected]"), false);
assert.equal(isEmail("[email protected]"), false);
assert.equal(isEmail("@example.com"), false);
});
test("validates Japanese phone numbers after normalization", () => {
assert.equal(isJapanesePhone("090-1234-5678"), true);
assert.equal(isJapanesePhone("03-1234-5678"), true);
assert.equal(isJapanesePhone("05012345678"), true);
assert.equal(isJapanesePhone("123-4567-8901"), false); // 0始まりでない
assert.equal(isJapanesePhone("090-123-456"), false); // 桁が足りない
});
test("extracts contacts from free text", () => {
assert.deepEqual(
extractContacts("support: [email protected], tel: 090-1234-5678"),
{
emails: ["[email protected]"],
phones: ["090-1234-5678"],
},
);
});
test("parses application logs with named captures", () => {
const parsed = parseLogLine(
'[2026-06-02T10:15:30.000Z] WARN auth-service requestId=req_abc message="retry required"',
);
assert.deepEqual(parsed, {
timestamp: "2026-06-02T10:15:30.000Z",
level: "WARN",
service: "auth-service",
requestId: "req_abc",
message: "retry required",
});
});
そしてClaude Codeには、テスト結果まで確認させてから修正させます。
regex-helper.mjs と regex-helper.test.mjs を作成しました。
node --test regex-helper.test.mjs を実行し、失敗した場合は
正規表現とテストデータのどちらが間違っているか説明してから修正してください。
メール仕様を広げる場合は、許可例と拒否例を先に追加してから変更してください。
「広げる前に例を足せ」と縛るのが効きます。AIは放っておくと、テストを通すために正規表現をゆるめる方向に逃げがちです。先にテストを足させれば、その逃げ道がふさがります。テストの考え方そのものは Claude Codeでバグを潰す手順 でも掘り下げているので、デバッグとセットで読むと身につきます。
量指定子・貪欲・エスケープの最低限
ここだけは記号の話をします。Claude Codeに任せるにしても、出てきたものを読めないとレビューできないので。
量指定子は「直前を何回繰り返すか」です。*(0回以上)、+(1回以上)、?(0回か1回)、{2,63}(2回から63回)。上のメール正規表現の [A-Z]{2,63} は「トップレベルドメインは2〜63文字」という意味です。
**貪欲(greedy)**は、量指定子がデフォルトで「できるだけ長く」マッチする性質のこと。".*" を "a" and "b" に当てると、"a" だけでなく "a" and "b" 全体を飲み込みます。終端の " まで貪欲に伸びるからです。これを防ぐには ? を付けて ".*?"(最小マッチ)にするか、そもそも "[^"]*"(ダブルクオート以外を繰り返す)と書く。上のログ正規表現で (?<message>[^"]*) にしているのは、貪欲で取りすぎないための定番です。
エスケープは、記号そのものを文字として扱う指定です。. は「任意の1文字」ですが、本物のドットを表したいなら \. と書く。日付の 2026-06-07 を厳密に書くなら \d{4}-\d{2}-\d{2} で、ハイフンは記号として書けますが、ドットを含むなら \. を忘れると別物になります。Claude Codeに「このパターンでエスケープが抜けている箇所はある?」と聞くと、地味なミスをよく拾ってくれます。
| 記号 | 意味 | よく使う場面 |
|---|---|---|
\d | 数字1文字 | 日付・電話・ID |
\w | 英数字とアンダースコア | 変数名・トークン |
[^"] | ダブルクオート以外 | 引用符で囲まれた中身 |
(?:...) | グループ化のみ(取り出さない) | まとめて繰り返したいだけのとき |
(?<name>...) | 名前付きキャプチャ | 後で取り出したい部分 |
(?!...) | 否定先読み | 「直後にこれが来ない」条件 |
用途ごとの使い分け
正規表現は万能ではありません。「これは正規表現でやらないほうがいい」を知っておくと、無駄に苦しまずに済みます。
| 用途 | 正規表現でよい場面 | 別の方法を選ぶ場面 |
|---|---|---|
| メール検証 | 入力フォームで明らかなミスを落とす | 本当に届くか確認したい |
| 電話番号 | 国内向けフォームで形式をそろえる | 国際番号や事業者の厳密判定が必要 |
| ログ抽出 | 形式が固定されたアプリログを読む | JSONログを扱える環境がある |
| URL処理 | 短い抽出や検索 | URLを正確に分解する必要がある |
| Markdown | 単純なキーワード検索 | 構造を理解して変換したい |
特にURLとMarkdownは要注意です。URLはJavaScriptに URL クラスがあるので、分解はそっちに任せて正規表現は抽出だけに使う。Markdownにいたっては、正規表現で見出しやリンクを拾おうとすると入れ子で必ず破綻します。なぜダメで何を使うべきかは Markdownを変換するなら正規表現は捨てる に詳しく書きました。Claude Codeにも「URLの分解は標準APIを優先、Markdownはパーサを使って、正規表現は単純抽出だけ」と最初に書いておくと、壊れにくい提案が返ってきます。
僕がやらかした落とし穴3つ
正直に書きます。正規表現では何度も足をすくわれました。
ひとつ目は冒頭の話、通る例だけで作ったこと。[email protected] だけ確認して満足し、プラス記号付きを落としていました。今は拒否例を最低3つ書いてから始めます。
ふたつ目は、g フラグ付きを使い回したこと。JavaScriptの RegExp は g や y を付けると lastIndex という内部状態を持ちます。同じ正規表現で test() を繰り返すと、2回目以降が前回の続きから探して、結果が交互に true/false になる。最初これでテストが意味不明に落ちて30分溶かしました。検証用と検索用を分けたのは、この事故を二度と起こさないためです。
みっつ目が一番こわい、**壊滅的バックトラッキング(ReDoS)**です。(a+)+$ のような曖昧な繰り返しの入れ子を書くと、マッチに失敗する入力に対して正規表現エンジンが組み合わせを延々と試して、aaaaaaaaaaaaaaa! みたいな短い文字列でも処理が固まります。攻撃にも障害にもなる。だから僕はClaude Codeへのレビュー依頼に必ず「ReDoSの観点で見て」と入れます。
Claude Codeへのレビュー依頼テンプレート
正規表現は、書いた本人ほど危険に気づけません。最後にこのテンプレートでレビューさせます。
## Regex review request
対象:
- regex-helper.mjs
- regex-helper.test.mjs
レビュー観点:
- 許可例と拒否例がテストに入っているか
- メール・電話・ログ抽出の責務が混ざりすぎていないか
- 名前付きキャプチャの名前が用途を説明しているか
- ReDoSにつながる曖昧な繰り返しの入れ子がないか
- 個人情報をログやCSVに出す処理がないか
出力形式:
- 問題があればファイル名・該当行・理由・修正案を出す
- 仕様として判断できない点は断定せず質問にする
- 修正後に node --test regex-helper.test.mjs を実行する
「判断できない点は質問にする」と指定するのが地味に効きます。これがないと、AIは仕様を勝手に推測して、こっちが意図していない方向に正規表現を変えてしまうからです。
よくある質問
Q. 正規表現を全部Claude Codeに丸投げしてもいい? 記号の暗記は任せていいです。ただし許可例・拒否例・テストは自分で決めてください。そこが曖昧だと、AIは広すぎる正規表現を出します。判断の責任は人間側に残ります。
Q. メールは厳密に検証すべき?
フォームでは「明らかなミスを落とす」程度で十分です。本当に届くかは確認メールでしか分かりません。厳密にしすぎると、+ 付きや新しいトップレベルドメインの正しいユーザーを弾いて機会損失になります。
Q. 名前付きキャプチャと (?:...) はどう使い分ける?
後で値を取り出すなら名前付きキャプチャ (?<name>...)。まとめて繰り返したいだけで取り出さないなら非キャプチャ (?:...)。番号キャプチャ (...) は途中に足すと列番号がずれるので、取り出す目的があるなら避けます。
Q. 壊滅的バックトラッキングを避けるコツは?
(.+)+ や (a*)* のような「繰り返しの中に繰り返し」を作らないことです。区切り文字があるなら [^,]* のように「その文字以外」で限定する。心配なら入力長に上限を設け、Claude Codeに「ReDoSの観点で」とレビューさせます。
Q. JavaScript以外の言語でも同じ考え方? 基本は同じです。許可例・拒否例から組む、テストで固定する、貪欲に取りすぎない。ただし名前付きキャプチャの書き方やフラグの挙動は言語ごとに違うので、そこだけ公式リファレンスで確認してください。
実際に試した結果
この手順に変えてから、僕の正規表現まわりのバグ報告は目に見えて減りました。一番効いたのは、やっぱり先に落としたい例を3つ書くこと。これをやるだけで、出てくる正規表現が勝手に引き締まります。
Claude Codeへの頼み方も、「正規表現だけ書いて」から「失敗例とテストを同時に作って」に変えたら、レビューにかかる時間がはっきり短くなりました。user+tag のメール、ハイフンあり電話番号、ログの名前付きキャプチャ——このあたりはテストがないと後から静かに壊れる常連です。レビュー依頼テンプレートまで渡しておけば、正規表現の便利さを保ったまま、フォームやログ処理の事故を先回りで潰せます。
正規表現は小さな作業に見えて、問い合わせフォーム、資料請求、決済ログの品質に直結します。手元に依頼の型を増やしたい人は 教材一覧 を入口にどうぞ。チームで入力検証やログ調査の運用ごと整えるなら Claude Code研修・導入相談 で、既存コードに合わせた型を一緒に作るのが早いです。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。