正規表現: 文字クラス [ ] 内でエスケープしなくてもよい記号

こんにちは、hachi8833です。久しぶりに正規表現についての記事を書いてみました。 一応Ruby(Onigmo)を対象にしましたが、なるべく一般的になるようにしています。エッジケースを扱っているためにシンタックスハイライトがついていけてない部分がありますのでご了承ください。 文字クラス[ ]内のエスケープ 正規表現でのエスケープ、特に記号のエスケープは何かと面倒になりがちです。記号が出てくるたびに片っ端からバックスラッシュ\でエスケープしてばかりだと疲れてしまいます。 しかしつい忘れがちですが、正規表現の中でも文字クラス[ ]の中だけは別世界になっていて、文字クラスの外よりもエスケープが少なくてすみます。 あくまで原則としてですが、文字クラス[ ]の中に限り、以下の4つの記号だけがメタキャラクタ(=機能を持つ記号)として扱われます。 マイナスハイフン – 山形記号 ^ 閉じ角かっこ ] バックスラッシュ \ ということは、それ以外の記号はエスケープなしで楽々と文字クラス[ ]内に置けることになります。本当に?本当に? 文字クラス内メタキャラクタにも種類がある ただし、上の中でバックスラッシュ\と他の3つのメタキャラクタは挙動が異なります。 バックスラッシュ\だけは絶対的なメタキャラクタであり、自分自身を含む直後の文字は何でもエスケープします。 逆に、マイナスハイフン: -、山形記号: ^、閉じ角かっこ: ]の3つは、文字クラス[ ]内では相対的なメタキャラクタです。 具体的には、文字クラス[ ]の中の先頭に置くかどうかで挙動が異なります。 文字クラス[ ]内の山形記号^ 文字クラス[ ]内の先頭が山形記号^だと、文字クラスの否定を表します(Rubularの実行例)。 [^0-9a-zA-Z] 上のようにすると、「英数字以外のあらゆる文字」を表します。これはよく使われるのでご存じの方も多いと思います。 実は、^は文字クラスの先頭以外の場所に置くのであればエスケープ不要です(Rubularの実行例)。 [0-9a-z^A-Z] 正確に言うと、先頭以外の場所であればエスケープしてもしなくて同じです。 文字クラス[ ]内のマイナスハイフン – 文字クラス[ ]内のマイナスハイフン-は、範囲を表すメタキャラクタです。 [a-zA-Z] [a-zA-Z]とすると、小文字のaからzと、大文字のAからZの文字を表します。これもよく使われるのでご存じの方が多いと思います。 文字クラス[ ]内でマイナスハイフン自体を表すには、\^のようにバックスラッシュでエスケープする方法のほかに、以下のように文字クラスの冒頭または末尾に置くことでもできます(Rubularの実行例)。 [a-zA-Z\-] [-a-zA-Z] 上の文字クラスは、どちらもマイナスハイフンと、小文字のaからzと、大文字のAからZの文字を表します。ここでも文字クラスの冒頭が特別扱いされています。 文字クラス[ ]内の閉じ角かっこ ] 次がやや気色悪いです。 文字クラス[ ]内の先頭が閉じ角かっこ ]だと、エスケープなしで通常の文字として扱われます。つまり、]は文字クラスの冒頭においた場合に限り機能しなくなります。 [a-zA-Z&*()\]] []a-zA-Z&*()] たとえば上はいずれも、]a-zA-Z&*()のいずれかにマッチします。 とはいうものの、[]なんちゃら]などと書くと、間違って2回閉じてしまったように見えてしまうので、いくらバックスラッシュでエスケープしなくてよくなるからといっても使いたくないですね。少なくとも私は。 普通に[a-zA-Z&*()\]]と]を\でエスケープする方がなんぼかましです。 さらに、JavaScriptでは[]を空の文字クラスと認識する(regex101.comの実行例)など、実装によって変わる可能性が大なので、この挙動をあてにするとはまると思います。 文字クラス[ ]内で「現実に」エスケープが必要なその他の文字 いよいよ本題です。 さきの原則どおりであれば、文字クラス内に記号を書くときは^-]\の4つの文字だけをエスケープすればよいことになります。 !”#$%&'()*,-./:;<>?@[\]^_`{|}~ 上はASCIIの記号たちです(UTF-8でも共通です)。これを文字クラスの中に書いてエスケープするとしましょう。例の4つのメタキャラクタ「^-]\」をエスケープすると以下のようになります。 !”#$%&'()*,\-./:;<>?@[\\\]\^_`{|}~ しかし実際にやってみると怒られます(Rubularの実行例)。 うすうす見当の付いた方もいらっしゃると思いますが、現実には4つのメタキャラクタの他に、少なくとも以下のエスケープも必要になることがあります。実装によってはこの他にもエスケープが必要になる記号があるかもしれません。 正規表現の開始/終了記号 スラッシュ/が一般的ですが、少なくともRubyでは開始/終了記号を他の文字に変えることもできます。 開始かっこ[ これは実装に依存する可能性があります。少なくともRubyのOnigmoでは文字クラスの中で開始かっこ[もエスケープが必要です。 というわけで、/と[もエスケープしてみました(Rubularの実行例)。 !”#$%&'()*,\-.\/:;<>?@\[\\\]\^_`{|}~ はい、今度はきれいにすべての記号にマッチしました。コロンもセミコロンもドル記号も引用符もパーセントもアンパサンドもハットもエスケープしてません。 まとめ まとめると、少なくともRubyの正規表現の文字クラス内であれば、メタキャラクタである/と^と-と]、そのほかに/と[だけをエスケープすればよいことになります。さらに、^は先頭に置かないように注意すればエスケープ不要です。 それ以外の記号はエスケープなんかしなくたってよいのです(エスケープしても動きますが)。 ここまでわかれば、文字クラスの中をエスケープだらけにせずに安心して記号を書けるようになります。 以下の文字クラスではエスケープをひとつも使っていませんが、ちゃんと機能します(Rubularの実行例)。 [#$@%&*._^(){}] これでエスケープの強迫観念がだいぶ軽くなった気がします。 おまけ ついでながら、文字クラス[ ]の中には順序の概念がありません(例の4つのメタキャラクタの振る舞いはもちろん除きます)。 したがって、[bar]でも[abr]でも同じです。 間違えられやすいのですが、[^bar]は「”bar”でない文字」ではなく、「”b”でも”a”でも”r”でもない文字」です。 おまけ 2 参考までに、RubyやPerlや.NET Frameworkなどにバンドルされているリッチな正規表現ライブラリで、かつ対象がASCII限定であれば、以下のようにUnicodeのカテゴリを指定してASCIIの全記号にマッチさせることもできます(Rubularの実行例)。JavaScriptやsed/awkなどでは残念ながら標準ではサポートされていません。 [\p{P}\p{S}] ただしこれは全角記号や句読点や絵文字を含むありとあらゆる記号にマッチする、やばいぐらい大ざっぱな正規表現です。対象がソースコードのようなものならともかく、一般的な文書だといらん記号にまでがんがんマッチするので大変なことになります。 \p{P}はあらゆる約物(punctuation)、\p{S}はあらゆるシンボル(symbol)を表します。詳しくはUnicode Character Categoriesをご覧ください。 参考 Escape Characters Only When Necessary 関連記事(正規表現) 正規表現の先読み・後読み(look ahead、look behind)を活用しよう Ruby 2.4.1新機能: Onigmo正規表現の非包含演算子(?~ )をチェック [Java] Checkstyleと正規表現でコーディングスタイルをチェックする