この記事の対象者
-
以下の方が読んで勉強・コピペできる記事になってます。
- DartやFlutterで正規表現を使ってみようという方
- 基本的な正規表現を覚えたい方
- 実際のユースケースでの正規表現を知りたい方
- どうせ正規表現を暗記できない明日の自分
正規表現とは
- パターンマッチングの一種
- FlutterやDart専用ではない
正規表現を使えば、通常の文字列検索より複雑で色々な検索を行うことができます。見つかった文字列を処理したり、新しい文字列を作成したりできます。
Dart以外の言語でプログラムをしていれば、その言語でも正規表現を使って文字列を検索する手段があったと思います。また、お使いのエディタにも組み込まれていると思います。
筆者は20年前にPerlで覚えました。そのあとも、JavaやC#でも使用してきました。言語毎で使用方法や方言がありますが、基本的には一度正規表現を覚えれば、色々な場面で一生使用できます。
Dartによる正規表現の利用
ここでは、正規表現そのものより、Dart特有の正規表現の使い方を書きます。(おおよそ他の言語でも一緒でしょうが、、)
正規表現の作成
final reg = RegExp(r'A');
正規表現の該当箇所があるか確認
final regA = RegExp(r'A');
expect(regA.hasMatch('A'), true);
expect(regA.hasMatch('aA'), true);
expect(regA.hasMatch('Aa'), true);
expect(regA.hasMatch('a'), false);;
正規表現で該当箇所を取得
firstMatchは最初の該当箇所ひとつだけを取得する。
一方で、allMatchesで該当箇所を全てIteratableとして取得する。そのため、for文かtoList()にして、処理をする。
実際の該当の文字列はgroup(0)で取得する。
final matchOne = regA.firstMatch('A');
expect(matchOne!.groupCount, 0);
expect(matchOne.group(0), 'A');
final matchNone = regA.firstMatch('a');
expect(matchNone, null);
final matchOne2 = regA.firstMatch('AaA');
expect(matchOne2!.groupCount, 0);
expect(matchOne2.group(0), 'A');
final matchSome = regA.allMatches('AaA');
expect(matchSome.length, 2);
expect(matchSome.toList()[0].group(0), 'A');
expect(matchSome.toList()[1].group(0), 'A');
正規表現で一致箇所を置換
置換はString型の、replaceFirst/replaceAllを使用する。
final regA = RegExp(r'A');
expect('A'.replaceFirst(regA, 'B'), 'B');
expect('AaA'.replaceFirst(regA, 'B'), 'BaA');
expect('AaA'.replaceAll(regA, 'B'), 'BaB');
大文字・小文字の区別をする・しないの選択
大文字小文字を区別しないための正規表現(\i)があるが、Dartでは使えない。単にRegExpのcaseSensitiveを使う。true:区別する false:区別しない
final regDefault = RegExp(r'A');
final regSensitive = RegExp(r'A', caseSensitive: true);
final regNotSensitive = RegExp(r'A', caseSensitive: false);
expect(regDefault.isCaseSensitive, true);
expect(regSensitive.isCaseSensitive, true);
expect(regNotSensitive.isCaseSensitive, false);
expect(regDefault.hasMatch('a'), false);
expect(regSensitive.hasMatch('a'), false);
expect(regNotSensitive.hasMatch('a'), true);
複数回の一致している場合の検索
final reg = RegExp(r'AB.');
final matches = reg.allMatches('AB1aAB2aAB3');
expect(matches.length, 3);
final matchWords = matches.map((e) => e.group(0)).toList();
expect(matchWords.length, 3);
expect(matchWords[0], 'AB1');
expect(matchWords[1], 'AB2');
expect(matchWords[2], 'AB3');
stringMatchでマッチ部分を文字列で取得
final regExp = RegExp(r'bird');
expect(regExp.stringMatch('bird'), 'bird');
expect(regExp.stringMatch('a bird'), 'bird');
expect(regExp.stringMatch('bir'), isNull);
RegExpMatchのプロパティ
const string = 'Dash is a bird.';
final regExp = RegExp(r'bird');
final match = regExp.firstMatch(string)!;
expect(match.start, 10);
expect(match.end, 14);
expect(match.pattern, regExp);
正規表現で一致する文字列前後に文字を追加(固定)
final regA = RegExp(r'A');
expect('ABC'.replaceFirst(regA, '@A'), '@ABC');
expect('ABC'.replaceFirst(regA, 'A@'), 'A@BC');
正規表現で一致する文字列前後に文字を追加(動的)
const original = 'Dash is a bird.';
const result = 'Dash is a "bird".';
final regExp = RegExp(r'bird');
final match = regExp.firstMatch(original)!;
final value = original.substring(0, match.start) +
'"' +
match.group(0)! +
'"' +
original.substring(match.end);
expect(value, result);
正規表現の記法
「これぞ正規表現!」的な書き方を書いていきます。
任意の文字列 「.」
任意の一文字は「.」で示します。ただし、改行にはヒットしない。
final reg = RegExp(r'c.t');
expect(reg.hasMatch('cat'), true);
expect(reg.hasMatch('cut'), true);
expect(reg.hasMatch('cutboard'), true);
expect(reg.hasMatch('ct'), false);
expect(reg.hasMatch('cuT'), false);
// 改行にはヒットしない
expect(reg.hasMatch('c{t'), true);
expect(reg.hasMatch('c\nt'), false);
expect(reg.hasMatch('c\tt'), true);
文字列の開始・終了への一致 「開始^ 終了$」
文字列の最初からの場合、正規表現の最初に「^」、文字列の最後の場合、正規表現の最後に「$」をつけます。
否定の表現と間違えやすいです。( [^文字列]⇒否定は括弧が付く )
「この正規表現でこの文字列にはヒットしないはずだが、、」と思ったら、まずこちらを確認すると良いかも知れません。想定外のところでヒットしてたりします。
final regStartWithA = RegExp(r'^A');
final regEndWithA = RegExp(r'A$');
final regOnlyA = RegExp(r'^A$');
expect(regStartWithA.hasMatch('A'), true);
expect(regEndWithA.hasMatch('A'), true);
expect(regOnlyA.hasMatch('A'), true);
expect(regStartWithA.hasMatch('Aab'), true);
expect(regEndWithA.hasMatch('Aab'), false);
expect(regOnlyA.hasMatch('Aab'), false);
expect(regStartWithA.hasMatch('abA'), false);
expect(regEndWithA.hasMatch('abA'), true);
expect(regOnlyA.hasMatch('abA'), false);
複数行への検索
$で文字列の最後にヒットさせる方法を紹介しました。文字列の最後は基本「文字列の最後」です。しかしDartの場合、multiLineのフラグをtrueにすることによって「各行の最後」の扱いにすることが可能です。以下の例では、引数のmultiLineが
- 未設定時とfalseの場合、文字列の最後だけのため1カ所
- trueの場合、改行毎のため3カ所
ヒットすることが分かります。また改行コードが「\n」「\r\n」のどちらでも同じように動作することを確認してます。
multiLineをtrueにすれば、CSVファイルなどでファイル内容全てを一つの文字列として取得した場合でも、一つの正規表現で処理することができます。
const lineN = '1END\n2END\n3END';
const lineRn = '1END\r\n2END\r\n3END';
final regDefault = RegExp(r'END$');
final regSingleLine = RegExp(r'END$', multiLine: false);
final regMultiLine = RegExp(r'END$', multiLine: true);
expect(regDefault.allMatches(lineN).length, 1);
expect(regDefault.allMatches(lineRn).length, 1);
expect(regSingleLine.allMatches(lineN).length, 1);
expect(regSingleLine.allMatches(lineRn).length, 1);
expect(regMultiLine.allMatches(lineN).length, 3);
expect(regMultiLine.allMatches(lineRn).length, 3);
連続した文字列
正規表現が、指定の文字列内にあれば、マッチします。
最初や最後のみにマッチさせたい場合は、「文字列の開始・終了への一致」を使います。
final reg = RegExp(r'ABC');
expect(reg.hasMatch('ABC'), true);
expect(reg.hasMatch('aABC'), true);
expect(reg.hasMatch('ABCc'), true);
expect(reg.hasMatch('AbC'), false);
指定文字列のどれかの文字 「[]」
[]内の指定の文字のどれかにマッチします。
final reg = RegExp(r'[ABC]');
expect(reg.hasMatch('A'), true);
expect(reg.hasMatch('B'), true);
expect(reg.hasMatch('C'), true);
expect(reg.hasMatch('Abc'), true);
expect(reg.hasMatch('aBc'), true);
expect(reg.hasMatch('abC'), true);
expect(reg.hasMatch('abc'), false);
12のメタ文字には、\をつける
メタ文字は、以下の12個: $()*+.?[\^{|
]}はメタ文字ではない。
final regDot = RegExp(r'c\.t');
expect(regDot.hasMatch('cat'), false);
expect(regDot.hasMatch('c.t'), true);
// RegExp(r'[');
// ⇒] がないので、実行エラー(FormatException: Unterminated character class[)
final regOpen = RegExp(r'\[');
final regClose = RegExp(r']');
expect(regOpen.hasMatch('[]'), true);
expect(regClose.hasMatch('[]'), true);
文字の連続 「0以上:* 1以上:+ 0か1:? 数指定:{}」
連続する数を指定するときは、
- 0以上の連続: *
- 1以上の連続: +
- ないか一つだけ: ?
- 数を指定{最小,最大}
- 最小を指定:{最小,}
- 最大と最小を指定{最小,最大}
- 最大を指定:指定不可
.*: 全ての文字にマッチ A*: a,aA,aAAにもマッチ(aはAが0個あるのでマッチする)
https?: httpとhttpsにマッチ
final regZeroOrMore = RegExp(r'A*');
final regOneOrMore = RegExp(r'A+');
expect(regZeroOrMore.hasMatch('a'), true);
expect(regOneOrMore.hasMatch('a'), false);
expect(regZeroOrMore.hasMatch('Aa'), true);
expect(regOneOrMore.hasMatch('Aa'), true);
// ?: 0か1の連続
final regZeroOrOne = RegExp(r'cou?lor');
expect(regZeroOrOne.hasMatch('color'), true);
expect(regZeroOrOne.hasMatch('coulor'), true);
expect(regZeroOrOne.hasMatch('c_lor'), false);
expect(regZeroOrOne.hasMatch('c_ulor'), false);
final regTwo = RegExp(r'^A{2}$');
final regTwoOrMore = RegExp(r'^A{2,}$');
final regBetweenTwoAndFour = RegExp(r'^A{2,4}$');
expect(regTwo.hasMatch('A'), false);
expect(regTwoOrMore.hasMatch('A'), false);
expect(regBetweenTwoAndFour.hasMatch('A'), false);
expect(regTwo.hasMatch('AA'), true);
expect(regTwoOrMore.hasMatch('AA'), true);
expect(regBetweenTwoAndFour.hasMatch('AA'), true);
expect(regTwo.hasMatch('AAA'), false);
expect(regTwoOrMore.hasMatch('AAA'), true);
expect(regBetweenTwoAndFour.hasMatch('AAA'), true);
expect(regTwo.hasMatch('AAAA'), false);
expect(regTwoOrMore.hasMatch('AAAA'), true);
expect(regBetweenTwoAndFour.hasMatch('AAAA'), true);
expect(regTwo.hasMatch('AAAAA'), false);
expect(regTwoOrMore.hasMatch('AAAAA'), true);
expect(regBetweenTwoAndFour.hasMatch('AAAAA'), false);
// 最大回数の指定はできないと思われる
final regBetweenFourOrLess = RegExp(r'^A{,4}$');
expect(regBetweenFourOrLess.hasMatch('A'), false);
単語の区切りは\bを使う
final reg = RegExp(r'\bcat\b');
expect(reg.hasMatch('cat'), true);
expect(reg.hasMatch('cats'), false);
expect(reg.hasMatch('animal cat dog'), true);
expect(reg.hasMatch('animal cats dog'), false);
数字・数字以外 \d \D
final regNumber = RegExp(r'\d');
final regNotNumber = RegExp(r'\D');
expect(regNumber.hasMatch('0'), true);
expect(regNotNumber.hasMatch('0'), false);
expect(regNumber.hasMatch('A'), false);
expect(regNotNumber.hasMatch('A'), true);
アルファベット&アンダーバー&数字・それ以外 \w \W
final regWord = RegExp(r'\w');
final regNotWord = RegExp(r'\W');
expect(regWord.hasMatch('0'), true);
expect(regNotWord.hasMatch('0'), false);
expect(regWord.hasMatch('A'), true);
expect(regNotWord.hasMatch('A'), false);
expect(regWord.hasMatch('_'), true);
expect(regNotWord.hasMatch('_'), false);
expect(regWord.hasMatch('.'), false);
expect(regNotWord.hasMatch('.'), true);
expect(regWord.hasMatch('!@#%^&*()+='), false);
expect(regNotWord.hasMatch('!@#%^&*()+='), true);
空白・空白以外 \s \S
final regSpace = RegExp(r'\s');
final regNotSpace = RegExp(r'\S');
// 空白
expect(regSpace.hasMatch(' '), true);
expect(regNotSpace.hasMatch(' '), false);
// 全角空白
expect(regSpace.hasMatch(' '), true);
expect(regNotSpace.hasMatch(' '), false);
// フォームフィード文字(改ページ)
expect(regSpace.hasMatch('\f'), true);
expect(regNotSpace.hasMatch('\f'), false);
//改行文字
expect(regSpace.hasMatch('\n'), true);
expect(regNotSpace.hasMatch('\n'), false);
// 復帰文字
expect(regSpace.hasMatch('\r'), true);
expect(regNotSpace.hasMatch('\r'), false);
// タブ文字
expect(regSpace.hasMatch('\t'), true);
expect(regNotSpace.hasMatch('\t'), false);
// 垂直タブ文字
expect(regSpace.hasMatch('\v'), true);
expect(regNotSpace.hasMatch('\v'), false);
// 文字 →マッチせず
expect(regSpace.hasMatch('A'), false);
expect(regNotSpace.hasMatch('A'), true);
// アラート文字(ベル)、バックスペース文字、行末の改行を抑止する →マッチせず
expect(regSpace.hasMatch('\a\b\c'), false);
expect(regNotSpace.hasMatch('\a\b\c'), true);
否定 [^~]
final regNotOne = RegExp(r'[^A]');
final regNotSome = RegExp(r'[^ABC]');
expect(regNotOne.hasMatch('A'), false);
expect(regNotSome.hasMatch('A'), false);
expect(regNotOne.hasMatch('ABC'), true);
expect(regNotSome.hasMatch('ABC'), false);
expect(regNotOne.hasMatch('BCA'), true);
expect(regNotSome.hasMatch('BCA'), false);
expect(regNotOne.hasMatch('ABCD'), true);
expect(regNotSome.hasMatch('ABCD'), true);
連続した文字
final regZeroToFive = RegExp(r'[0-5]');
expect(regZeroToFive.hasMatch('0'), true);
expect(regZeroToFive.hasMatch('1'), true);
expect(regZeroToFive.hasMatch('3'), true);
expect(regZeroToFive.hasMatch('5'), true);
expect(regZeroToFive.hasMatch('6'), false);
expect(regZeroToFive.hasMatch('9'), false);
final regAtoE = RegExp(r'[A-E]');
expect(regAtoE.hasMatch('A'), true);
expect(regAtoE.hasMatch('C'), true);
expect(regAtoE.hasMatch('E'), true);
expect(regAtoE.hasMatch('F'), false);
expect(regAtoE.hasMatch('a'), false);
範囲を表さないマイナスは隅に寄せる
final reg1_9 = RegExp(r'[1-9]');
final reg19_ = RegExp(r'[19-]');
final reg_19 = RegExp(r'[-19]');
expect(reg1_9.hasMatch('8'), true);
expect(reg_19.hasMatch('8'), false);
expect(reg19_.hasMatch('8'), false);
expect(reg1_9.hasMatch('-'), false);
expect(reg_19.hasMatch('-'), true);
expect(reg19_.hasMatch('-'), true);
]を[]で使用するときは、\をつける
final reg = RegExp(r'[AB\]]');
final regNot = RegExp(r'[^\]AB]');
expect(reg.hasMatch('A'), true);
expect(reg.hasMatch(']'), true);
expect(reg.hasMatch('C'), false);
expect(regNot.hasMatch('A'), false);
expect(regNot.hasMatch(']'), false);
expect(regNot.hasMatch('C'), true);
文字列内の一致箇所をいくつかに分けてグループとして取得
final regGroup = RegExp(r'^http://([^/]+)/(.+)$');
final matches =
regGroup.allMatches('http://domain.com/path1/path2').toList();
expect(matches.length, 1);
var match = matches[0];
expect(match.groupCount, 2);
expect(match.group(0), 'http://domain.com/path1/path2');
expect(match.group(1), 'domain.com');
expect(match.group(2), 'path1/path2');
// リストでまとめてグループを取得
final groups = match.groups([1, 2]);
expect(groups.length, 2);
expect(groups[0], 'domain.com');
expect(groups[1], 'path1/path2');
グループ名で取得
final regGroup = RegExp(r'^http://(?<domain>[^/]+)/(?<path>.+)$');
final matches =
regGroup.allMatches('http://domain.com/path1/path2').toList();
expect(matches.length, 1);
var match = matches[0];
expect(match.groupCount, 2);
final groupNames = match.groupNames.toList();
expect(groupNames.length, 2);
expect(groupNames[0], 'domain');
expect(groupNames[1], 'path');
expect(match.group(0), 'http://domain.com/path1/path2');
expect(match.group(1), 'domain.com');
expect(match.namedGroup('domain'), 'domain.com');
expect(match.group(2), 'path1/path2');
expect(match.namedGroup('path'), 'path1/path2');
// グループ名を間違えると、ArgumentError が発行される
expect(() => match.namedGroup('wrongName'),
throwsA(isInstanceOf<ArgumentError>()));
パターンのグループ化
final reg = RegExp(r'c(?:at|ut)');
expect(reg.hasMatch('cat'), true);
expect(reg.hasMatch('cut'), true);
expect(reg.hasMatch('cute'), true);
expect(reg.hasMatch('c'), false);
expect(reg.hasMatch('cote'), false);
final regName = RegExp(r'c(?<name>at|ut)');
expect(regName.firstMatch('cat')!.namedGroup('name'), 'at');
expect(regName.firstMatch('cut')!.namedGroup('name'), 'ut');
グループでリピート
^と$で全体で判定させる(一部でも合えばよし、ではなく)。cから始まり、その後がatかutのどちらかの連続にのみヒットする。cが間に入ると、ヒットしない。
final regRepease = RegExp(r'^c(at|ut)+$');
expect(regRepease.hasMatch('cat'), true);
expect(regRepease.hasMatch('catut'), true);
expect(regRepease.hasMatch('catcut'), false);
先読みと後読みを使ったパターン
正規表現でマッチする文字列が欲しいが、マッチさせたい文字列の前後に特定の文字列がある場合のみ取得したい、もしくは、特定の文字が合った場合は除外したい場合に使用する。
final reg = RegExp(r'(?<=<b>)\w*(?=</b>)');
expect(reg.firstMatch('<b>cat</b>')!.group(0), 'cat');
final matches = reg.allMatches('<b>cat</b>1234<b>dog</b>5678<b>fox</b>90');
expect(matches.length, 3);
final result = <String>[];
for (final match in matches) {
result.add(match.group(0)!);
}
expect(result[0], 'cat');
expect(result[1], 'dog');
expect(result[2], 'fox');
肯定先読み: マッチ箇所(?=後の一致条件)
否定先読み: マッチ箇所(?!後の不一致条件)
肯定後読み:(?<=前の一致条件)マッチ箇所
否定後読み:(?<!前の一致条件)マッチ箇所
final dragonQuest = 'DragonQuest';
final dragonBall = 'DragonBall';
final fotuneQuest = 'FortuneQuest';
//肯定先読み
final regBefore = RegExp(r'Dragon(?=Quest)');
expect(regBefore.hasMatch(dragonQuest), true);
expect(regBefore.firstMatch(dragonQuest)!.group(0), 'Dragon');
expect(regBefore.hasMatch(dragonBall), false);
// 否定先読み
final regNotBefore = RegExp(r'Dragon(?!Quest)');
expect(regNotBefore.hasMatch(dragonQuest), false);
expect(regNotBefore.hasMatch(dragonBall), true);
expect(regNotBefore.firstMatch(dragonBall)!.group(0), 'Dragon');
//肯定後読み
final regAfter = RegExp(r'(?<=Dragon)Quest');
expect(regAfter.hasMatch(dragonQuest), true);
expect(regAfter.firstMatch(dragonQuest)!.group(0), 'Quest');
expect(regAfter.hasMatch(fotuneQuest), false);
//否定後読み
final regNotAfter = RegExp(r'(?<!Dragon)Quest');
expect(regNotAfter.hasMatch(dragonQuest), false);
expect(regNotAfter.hasMatch(fotuneQuest), true);
expect(regNotAfter.firstMatch(fotuneQuest)!.group(0), 'Quest');
最大量指定子(デフォルト) と 最小量指定子(?)
最大量指定子は欲張りな(Greedy)なマッチで、なるべく多くの文字をマッチさせようとする。
最小量指定子は控えめな(Reluctant または Non-greedy)なマッチで、少ない文字をマッチさせようとする。
// 0回以上の繰り返し
final regMoreGreedy = RegExp(r'A*');
final regMore = RegExp(r'A*?');
expect(regMoreGreedy.firstMatch('AA')!.group(0), 'AA');
expect(regMore.firstMatch('AA')!.group(0), '');
// 1回以上の繰り返し
final regOneAndMoreGreedy = RegExp(r'A+');
final regOneAndMore = RegExp(r'A+?');
expect(regOneAndMoreGreedy.firstMatch('AA')!.group(0), 'AA');
expect(regOneAndMore.firstMatch('AA')!.group(0), 'A');
// 0回か1回
final regZeroOrOneGreedy = RegExp(r'A?');
final regZeroOrOne = RegExp(r'A??');
expect(regZeroOrOneGreedy.firstMatch('AA')!.group(0), 'A');
expect(regZeroOrOne.firstMatch('AA')!.group(0), '');
// 最小、最大の指定
final regMinMaxGreedy = RegExp(r'A{2,4}');
final regMinMax = RegExp(r'A{2,4}?');
expect(regMinMaxGreedy.firstMatch('AAAAA')!.group(0), 'AAAA');
expect(regMinMax.firstMatch('AAAAA')!.group(0), 'AA');
後方参照 \数字
後方参照とは、正規表現で対象の文字列内でマッチさせた箇所の特定の文字列を、対象の文字列内の後の方で特定の文字列を使用しているか、を確認するときに使用する方法です。標準の正規表現では9カ所までマッチできれば良いですが、Dartではそれ以上でもマッチします(12までは確認。reg12 参照)。
括弧を二重にした場合、前の括弧の位置が早いほうが、グループとして前の番号は割り振られます(regDouble参照)
final reg1 = RegExp(r'(aaa).*\1');
expect(reg1.hasMatch('aaa123aaa'), true);
expect(reg1.hasMatch('aaa123bbb'), false);
final reg12 = RegExp(
r'(1)(2)(3)(4)(5)(6)(7)(8)(9)(0)(11)(12)\1\2\3\4\5\6\7\8\9\10\11\12');
expect(reg12.hasMatch('1234567890111212345678901112'), true);
expect(reg12.hasMatch('1234567890111212345678901110'), false);
final regDouble = RegExp(r'(ab(c|d))_\1\2');
expect(regDouble.hasMatch('abc_abcc'), true);
expect(regDouble.hasMatch('abd_abdd'), true);
expect(regDouble.hasMatch('abc_abcd'), false);
final regHtml = RegExp(r'<(p|pre)>.*<(p|pre)>.*</\2>.*</\1>');
expect(regHtml.hasMatch('<pre>aaa<p>sdaa</p>ksdfs</pre>'), true);
expect(regHtml.hasMatch('<p>aaa<pre>sdaa</pre>ksdfs</p>'), true);
expect(regHtml.hasMatch('<p>aaa<p>sdaa</p>ksdfs</pre>'), false);
Dartで正規表現のコンテキストはサポートされていない
正規表現の置換に「マッチコンテキスト」というものがある。現在のDart(2.18.5)ではサポートしていない、と結論づけた。コンテキストを指定しても、そのまま表示される(ご存じの方、いらっしゃったらご教授ください。「$記号」は、Perl, .Netの置換テキスト方言を書いてます)
- $_:対象テキスト全体
- $&:マッチ
- $`:前コンテキスト(正規表現がマッチする部分の前)
- $':後コンテキスト(正規表現がマッチする部分の後)
final reg = RegExp(r'B');
expect('ABC'.replaceFirst(reg, r'$_'), r'A$_C');
expect('ABC'.replaceFirst(reg, r'\&'), r'A\&C');
expect('ABC'.replaceFirst(reg, r'$&'), r'A$&C');
正規表現の実用的な使い方
郵便番号の確認
final regPostalCode = RegExp(r'^\d{3}-?\d{4}$');
expect(regPostalCode.hasMatch('1234567'), true);
expect(regPostalCode.hasMatch('123-4567'), true);
expect(regPostalCode.hasMatch('123--4567'), false);
expect(regPostalCode.hasMatch('123456'), false);
expect(regPostalCode.hasMatch('12345678'), false);
expect(regPostalCode.hasMatch('1234-567'), false);
expect(regPostalCode.hasMatch('123-456'), false);
expect(regPostalCode.hasMatch('123-45678'), false);
郵便番号を変換
final regPostalCode = RegExp(r'^(\d{3})-?(\d{4})$');
final match = regPostalCode.firstMatch('1234567')!;
expect('${match.group(1)}${match.group(2)}', '1234567');
expect('${match.group(1)}-${match.group(2)}', '123-4567');
final matchMinus = regPostalCode.firstMatch('123-4567')!;
expect('${matchMinus.group(1)}${matchMinus.group(2)}', '1234567');
expect('${matchMinus.group(1)}-${matchMinus.group(2)}', '123-4567');
メールアドレスの確認
final regEmail = RegExp(
caseSensitive: false,
r"^[\w!#$%&'*+/=?`{|}~^-]+(\.[\w!#$%&'*+/=?`{|}~^-]+)*@([A-Z0-9-]{2,6})\.(?:\w{3}|\w{2}\.\w{2})$",
);
expect(regEmail.hasMatch('aaaa@aaaa.com'), true);
expect(regEmail.hasMatch('!.{}@aaaa.com'), true);
expect(regEmail.hasMatch('aaaa@aaaa.co.jp'), true);
expect(regEmail.hasMatch('aaaa.com'), false);
expect(regEmail.hasMatch('aa@aaaaaaa.com'), false);
expect(regEmail.hasMatch('.aaaa@aaaa.com'), false);
日付の確認
final regText = r'^(?<year>20[0-3][0-9])/(?:' +
r'(?<month1>0?2)/(?<day1>[12][0-9]|0?[1-9])|' +
r'(?<month2>0?[469]|11)/(?<day2>30|[12][0-9]|0?[1-9])|' + // 30日の月
r'(?<month3>0?[13578]|1[02])/(?<day3>3[01]|[12][0-9]|0?[1-9])' + //31日の月
r')$';
final regDay = RegExp(regText);
expect(regDay.hasMatch('1999/01/01'), false);
expect(regDay.hasMatch('2000/01/01'), true);
expect(regDay.hasMatch('2039/01/01'), true);
expect(regDay.hasMatch('2040/01/01'), false);
expect(regDay.hasMatch('2000/1/1'), true);
expect(regDay.hasMatch('2000/1/01'), true);
expect(regDay.hasMatch('2000/01/1'), true);
expect(regDay.hasMatch('20000/01/1'), false);
expect(regDay.hasMatch('02000/01/1'), false);
expect(regDay.hasMatch('2000/001/1'), false);
expect(regDay.hasMatch('2000/1/001'), false);
expect(regDay.hasMatch('2022/02/01'), true);
expect(regDay.hasMatch('2022/02/02'), true);
expect(regDay.hasMatch('2022/02/09'), true);
expect(regDay.hasMatch('2022/02/10'), true);
expect(regDay.hasMatch('2022/02/29'), true);
expect(regDay.hasMatch('2022/02/30'), false);
expect(regDay.hasMatch('2022/04/01'), true);
expect(regDay.hasMatch('2022/04/09'), true);
expect(regDay.hasMatch('2022/04/10'), true);
expect(regDay.hasMatch('2022/04/15'), true);
expect(regDay.hasMatch('2022/04/19'), true);
expect(regDay.hasMatch('2022/04/20'), true);
expect(regDay.hasMatch('2022/04/21'), true);
expect(regDay.hasMatch('2022/04/29'), true);
expect(regDay.hasMatch('2022/04/30'), true);
expect(regDay.hasMatch('2022/04/31'), false);
expect(regDay.hasMatch('2022/04/32'), false);
expect(regDay.hasMatch('2022/04/31'), false);
expect(regDay.hasMatch('2022/06/31'), false);
expect(regDay.hasMatch('2022/09/31'), false);
expect(regDay.hasMatch('2022/01/01'), true);
expect(regDay.hasMatch('2022/01/09'), true);
expect(regDay.hasMatch('2022/01/10'), true);
expect(regDay.hasMatch('2022/01/15'), true);
expect(regDay.hasMatch('2022/01/19'), true);
expect(regDay.hasMatch('2022/01/20'), true);
expect(regDay.hasMatch('2022/01/21'), true);
expect(regDay.hasMatch('2022/01/29'), true);
expect(regDay.hasMatch('2022/01/30'), true);
expect(regDay.hasMatch('2022/01/31'), true);
expect(regDay.hasMatch('2022/01/32'), false);
final match = regDay.firstMatch('2022/01/01');
expect(match!.groupNames.contains('month1'), true);
expect(match.groupNames.contains('month2'), true);
expect(match.groupNames.contains('month3'), true);
expect(match.namedGroup('month1'), null);
expect(match.namedGroup('month2'), null);
expect(match.namedGroup('month3'), '01');
日付に変換
DateTime? getDate(String targetDay) {
// final static で定義する?
final regText = r'^(?<year>20[0-3][0-9])/(?:' +
r'(?<month0>0?2)/(?<day0>[12][0-9]|0?[1-9])|' +
r'(?<month1>0?[469]|11)/(?<day1>30|[12][0-9]|0?[1-9])|' + // 30日の月
r'(?<month2>0?[13578]|1[02])/(?<day2>3[01]|[12][0-9]|0?[1-9])' + //31日の月
r')$';
final regDay = RegExp(regText);
if (!regDay.hasMatch(targetDay)) {
return null;
}
final match = regDay.firstMatch(targetDay)!;
for (int i = 0; i < 3; i++) {
if (match.namedGroup('month$i') != null) {
return DateTime(
int.parse(match.namedGroup('year')!),
int.parse(match.namedGroup('month$i')!),
int.parse(match.namedGroup('day$i')!),
);
}
}
}
test('getDate', () {
expect(getDate('2022/01/01'), DateTime(2022, 1, 1));
expect(getDate('2022/01/31'), DateTime(2022, 1, 31));
expect(getDate('2022/02/29'), DateTime(2022, 2, 29));
expect(getDate('20220229'), null);
expect(getDate('2022/04/31'), null);
expect(getDate('AAAA/04/31'), null);
});
HTMLのパースなど複数行にまたがってマッチさせる
改行を含んだ複数行にまたがる対象を検索
\sが改行も含んだ空白で、\Sはそれ以外なので、\s\Sで改行と全ての文字が含まれるみたいです。詳しくは「正規表現の例文-複数行にまたがる処理」まで。
const data = '''
<div>
データ1
</div>
<div>
データ2
</div>
''';
final reg = RegExp(r'<div>[\s\S]*?</div>');
final matchers = reg.allMatches(data).toList();
expect(matchers.length, 2);
expect(matchers[0].group(0), '''
<div>
データ1
</div>''')
expect(matchers[1].group(0), '''
<div>
データ2
</div>''');
日本語
final regHiragana = RegExp('[\u3040-\u309F]');
expect(regHiragana.hasMatch('あ'), true);
expect(regHiragana.hasMatch('を'), true);
expect(regHiragana.hasMatch('ぁ'), true);
expect(regHiragana.hasMatch('だ'), true);
expect(regHiragana.hasMatch('A'), false);
expect(regHiragana.hasMatch('ア'), false);
final regKatakana = RegExp('[\u30A0-\u30FF]');
expect(regKatakana.hasMatch('ア'), true);
expect(regKatakana.hasMatch('ヲ'), true);
expect(regKatakana.hasMatch('ァ'), true);
expect(regKatakana.hasMatch('ダ'), true);
expect(regKatakana.hasMatch('A'), false);
expect(regKatakana.hasMatch('あ'), false);
final regKanji = RegExp('[\u4E00-\u9FFF]');
expect(regKanji.hasMatch('亜'), true);
expect(regKanji.hasMatch('正'), true);
expect(regKanji.hasMatch('A'), false);
expect(regKanji.hasMatch('あ'), false);
expect(regKanji.hasMatch('ア'), false);
中括弧{}の中身を取り出す
ポイント
- \{:中括弧の前はメタ文字のため、スラッシュをつける
- [^}]*:中括弧開くから、中括弧閉じる「以外」の連続した文字列でヒットするようにする。そうすると、中括弧閉じるの前までのデータが取得できる
- ():中括弧の内側を括弧を使うことでグループ化して、中括弧内の文字列をそのまま取得できる
final reg = RegExp(r'id=\{[^}]*}');
expect(reg.hasMatch('id={123}'), true);
expect(reg.firstMatch('id={123}')?.group(0), 'id={123}');
final regGroup = RegExp(r'id=\{([^}]*)}');
expect(regGroup.hasMatch('id={123}'), true);
expect(regGroup.firstMatch('id={123}')?.group(0), 'id={123}');
expect(regGroup.firstMatch('id={123}')?.group(1), '123');
Excelのシート名・セル名を抽出
ポイント
- (?<name>):nameで名前をつけてグループ化
- *?: 最小量指定子
- '?:'の0か1個
シート名の前後に「'」があってもなくても、最小指定子と?を使うことで、シート名のみ抽出できるようにしている。
final reg = RegExp(r"'?(?<sheet>.*?)'?!(?<cell>.*)");
final matchWithQuote = reg.firstMatch("'シート名'!A12")!;
expect(matchWithQuote.namedGroup('sheet'), 'シート名');
expect(matchWithQuote.namedGroup('cell'), 'A12');
final matchWithoutQuote = reg.firstMatch('sheet!A12')!;
expect(matchWithoutQuote.namedGroup('sheet'), 'sheet');
expect(matchWithoutQuote.namedGroup('cell'), 'A12');
CSVファイルの一行をグループで読み込む
const line = 'ナイトパレード,ハリウッドエリア,19:00';
final regNo = RegExp(r'^([^,]*),([^,]*),(\d+):(\d{2})$');
final matchNo = regNo.firstMatch(line);
expect(matchNo, isNotNull);
expect(matchNo!.group(1), 'ナイトパレード');
expect(matchNo.group(2), 'ハリウッドエリア');
expect(matchNo.group(3), '19');
expect(matchNo.group(4), '00');
final regNamed = RegExp(
r'^(?[^,]*),(?[^,]*),(?\d+):(?\d{2})$');
final matchNamed = regNamed.firstMatch(line);
expect(matchNamed, isNotNull);
expect(matchNamed!.namedGroup('name'), 'ナイトパレード');
expect(matchNamed.namedGroup('place'), 'ハリウッドエリア');
expect(matchNamed.namedGroup('hour'), '19');
expect(matchNamed.namedGroup('minute'), '00');
ひらがな から カタカナ への変換
ひらがなとカタカナの文字コードの差が0x60なので、マッチした箇所に対して差分を加算してカタカナにします。
String convert(String value) => value.replaceAllMapped(
RegExp('[ぁ-ゔ]'),
(Match match) =>
String.fromCharCode(match.group(0)!.codeUnitAt(0) + 0x60));
expect(convert('あいを'), 'アイヲ');
片仮名 から 平仮名 への変換
String convert(String value) => value.replaceAllMapped(
RegExp('[ァ-ン]'),
(Match match) =>
String.fromCharCode(match.group(0)!.codeUnitAt(0) - 0x60));
expect(convert('アイヲ'), 'あいを');
使ってみてメモ
正規表現で$がある場合はRawStringは使わない
以下のテストが成立する。そして、マッチする文字列が見つからないorz そのため、$がある場合はRawStringは使わない方がよさそう。さらにRawStringを使わないと、$の後は変数名になるので、エスケープ文字が必要。hasMatchのところもRawStringだと想定通りに動かないなぁ。expect(RegExp(r'$test').hasMatch('test'), false);
expect(RegExp(r'$test').hasMatch('\$test'), false);
expect(RegExp(r'$test').hasMatch('\\\$test'), false);
const test = 'TEST';
expect(RegExp('$test').hasMatch('TEST'), true);
expect(RegExp('\\$test').hasMatch('\\$test'), true);
expect(RegExp('\\$test').hasMatch(r'$test'), false);
まとめ
正規表現について、Dartでの使用方法、正規表現の基本的な記法、実際の使用例をあげました。個人的によく使う正規表現を記載しました。今後も追加していこうと思います。説明もちょこちょこ追加しなくては、、