正規表現が苦手なエンジニアのための正規処理入門

作成日: 2024-12-22 /

「入力欄に特定のパターンで文字が入力されているかバリデーションをかけてもらえませんか」

とお願いされて、不安に駆られなかったことが一度もない。この場合は正規表現を使ってバリデーションを用意するが、いつも頭を抱えている。不当な値がアプリケーションの中で使いまわされていないかを考えるだけで、胃がキリキリする。恥ずかしい話だが、正規処理が全然わからず毎度他の事例を参考にして、最後ユニットテストを書いてしっかり仕様を満たしているかを確認する始末だ。ゼロベースからかけるレベルまで達せなくてもいいいが、参考にする正規表現の是非の判断を自信をもってできるようにしたい。振り返ると基礎的な部分が抜けているので、まずはそこから理解を始める。

そもそも正規表現とは

特定の文字列からパターンに合致している部分を抽出するものかと思いきや、文字列の集合を 1 つの文字列で表現する方法の 1 つになる。 使い所としては、フォームで入力された郵便番号や住所の番地、電話番号や、クーポンや商品コードの形式が正しく沿っているかを確認するのに使う。テキスト検索の用途もある。

下の正規表現は文字列が電話番号か否かを確認するためのものとなる。読み解いていくと、「0 で始まり / 2 桁または 3 桁の数字であり / ハイフン繋がり / 3 桁または 4 桁の数字であり / ハイフン繋がりであり / 4 桁の数字」のパターンの文字列を表す。

tel_regexp.txt
/^0\d{2,3}-\d{3,4}-\d{4}$/

何が難しいって、メタ文字や特殊シーケンスがどんな文字を抽出してくれるのかパッと分かりづらい。暗記が必要という話に着地するが、Uglify で本番用にビルドで暗号化された JavaScript と同じぐらい、パッと見たときの中身が分かりづらい。

この記事では基本的な正規表現のことはもちろんのこと、業務でよく正規表現を並べて細かく見ていくことで、別日でこの記事を読んだ後でも正規表現を思い出せるものにする。

コードについては、個人的な都合になってしまうが、今回自分が業務で使う Node.js (TypeScript)Dart を使用する。

表現方法

まずは両端を / で囲う。正規表現のメタ文字を使うとき \ を先に書く。

メタ文字

メタ文字とは、文字列のパターンを表現するために特別な意味を持つ文字のこと。1 つ 1 つ意味と例を書いていく。量指定子とアンカーも含めてまとめる。

メタキャラクターどんな意味
^入力の先頭にマッチする。^abc は、abc abc abc の行の先頭にある abc だけが合致する。
$行の末尾にマッチする。abc$ は、abc abc abc の行の先頭にある abc だけが合致する。
[]カッコの中の任意の 1 文字と一致します。[abc] は、a、b、または c のいずれか 1 文字にマッチする。
*直前の文字が 0 回以上繰り返されることを表す。ab*cac / abc / abbc / abbbc にマッチする。
+直前の文字の 1 回以上繰り返されることを表す。ab+cabc / abbc / abbbc にマッチする。
?直前の文字の 0 回もしくは、1 回の出現されることを表す。ab?cac / abc にマッチする。
{n}直前の文字 n 個の繰り返しとマッチする。
{n,m}直前の文字を m ~ n 回とマッチする。
.任意の 1 文字として代用される。

よく使われる組み合わせとして、^$ がある。例えば、^abc と書いた場合には、abcabcd などがマッチする。abc$と書いた場合にはabczabcなどがマッチする。^abc$と書くと、先頭と末尾が決まっているのでabcだけがマッチする。

検索修飾子

検索修飾子というものがあり、正規表現パターンの挙動を変更するための設定やオプションになる。使い方は / / の正規表現の後に書く。複数モードを並べられる。

修飾子意味
g「すべての」→指定しないときは最初に一致したものだけ
i「大文字・小文字を区別せずに」→指定しないときは区別する
m「複数行ある場合はすべての行を対象に」→1行のときは指定しない
sドット (.) が改行文字 (\n) にも一致するようになる。
uUnicode コードポイントを正しく解釈して処理する。
x正規表現内の空白やコメントを無視して可読性を向上させる。
yスティッキーモード。現在の検索位置からのみ一致を試みる。

特殊シーケンス

特定の種類の文字やパターンを簡潔に表現するためのメタ文字やエスケープされた文字列になる。

文字どんな意味
.任意の 1 文字
\w英単語を構成する文字(a ~ z、A ~ Z、_、1 ~ 9)
\W英単語を構成する文字以外
\s空白文字(半角スペース、タブ、改行、キャリッジリターン)
\S空白文字以外
\d半角数字(0 ~ 9)
\D半角数字以外
\b単語の境界に一致
xyz指定された文字のどれかに一致(この場合xyzのいずれかに一致)
a-zマッチする文字の範囲を指定する表現(この場合 a から z まで他には [1-9][A-Z] など文字コードが連続していれば使える。)
(pattern1|pattern2)指定されたパターンのどれかにマッチする表現

グループ化

正規表現をひとまとまりにし、複数の条件にあう文字列を探せる。 () を使う。

否定の正規表現

今までは特定のパターンを含めた文字列を抽出するにはどうしたらいいかを考えたが、特定の文字を含めない正規表現を書きたい場合がある。大きく分けて2種類のケースがあり、「特定の 1 文字を含まない」といった否定表現か、もしくは「正規表現パターンを含まない」といった表現かで、対応が変わるので注意が必要になる。

任意の 1 文字を含まない文字列の正規表現

1 文字だけの場合は、[^ ] を使って、否定の文字クラス(Negative Character Class)を表せる。

negative_character_class.txt
// 否定の文字クラス: AかBかCのいずれか以外の一文字
[^ABC]

任意の文字列を含まない文字列の正規表現

「パターンの否定」を利用する正規表現に「否定先読み」「否定戻り読み」がある。

特定のパターンで開始しない文字列

否定先読みを用いて、^すなわち文頭の直後に、指定のサブパターンを含まない文字列をマッチングする表現となる。

negative_start_pattern.txt
// PATTERN で開始しない文字列の表現 
^(?!PATTERN).*$
特定のパターンで終了しない文字列

否定戻り読みを用いて、$ が示す文末の直前に特定のパターンを含まない文字列をマッチングする。

negative_end_pattern.txt
// PATTERNで終了しない文字列の表現 
^.*(?<!PATTERN)$
特定のパターンを含まない文字列

パターンの出現位置を限定せず、文内に「特定の文字列を含まない」パターンを記述する方法となる。カッコ内のサブパターンの最初に量指定子「.*」を追加し、これで行内での位置を問わずにパターンのマッチング成否が可能となる。

exclusive_pattern.txt
// PATTERNを含まない文字列
^(?!.*PATTERN).*$

言語ごとの正規表現クラス

ルールがわかった後は、実際に業務レベルで扱えるように、言語の正規表現クラスがどうなっているかを調べてみる。両方とも言語が提供している RegExp クラスがある。ちなみに regular expression の略となる。

Node.js (TypeScript)

Node.js での正規表現をどう扱うのかを見てみる。

RegExp - JavaScript | MDN

RegExp - JavaScript | MDN

https://developer.mozilla.org

RegExp - JavaScript | MDN
regexp.ts
// 以下は全部書き方が同じ

const regexp = /ab+c/i; // リテラル記法
// または
const regexp = new RegExp("ab+c", "i"); // 最初の引数に文字列のパターンを渡したコンストラクター
// または
const regexp = new RegExp(/ab+c/, "i"); // 最初の引数に正規表現リテラルを渡したコンストラクター
test.ts
const str = 'table football';
const regex = new RegExp('foo*');

console.log(regex.test(str)); // Expected output: true
exec.ts
const regex1 = RegExp('foo*', 'g');
const str1 = 'table football, foosball';
const array = regex1.exec(str1);

console.log(array); // Expected output: Array ["foo"]

マッチするかの確認は、String クラスから match メソッドが提供されるので、正規表現を渡してマッチするか確認する。

match.ts
const paragraph = 'The quick brown fox jumps over the lazy dog. It barked.';
const regex = /[A-Z]/g;
const found = paragraph.match(regex);

console.log(found); // Expected output: Array ["T", "I"]

Dart

Dart での正規表現をどう扱うのを見てみる。

https://api.flutter.dev/flutter/dart-core/RegExp-class.html

https://api.flutter.dev

RegExp クラスが言語標準で提供されており、オプションでいくつか検索修飾子の代わりになるものがある。

regexp.dart
final regexp = RegExp(
  r'pattern',
  {
    bool multiLine = false,
    bool caseSensitive = false,
    bool unicode = false,
    bool dotAll = false,
  },
);

RegExp クラスのインスタンスメソッドも軽く紹介する。

allMatches.dart
final regA = RegExp(r'A');

final matches = regA.allMatches('AbA');
expect(matches.length, 2);
expect(matches.toList()[0].group(0), 'A');
expect(matches.toList()[1].group(0), 'A');
firstMatch.dart
final regA = RegExp(r'A');

final firstMatch = regA.firstMatch('A');
expect(firstMatch!.groupCount, 0);
expect(firstMatch.group(0), 'A');
final firstMatch = regA.firstMatch('a');
expect(firstMatch, null);

final secondMatch = regA.firstMatch('AbA');
expect(secondMatch!.groupCount, 0);
expect(secondMatch.group(0), 'A');
hasMatch.dart
final regA = RegExp(r'A');

expect(regA.hasMatch('A'), true);
expect(regA.hasMatch('bA'), true);
expect(regA.hasMatch('Ab'), true);
expect(regA.hasMatch('b'), false);;
stringMatch.dart
final regExp = RegExp(r'cookie');

expect(regExp.stringMatch('cookie'), 'cookie');
expect(regExp.stringMatch('a cookie'), 'cookie');
expect(regExp.stringMatch('cooki'), isNull);
firstMatch.dart
const string = 'I baked a cookie.';
final regExp = RegExp(r'cookie');
final match = regExp.firstMatch(string)!;
expect(match.start, 10);
expect(match.end, 16);
expect(match.pattern, regExp);

チートシート

ここまで学べたので、チートシートを用意して、困ったらここを見るようにする。随時追加していく。

文字

項目電話番号
「abc」を含まない^(?!.*abc).*$
「abc」or「def」を含まない^(?!.*(abc|def)).*$
「abc」から始まらない^(?!abc).*$
「abc」で終わらない^(?!.*abc$).*$
「abc」を含み、「def」を含まない^(?=.*abc)(?!.*def).*$
全角ひらがな、空文字^[ぁ-んー]*$
全角カタカナ、空文字^[ァ-ンヴー]*$
アルファベットを含まない文字列[^a-zA-Z]+
数字を含まない文字列[^0-9]+

電話番号

項目正規表現
メールアドレス^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
インターネット^(http|https)://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
ドメイン名^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$
キャリア端末ハイフンありなし^0[789]0(-\d{4}-\d{4}|\d{8})$
IP 電話^050(-\d{4}-\d{4}|\d{8})$
フリー ダイヤル^0120-\d{3}-\d{3}$

クレジットカード番号

項目正規表現
VISA^4[0-9]{12}(?:[0-9]{3})?$
MasterCard^5[1-5][0-9]{14}$
Discover Card6011[0-9]{12}
Diners^3(?:0[0-5]|[68][0-9])[0-9]{11}$|^6(?:011|5[0-9][0-9])[0-9]{12}$
Amex^3[47][0-9]{13}$
JCB^3[0-9]{15}$

まとめ

ゼロベースから正規表現が書けるようになるレベルまでは至らないが、このまとめを使って、当初の目的である初見の正規表現の是非をなんとか見極められるものになったと感じる。正直色々な記事のまとめ集みたいになり、自分の言葉で書き足りていない箇所が残っているので、業務で正規表現を使うときが増えたら、学んだことをここに足していく。

参考にした記事たち

https://www-creators.com/archives/1827

https://www-creators.com

よく使う正規表現とシーン別使用例まとめ - Treasure Data User Engagement

よく使う正規表現とシーン別使用例まとめ - Treasure Data User Engagement

https://user-engagement.treasuredata.co.jp

よく使う正規表現とシーン別使用例まとめ - Treasure Data User Engagement

正規表現でのエクスクラメーションマークやビックリマーク(!)は先読み,後読み|僕らの適正技術

正規表現でのエクスクラメーションマークやビックリマーク(!)は先読み,後読み|僕らの適正技術

https://ourinttech.com

正規表現でのエクスクラメーションマークやビックリマーク(!)は先読み,後読み|僕らの適正技術

よく使う正規表現チートシート

よく使う正規表現チートシート

https://yklog.net

よく使う正規表現チートシート

【Flutter/Dart】で 正規表現リファレンス RegExpで文字列チェック、置換などを一通り

【Flutter/Dart】で 正規表現リファレンス RegExpで文字列チェック、置換などを一通り

https://flutter.salon

【Flutter/Dart】で 正規表現リファレンス RegExpで文字列チェック、置換などを一通り

忘れっぽい人のための正規表現チートシート - Qiita

忘れっぽい人のための正規表現チートシート - Qiita

https://qiita.com

忘れっぽい人のための正規表現チートシート - Qiita

初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」 - Qiita

初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」 - Qiita

https://qiita.com

初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」 - Qiita

初学者が一番初めに読むべき正規表現入門 - セキュリティ専門企業発、ネットワーク・ログ監視の技術情報 - KnowledgeStare(ナレッジステア)

初学者が一番初めに読むべき正規表現入門 - セキュリティ専門企業発、ネットワーク・ログ監視の技術情報 - KnowledgeStare(ナレッジステア)

https://www.secuavail.com

初学者が一番初めに読むべき正規表現入門 - セキュリティ専門企業発、ネットワーク・ログ監視の技術情報 - KnowledgeStare(ナレッジステア)