- 1: 基本となる8つの正規表現
- 2: 正規表現とは何か/ワイルドカードとの違い
- 3: 冒頭/末尾にマッチするメタ文字とセキュリティ、文字セットの否定と範囲
- 4: 先読みと後読みを極める
- 5(特別編)
|
と部分マッチのワナ - 6: 文字セットのショートハンド
- 7: Unicode文字ポイントとUnicode文字クラス
- 8: 対象の構造を意識した「適度にDRYな」書き方(本記事)
- 9:
.*
や.+
がバックトラックで不利な理由 - 10: 危険な「Catastrophic Backtracking」前編
⚓対象の構造を意識して正規表現を書く
日本語のような自然言語を対象とする場合、初めのうちはついつい「マッチすればそれでいい」的な正規表現を書いてしまいがちです。そういうときの気持ちになって、わざと雑な正規表現を書いてみます。
- 例: モビルスーツ(ジオン公国軍)の型番にマッチする正規表現: /
(MSM|YMS|MSN|MAN|MAM|MAX?|MS)-([\d]{1,2}([ABFHJSRX])?|[XR][\d]{1,2})
/(Rubular)
しかし上の[\d]{1,2}
という書き方だと、数値が2桁でありさえすればよいという発想なので、「MS-99」のような存在しない型番にいくらでもマッチしてしまいます。
次はそうした不要なマッチをストレートに排除してみましょう。
- 例: 上を元に手直し: /
\b(YMS-07B|MAN-(0[378]|X[38])|MAM-07|MSN-[0X]2|MAX-03|MSM-(07S|04F|0[347]|10)|YMS-1[45][AS]?|MS-(0[5679][BFJRS]?|1[14][AS]?)|MS-X1[06]|MS-R09|MA-(0[58]|04X|05H))\b
/(Rubular)
先ほどより随分長くなりましたが、型番の構造が正規表現の中でだいぶ見えてきました。以下のように|
のところで分解してみるとさらにわかりやすくなります。
\b(
YMS-07B|
MAN-(0[378]|X[38])|
MAM-07|
MSN-[0X]2|
MAX-03|
MSM-(07S|04F|0[347]|10)|
YMS-1[45][AS]?|
MS-(0[5679][BFJRS]?|1[14][AS]?)|
MS-X1[06]|
MS-R09|
MA-(0[58]|04X|05H)
)\b
最初に書いた正規表現と比べて、何となくメンテナンス性が高まっているのがおわかりでしょうか。書き捨ての正規表現ならともかく、長期に渡ってメンテナンスする可能性のある正規表現であれば、このような「DRYにしすぎない」「構造を適度に保った」書き方の方が有利だと私は考えます。
参考: Don't repeat yourself - Wikipedia -- DRYの解説です
⚓対象リストを元に正規表現を作るのもひとつの方法
パターンを以下のような降順ソート済みリストの形にして、各行を全部|
でつないで「そのまんま」な正規表現にするのもありです。経験上このような文字列が対象であれば、むしろ降順ソート済みリストを元に正規表現を作り始める方が、結果的に早道だと感じています。
YMS-15
YMS-14
YMS-07B
MSN-X2
MSN-02
MSM-10
MSM-07S
MSM-07
MSM-04F
MSM-04
MSM-03
MS-X16
MS-X10
MS-R09
MS-14S
MS-14A
MS-14
MS-11
MS-09R
MS-09
MS-07B
MS-07
MS-06S
MS-06J
MS-06F
MS-06
MS-05B
MAX-03
MAN-X8
MAN-X3
MAN-08
MAN-07
MAN-03
MAM-07
MA-08
MA-05H
MA-05
MA-04X
もちろんこのアプローチは、正規表現でチェックしたいパターンのリストが有限かつ大きすぎないことが前提です。だいたい100行を超えるようなら、正規表現だけで頑張らずにコードも用いる方法を検討する方がよいかもしれません。
上のリストをそのまま正規表現に用いると以下のようになります。このまま使っても構いません。
- 例: /
\bYMS-15|YMS-14|YMS-07B|MSN-X2|MSN-02|MSM-10|MSM-07S|MSM-07|MSM-04F|MSM-04|MSM-03|MS-X16|MS-X10|MS-R09|MS-14S|MS-14A|MS-14|MS-11|MS-09R|MS-09|MS-07B|MS-07|MS-06S|MS-06J|MS-06F|MS-06|MS-05B|MAX-03|MAN-X8|MAN-X3|MAN-08|MAN-07|MAN-03|MAM-07|MA-08|MA-05H|MA-05|MA-04X\b
/(Rubular)
余力があれば、このリストの構造を残しながら以下のように「適度に」DRYに変えてみましょう。
YMS-(15|14|07B)|
MSN-(X2|02)|
MSM-(10|07S?|04F?|03)|
MS-(X16|X10|R09|14S|14A|14|11|09R?|07B?|06S|06J|06F|06|05B)|
MAX-03|
MAN-(X8|X3|08|07|03)|
MAM-07|
MA-(08|05H?|04X)
後は上の改行を|
に置き換えて\b
で挟めば完了です。
- 例: /
\bYMS-(15|14|07B)|MSN-(X2|02)|MSM-(10|07S?|04F?|03)|MS-(X16|X10|R09|14S|14A|14|11|09R?|07B?|06S|06J|06F|06|05B)|MAX-03|MAN-(X8|X3|08|07|03)|MAM-07|MA-(08|05H?|04X)\b
/(Rubular)
やはり冒頭の/(MSM|YMS|MSN|MAN|MAM|MAX?|MS)-([\d]{1,2}([ABFHJSRX])?|[XR][\d]{1,2})
/という正規表現よりは長いのですが、後で型番を修正・追加することを考えれば、私は最終的にこのような「適度にDRY」な正規表現を使いたいと思います。それでいて「MS-99」のような存在しない型番に誤ってマッチすることもありません(ちゃんと書けばですが)。
もちろん、どこまで構造を保ち、どこまでDRYにするかは状況によって異なります。()
の数が増えるとグルーピングが増えてパフォーマンスが落ちる可能性もあるといえばありますので、Rubyであれば(?:正規表現)
と書くことでグループ化をキャンセルする手もあります(詳しくは今後の記事で)。
最終的な正規表現では「07」と「07S」の両方にマッチさせるために07S?
としていますが、あえて(07S|07)
と並列させるのもありだと思います。
⚓|
の部分マッチに注意
ただし、(06S|06J|06F|06)
のような並列の部分マッチについては、(06|06S|06J|06F)
のように短い06
を最初に置いてしまうと、以下のように「MS-06S」のマッチが「MS-06」で止まってしまい、末尾の「S」「J」「F」が落ちてしまいます。そうならないようにするために、上のリストを降順でソートしました。
- 例: 一部のマッチに失敗している(Rubular)
詳しくは以下の記事をご覧ください。