テクニック

正規表現を使う上でのテクニック(パフォーマンスを向上させる方法、移植性の高い方法等)を紹介します。 なお、この記事の内容はMercury::Regexを使う上での話であり、それ以外のライブラリでは状況が異なる場合があります。

汎用的なパターンよりも専用パターンを

同じ文字列にマッチするパターンでも、記述の仕方によってパフォーマンス(速度)が変わることがあります。 一般的に、いくつかの汎用的なパターンを組み合わせたものよりも、専用に用意されたパターンを使用したほうがパフォーマンスが向上します。

例えば、「1回以上の"a"にマッチする正規表現」は"a+"と"aa*"と"a{1,}"の3通りあります。 数学的にはいずれも同値ですが、"a+"が最も高速に動作します。 ここでは、"a+"と"aa*"のコンパイルのされ方の違いを例にとって説明します。

"a+"の場合は、字句解析・構文解析の後で、以下のような専用のNFAを生成します(色がついている場所が受容状態です)。

図1: "a+"のNFA

一方、"aa*"の場合は、以下のように少し面倒な処理を行っています。 まず、"a"を受理するNFAを生成します。

図2: "a"のNFA

次に、"a*"を受理するNFAを生成します。

図3: "a*"のNFA

最後に、この2つ(図2, 3)をε遷移でつなぎ、状態1を受理状態から外します。

図4: "aa*"のNFA

図1と図4を見比べると、どちらがパフォーマンスが高いか一目瞭然ですね。 NFAのことを知らない人でもなんとなくわかると思います。 数学的には同値でも、生成されるNFAやパフォーマンスが全く異なってしまうのです。 逆に言えば、より効率的なNFAを生成する方法があるからこそ限定的なパターン(記述方法)が存在するとも言えます。

同じように、"(a|)"よりも"a?"の方が、"(a|b|c|d|e)"よりも"[abcde]"の方がより効率的なNFAを生成でき、パフォーマンスも向上します。

範囲指定演算子は使わない

「数字のいずれか」の意味で"[0-9]"というパターンを使う人がいますが、この書き方は推奨されません。 なぜなら、"[0-9]"は「数字のいずれか」ではなく「コードポイントが0以上9以下の文字のいずれか」を意味するからです。 ASCIIやUnicodeなど、ほとんどの文字コードでは正解なのですが、移植性を高めようと思うと必ずしも正解とは限りません。 実際にそのような目的(コードポイントが云々)で使うのなら構いませんが、そういうことはまずないと思います。 「数字のいずれか」の意味で使うのであれば、専用に用意された文字クラスやエスケープシーケンス、つまり"[:digit:]"や"\d"を使うことを強く推奨します。 これらは、どのような文字コードでも正しく動作することが保証されています。

気になるパフォーマンスは、エスケープシーケンス>文字クラス>範囲指定の順です(環境によって変わる場合があります)。 とは言っても、その差は微々たるもので、何百・何千回も現れなければ明確な違いは出ません。 いずれも同じNFAを生成するので、コンパイル後のマッチング速度は変わりません。

よほど特殊な理由がない限りは、移植性の高い方法を使いましょう。

なお、使える文字クラスおよびエスケープシーケンスの一覧は正規表現リファレンス#文字クラスおよび#エスケープシーケンスを参照してください。

文字配列を渡すときは長さも一緒に渡す

正規表現パターンやテキストを文字配列として渡すとき、文字列の長さを省略することができます(その場合、NUL文字を終端文字とみなします)。 しかし、文字列の長さがあらかじめわかっているときは長さも渡すことを推奨します。

省略した場合は、一度文字列中をNUL文字が現れるまで数え、長さを測っています。 そのため、あらかじめ長さがわかっている場合は二度手間になってしまうのです。 文字列がとても長い場合や、何度も呼び出す場合は目に見える差が現れます。