「入力欄に特定のパターンで文字が入力されているかバリデーションをかけてもらえませんか」
とお願いされて、不安に駆られなかったことが一度もない。この場合は正規表現を使ってバリデーションを用意するが、いつも頭を抱えている。不当な値がアプリケーションの中で使いまわされていないかを考えるだけで、胃がキリキリする。恥ずかしい話だが、正規処理が全然わからず毎度他の事例を参考にして、最後ユニットテストを書いてしっかり仕様を満たしているかを確認する始末だ。ゼロベースからかけるレベルまで達せなくてもいいいが、参考にする正規表現の是非の判断を自信をもってできるようにしたい。振り返ると基礎的な部分が抜けているので、まずはそこから理解を始める。
特定の文字列からパターンに合致している部分を抽出するものかと思いきや、文字列の集合を 1 つの文字列で表現する方法の 1 つになる。 使い所としては、フォームで入力された郵便番号や住所の番地、電話番号や、クーポンや商品コードの形式が正しく沿っているかを確認するのに使う。テキスト検索の用途もある。
下の正規表現は文字列が電話番号か否かを確認するためのものとなる。読み解いていくと、「0 で始まり / 2 桁または 3 桁の数字であり / ハイフン繋がり / 3 桁または 4 桁の数字であり / ハイフン繋がりであり / 4 桁の数字」のパターンの文字列を表す。
/^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*c は ac / abc / abbc / abbbc にマッチする。 |
+ | 直前の文字の 1 回以上繰り返されることを表す。ab+c は abc / abbc / abbbc にマッチする。 |
? | 直前の文字の 0 回もしくは、1 回の出現されることを表す。ab?c は ac / abc にマッチする。 |
{n} | 直前の文字 n 個の繰り返しとマッチする。 |
{n,m} | 直前の文字を m ~ n 回とマッチする。 |
. | 任意の 1 文字として代用される。 |
よく使われる組み合わせとして、^
と $
がある。例えば、^abc
と書いた場合には、abc
も abcd
などがマッチする。abc$
と書いた場合にはabc
とzabc
などがマッチする。^abc$
と書くと、先頭と末尾が決まっているのでabc
だけがマッチする。
検索修飾子というものがあり、正規表現パターンの挙動を変更するための設定やオプションになる。使い方は / /
の正規表現の後に書く。複数モードを並べられる。
修飾子 | 意味 |
---|---|
g | 「すべての」→指定しないときは最初に一致したものだけ |
i | 「大文字・小文字を区別せずに」→指定しないときは区別する |
m | 「複数行ある場合はすべての行を対象に」→1行のときは指定しない |
s | ドット (.) が改行文字 (\n) にも一致するようになる。 |
u | Unicode コードポイントを正しく解釈して処理する。 |
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 文字だけの場合は、[^ ]
を使って、否定の文字クラス(Negative Character Class)を表せる。
// 否定の文字クラス: AかBかCのいずれか以外の一文字
[^ABC]
「パターンの否定」を利用する正規表現に「否定先読み」「否定戻り読み」がある。
否定先読みを用いて、^
すなわち文頭の直後に、指定のサブパターンを含まない文字列をマッチングする表現となる。
// PATTERN で開始しない文字列の表現
^(?!PATTERN).*$
否定戻り読みを用いて、$
が示す文末の直前に特定のパターンを含まない文字列をマッチングする。
// PATTERNで終了しない文字列の表現
^.*(?<!PATTERN)$
パターンの出現位置を限定せず、文内に「特定の文字列を含まない」パターンを記述する方法となる。カッコ内のサブパターンの最初に量指定子「.*」を追加し、これで行内での位置を問わずにパターンのマッチング成否が可能となる。
// PATTERNを含まない文字列
^(?!.*PATTERN).*$
ルールがわかった後は、実際に業務レベルで扱えるように、言語の正規表現クラスがどうなっているかを調べてみる。両方とも言語が提供している RegExp
クラスがある。ちなみに regular expression の略となる。
Node.js での正規表現をどう扱うのかを見てみる。
RegExp - JavaScript | MDN
https://developer.mozilla.org
// 以下は全部書き方が同じ
const regexp = /ab+c/i; // リテラル記法
// または
const regexp = new RegExp("ab+c", "i"); // 最初の引数に文字列のパターンを渡したコンストラクター
// または
const regexp = new RegExp(/ab+c/, "i"); // 最初の引数に正規表現リテラルを渡したコンストラクター
const str = 'table football';
const regex = new RegExp('foo*');
console.log(regex.test(str)); // Expected output: true
const regex1 = RegExp('foo*', 'g');
const str1 = 'table football, foosball';
const array = regex1.exec(str1);
console.log(array); // Expected output: Array ["foo"]
マッチするかの確認は、String クラスから match
メソッドが提供されるので、正規表現を渡してマッチするか確認する。
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 での正規表現をどう扱うのを見てみる。
https://api.flutter.dev/flutter/dart-core/RegExp-class.html
https://api.flutter.dev
RegExp
クラスが言語標準で提供されており、オプションでいくつか検索修飾子の代わりになるものがある。
final regexp = RegExp(
r'pattern',
{
bool multiLine = false,
bool caseSensitive = false,
bool unicode = false,
bool dotAll = false,
},
);
RegExp
クラスのインスタンスメソッドも軽く紹介する。
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');
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');
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);;
final regExp = RegExp(r'cookie');
expect(regExp.stringMatch('cookie'), 'cookie');
expect(regExp.stringMatch('a cookie'), 'cookie');
expect(regExp.stringMatch('cooki'), isNull);
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 Card | 6011[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
https://user-engagement.treasuredata.co.jp
正規表現でのエクスクラメーションマークやビックリマーク(!)は先読み,後読み|僕らの適正技術
https://ourinttech.com
よく使う正規表現チートシート
https://yklog.net
【Flutter/Dart】で 正規表現リファレンス RegExpで文字列チェック、置換などを一通り
https://flutter.salon
忘れっぽい人のための正規表現チートシート - Qiita
https://qiita.com
初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」 - Qiita
https://qiita.com
初学者が一番初めに読むべき正規表現入門 - セキュリティ専門企業発、ネットワーク・ログ監視の技術情報 - KnowledgeStare(ナレッジステア)
https://www.secuavail.com