【Flutter/Dart】で 正規表現 RegExp

この記事の対象者

    以下の方が読んで勉強・コピペできる記事になってます。

  • 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');

正規表現の記法

「これぞ正規表現!」的な書き方を書いていきます。

任意の文字列 「.」

任意の一文字は「.」で示します。

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);

文字列の開始・終了への一致 「開始^ 終了$」

文字列の最初からの場合、正規表現の最初に「^」、文字列の最後の場合、正規表現の最後に「$」をつけます。
否定の表現と間違えやすいです。( [^文字列]⇒否定は括弧が付く )
「この正規表現でこの文字列にはヒットしないはずだが、、」と思ったら、まずこちらを確認すると良いかも知れません。想定外のところでヒットしてたりします。

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);

連続した文字列

正規表現が、指定の文字列内にあれば、マッチします。
最初や最後のみにマッチさせたい場合は、「文字列の開始・終了への一致」を使います。

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以上の連続: *
  • .*: 全ての文字にマッチ A*: a,aA,aAAにもマッチ(aはAが0個あるのでマッチする)

  • 1以上の連続: +
  • ないか一つだけ: ?
  • 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);

否定 [^~]

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 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');/code>

グループ名で取得

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');

最終的にマッチに加わらない部分(先読み・後読み)

    
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 regMore = RegExp(r'A*');
final regLess = RegExp(r'A*?');
final regOneAndMore = RegExp(r'A+');
final regOneAndLess = RegExp(r'A+?');
 
expect(regMore.firstMatch('AA')!.group(0), 'AA');
expect(regLess.firstMatch('AA')!.group(0), '');
expect(regOneAndMore.firstMatch('AA')!.group(0), 'AA');
expect(regOneAndLess.firstMatch('AA')!.group(0), 'A');

正規表現の実用的な使い方

郵便番号の確認

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);
});

日本語

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);

まとめ

正規表現について、Dartでの使用方法、正規表現の基本的な記法、実際の使用例をあげました。個人的によく使う正規表現を記載しました。今後も追加していこうと思います。説明もちょこちょこ追加しなくては、、

参考