Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails以外の開発一般

はじめての正規表現とベストプラクティス#8: 対象の構造を意識した「適度にDRYな」書き方

対象の構造を意識して正規表現を書く

日本語のような自然言語を対象とする場合、初めのうちはついつい「マッチすればそれでいい」的な正規表現を書いてしまいがちです。そういうときの気持ちになって、わざと雑な正規表現を書いてみます。

  • 例: モビルスーツ(ジオン公国軍)の型番にマッチする正規表現: /(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

詳しくは以下の記事をご覧ください。

はじめての正規表現とベストプラクティス#5(特別編)`|`と部分マッチのワナ

参考

参考: 宇宙世紀の登場機動兵器一覧 - Wikipedia


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。