【Ruby】正規表現の\sはタブや改行にもマッチする

記事タイトルとURLをコピーする

先日正規表現でスタックしてまい、正規表現やエスケープシークエンスについて調べていたのですが、その途中で気づいたことがあったのでブログにしてみます。

タイトルの通りなのですが、なんと正規表現の\s はスペースだけではなくタブや改行にもマッチしてしまいます。

irb(main):001:0> "\s".match?(/\s/)
=> true
irb(main):002:0> "\t".match?(/\s/)
=> true
irb(main):003:0> "\n".match?(/\s/)
=> true

バックスラッシュを使った記法で\sといえばスペースなので、正規表現でもそうだろうと思うところですが、実は正規表現の\sはより広い概念を表すのですね。
この記事ではなぜこういう結果になるのかについて整理してみたいと思います。
もしもRubyがセンター試験科目だったら絶対頻出ポイントなので、必見です。

文字列リテラルのバックスラッシュ記法について


そもそもバックスラッシュ記法とは、文字列中に\(バックスラッシュ、環境によっては¥マーク)を埋め込むことで、特殊な意味を持たせることができる書き方のことです。 \s以外にも\nで改行、\tでタブなどを表現できます。
ほかにも\改行改行させないなどいろいろな表現ができます。

puts "test\stest"
# => test test

puts "test\ttest"
# => test    test

puts "test\ntest"
# => test
# => test

s = "アイウエオ\
カキクケコ\
さしすせそ"

puts s
# => アイウエオカキクケコさしすせそ
# エディタ上では改行しつつ、実際の文字列としては改行させない

正規表現のバックスラッシュ記法

正規表現でもバックスラッシュ記法を使うことができ、文字列でバックスラッシュ記法と大体同じものを意味します。
大体同じというところがポイントで、マニュアルでも以下のような表現になっています。

文字列リテラルの記法とほぼ同様( リテラル/バックスラッシュ記法)で、以下の記法が利用可能です。

正規表現 (Ruby 2.7.0 リファレンスマニュアル)
数少ない例外のうちの1つが\sです。
なぜ\sが正規表現では単なるスペースではないものとして扱われるのかというと、それが文字クラスの省略記法として定義されているからです。

文字クラス

文字クラスとはなんなのかというと、正規表現において[ ]で囲い、複数の文字を表現する記法です。
例えば [aiueo] はa, i, u, e, o のどれでもマッチします。
[A-Z]は大文字のA~Zまでのアルファベット文字にマッチします。
[a-zA-Z0-9] では、小文字・大文字のアルファベットと数字にマッチします。

irb(main):001:0> /[aiueo]/.match?("server")
=> true
irb(main):002:0> /[A-Z]/.match?("SWX")
=> true
irb(main):003:0> /[a-zA-Z0-9]/.match?("aB3")
=> true
irb(main):004:0> /[a-zA-Z0-9]/.match?("あア亜")
=> false


さらに、この文字クラスのよく使われるパターンには省略した書き方も存在します。
例えば\w[a-zA-Z0-9_]の省略記法で、大文字小文字のアルファベットと数字 に加えて、アンダースコアにマッチします。

irb(main):001:0> /\w/.match?("aB3")
=> true
irb(main):002:0> /\w/.match?("あア亜")
=> false

ここで再び登場するのが\sで、\sも文字クラスの省略記法なのです。
\sは文字クラス[ \t\r\n\f\v]の省略記法なので、スペースのほかにもタブ、キャリッジリターン(\r)、改行、改ページ(\f)、垂直タブ(\v)の全てにマッチすることになります。

irb(main):001:0> /\s/.match?("\v")
=> true
irb(main):002:0> /\s/.match?("\f")
=> true
irb(main):003:0> /\s/.match?("\r")
=> true


これが\sがスペース以外にもマッチすることになる直接の理由となります。

各プログラミング言語との比較

ところで、これまでRubyでの実行を前提として記事を進めてきましたが、他の言語の正規表現でも文字クラスの\sは存在しています。
例えばPythonのドキュメントでは以下のように記載されています。


\s 任意の空白文字とマッチします; これは集合[ \t\n\r\f\v]と同じ意味です。

正規表現 HOWTO — Python 3.8.4rc1 ドキュメント

JavaScriptでも同様です。

\s スペース、タブ、改ページ、改行を含むホワイトスペース文字にマッチします。[ \f\n\r\t\v \u00a0\u1680 \u180e\u2000 -\u200a \u2028\u2029\u202f\u205f \u3000\ufeff]に相当します。

文字クラス

逆に、文字列のリテラル表現でのバックスラッシュ記法1つとして\sでスペースを表現できるかどうかも調べたのですが、少なくともPython, Javascriptに関しては\sでスペースを表現することはできないようです。(\t\nはRubyと同じように使えます。)

# python 3.7
>>> print("test\stest")
test\stest # \sがそのまま出力される
>>> print("test\ttest")
test    test
//  Node.js v12.16.2.
> console.log("test\stest")
teststest # \sがsとして出力される
> console.log("test\ttest")
test    test

ということで、ここまでを整理すると、Rubyでは他の言語にはあまりない珍しい文字列リテラルのバックスラッシュ記法\sでスペースを表現できる。
ところが、正規表現ではより一般的に知られている記法として\sで空白表現一般にマッチする文字クラスが存在しているので、そちらを優先して採用しているのではないかと考えられます。
以上のような経緯で同じ\sでも正規表現と文字列リテラルで異なるものを表現するという状態になったのではないかと思いました。

スペースだけを指定したい


本当にスペースだけをマッチさせたい場合は単にスペースを指定すればOKです。

irb(main):001:0> / /.match?("\s")
=> true


加えて\x20でもスペースだけをマッチさせることができます。
実際のスペースはエディタ上で読みづらいので、こちらの方が好まれるかもしれません。

irb(main):001:0> /\x20/.match?("\s")
=> true

おまけ

ちなみに、ほかにも正規表現のPOSIX 文字クラスという文字クラスの中限定で使える文字クラスの記法があります。
この中に[:space:]といういかにもスペースにマッチしそうなものがあるのですが、こちらもやはりタブと改行などの他の空白表現にもマッチしてしまいます。

irb(main):001:0> /:space:/.match?("\s")
=> true
irb(main):002:0> /:space:/.match?("\n")
=> true
irb(main):003:0> /:space:/.match?("\t")
=> true


正規表現は難しいですね...。