初心者歓迎!手と目で覚える正規表現入門・その3「空白文字を自由自在に操ろう」

  • 150
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

みなさんこんにちは!
この記事は「手と目で覚える正規表現入門」の第3回です。

この連載記事は「知識ゼロからでも理解できる」「実践的なサンプルを提供する」「自分の手と目で動きを確認できる」をモットーにした、正規表現の入門記事です。

今回は行頭や行末といった「位置」を表す正規表現や、タブ文字や改行文字といった目に見えない空白文字を操作する方法を説明します。

対象となる読者

本記事は正規表現の予備知識が全くない「正規表現初心者」を対象としています。
ただし連載記事なので、読者のみなさんは過去の記事で紹介した知識をすべて理解できている、という前提で進めます。

まだ第1回、第2回の記事を読んでない人は、先にそちらを読んでからこの記事に戻ってきてください。

初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」 - Qiita

初心者歓迎!手と目で覚える正規表現入門・その2「微妙な違いを許容しつつ置換しよう」 - Qiita

今回も前回までの記事と同様、今回もRubularを使って実際に正規表現を動かしながら学習できるようにしています。
また、置換処理ではテキストエディタのAtomを使用しています。

しっかりと内容が頭に定着するよう、実際に自分の手を動かしながら読み進めることを強くオススメします!

この記事で学ぶこと

本記事ではソースコードの「お掃除」や、アプリケーションログの「取捨選択」を通じて、正規表現に関する以下のような知識を学びます。

  • ^$\s\t\n の意味
  • | を使ったOR検索
  • 環境によって異なる改行コードと正規表現の関係
  • 使われる場所によって異なる ^ の意味

今回までの内容が日常的によく使う正規表現のほとんどだと思います。
なので、第1回から今回までの内容を理解できれば、正規表現初心者は卒業、といってもよいでしょう。
がんばってマスターしてください!

動作環境

本記事の正規表現は以下の環境で動作確認しています。

  • Ruby
  • JavaScript
  • Atom

正規表現が使えるプログラミング言語やテキストエディタであれば、本記事のサンプルコードはほぼ同じように動くはずですが、正規表現の仕様や使えるメタ文字が微妙に異なることもあります。
何か動きがおかしいなと思ったら、自分が使っている環境の正規表現の仕様を確認するようにしてください。

それでは第3回の本編を始めましょう!

スペースやタブ文字の入った空行を見つける

こんなRubyスクリプトがあります。


def hello(name)
  puts "Hello, #{name}!"
end

hello('Alice')
          
hello('Bob')
	
hello('Carol')

一見、何も問題無さそうに見えますが、実は空行の一部にスペースやタブ文字が含まれています。

空行にスペースやタブ文字が含まれていても意味が無いので、おかしな行をあぶり出してやりましょう。

まず、先ほどのテキストをRubularに貼り付けてください。

Kobito.VrSRhS.png

続いて、Your regular expression欄に  + (スペースとプラス)と入力してください。
(過去の記事を読んだ方はわかると思いますが、これは「スペースが1文字以上続く」という意味の正規表現です)
するとRubularは次のようにマッチした文字列を表示します。

Kobito.SMReHx.png

どうやら hello('Alice')hello('Bob') の間にスペースが含まれるようですね。
しかし、1行目や2行目にある正常なスペースまで検出してしまっています。

そこで先ほどの正規表現を ^ + に変えてみましょう。

Kobito.gBAr4d.png

するとマッチする部分がちょっと減りました。

ここで使った ^ は「行頭」を表すメタ文字です。
文字そのものではなく、マッチした 「位置」 を示すので、こういったメタ文字を特に アンカー と呼びます。

つまり、^ + は「行頭からスペースが1文字以上続く」という意味になります。

ただ、このままだと2行目にある puts "Hello, #{name}!" までマッチしています。
この行は除外すべきですね。

なので、さらにメタ文字を加えて ^ +$ という正規表現を使います。
$^ の反対で、「行末」を意味するメタ文字(アンカー)です。
つまり、^ +$ は「行頭から行末までスペースが1文字以上続く」という意味になります。

Rubularに ^ +$ と入力してみてください。

Kobito.ov3eqL.png

これで空行に含まれるスペースだけを検出できました!

さて、スペースが含まれる空行は見つかりましたが、タブ文字が含まれる空行はまだ見つかっていません。
こちらも正規表現を使って検出してみましょう。

タブ文字は \t というメタ文字(文字クラス)を使って表現できます。

Rubularに ^[ \t]+$ という正規表現を入力してみましょう。

Kobito.9WEHP2.png

おっ、hello('Bob')hello('Carol') の間にもタブ文字が隠れていたようですね!

これでスペースやタブの入った空行を見つけだすことができました。

空行に含まれる無駄なスペースやタブ文字を削除する

さて、変な行を見つけておしまい、ではなく、変な行はきれいにお掃除してあげましょう。
正規表現が使えるテキストエディタであれば簡単に無駄な空白文字を削除できます。

先ほどのテキストAtomにコピペしてください。


def hello(name)
  puts "Hello, #{name}!"
end

hello('Alice')
          
hello('Bob')
	
hello('Carol')

それから ^[ \t]+$ という正規表現を入力しましょう。
Rubularと同じように無駄なスペースやタブ文字が選択されるはずです。

Kobito.2FAhWe.png

置換語の文字列(Replace in current buffer欄)は何も入力しなくてOKです。
このまま Replace All ボタンをクリックすれば、無駄なスペースやタブ文字を削除できます。

QqN068ESrE.gif

Rubyで実行してみる

上の処理をRubyで実行するとこうなります。
画面上では違いがわかりませんが、無駄なスペースやタブ文字が削除されています。


text = <<-TEXT
def hello(name)
  puts "Hello, \#{name}!"
end

hello('Alice')
     
hello('Bob')
	
hello('Carol')
TEXT

puts text.gsub(/^[ \t]+$/, '')
# def hello(name)
#   puts "Hello, #{name}!"
# end
# 
# hello('Alice')
# 
# hello('Bob')
# 
# hello('Carol')

JavaScriptで実行してみる

JavaScript(JS)の場合、^$ を意図したとおりにマッチさせるためには m オプション(複数行検索オプション)を付ける必要があります。
下記のコードにある /^[ \t]+$/gm の m がそのオプションです。
(g はグローバルオプションです。こちらは第1回で紹介しました。)

こちらもやはり画面上だと違いがわかりませんが、無駄なスペースやタブ文字が削除されています。

var text = "def hello(name)\n  puts \"Hello, \#{name}!\"\nend\n\nhello('Alice')\n     \nhello('Bob')\n\t\nhello('Carol')\n";

console.log(text.replace(/^[ \t]+$/gm, ''));
// def hello(name)
//   puts "Hello, #{name}!"
// end
// 
// hello('Alice')
// 
// hello('Bob')
// 
// hello('Carol')

行末に入った無駄なスペースを削除する

先ほどの ^[ \t]+$ を応用すれば、いろいろと便利に活用できます。
たとえば、次のコードは行末に無駄なスペースが含まれています。

def hello(name)   
  puts "Hello, #{name}!"
end      

行末の無駄なスペースは [ \t]+$ という正規表現で抜き出せます。
これは「スペースまたタブ文字が行末まで1文字以上続く」という意味ですね。

以下はRubularの実行結果です。

Kobito.KTY5eo.png

あとは先ほどと同じようにテキストエディタで置換すれば行末の無駄なスペースを削除できます。
(実は最初の例題も [ \t]+$ でOKなのでした)

とはいえ、こういった無駄な空白文字の削除はIDEやプラグインの類が自動的に処理してくれることが多いので、実務ではそっちを使った方が便利だと思います。
これはあくまで正規表現のお勉強用と考えてくださいね。

インデントがガタガタになったテキストを左寄せにする

反対に ^[ \t]+ とすれば「行頭からスペースやタブ文字が1文字以上続く」の意味になります。
次のようにインデントがガタガタになったテキストも ^[ \t]+ を使えば簡単に左寄せできます。

  Lorem ipsum dolor sit amet.
Vestibulum luctus est ut mauris tempor tincidunt.
         Suspendisse eget metus
      Curabitur nec urna eget ligula accumsan congue.

0g58CcElLT.gif

不揃いなスペースを揃える

次はこんなRubyのハッシュリテラルをきれいにしてみます。


{
  japan:	'yen',
  america:'dollar',
  italy:     'euro'
}

画面上ではちょっとわかりにくいですが、コロン(:)のあとのスペースがいろいろおかしいです。

  • japan: のうしろにはスペースではなくタブ文字が入っています
  • america: のうしろにはスペースがありません
  • itally: のうしろのスペースが多すぎます

というわけで、これを「スペース1個」に揃えてみましょう。

ここでは「コロンの後ろにスペースまたはタブ文字が0文字以上」という正規表現を使います。
つまり以下のような正規表現になります。

:[ \t]*

あとは置換後の文字列を (コロンとスペース)にすればOKです。

Jn12MD8RCs.gif

[ \t] の代わりに \s を使ってみる

正規表現には \s というメタ文字があります。
これは半角スペースやタブ文字、改行文字など、目に見えない「空白文字全般」を表す文字クラスです。

なので先ほどの正規表現は次のように書くことも可能です。

:\s*

ご覧通り、これでも同じように不揃いなスペースを揃えることができました。

oWpzrQJz8q.gif

ただし、\s を使う場合は次のような点に注意してください。

1. \s に含まれる文字が言語や環境によって異なる

まず、言語や環境によって \s に含まれる文字が微妙に異なります(これは \s に限った話ではありませんが)。
次に示すように、Ruby と JavaScript(JS) でも \s の内容が違います。

Rubyの場合参考

  • \s = [ \t\r\n\f]

JSの場合参考

  • \s = [ \f\n\r\t\v​\u00a0\u1680​\u180e\u2000​-\u200a​\u2028\u2029\u202f\u205f​\u3000\ufeff]

Rubyの場合は半角スペース( )、タブ文字(\t)、改行文字(\n)、復帰文字(\r)、改ページ文字(\f)だけですが、JSの場合は全角スペース(\u3000)のような空白文字も \s に含まれます。

2. \s には改行文字や復帰文字も含まれる

上に示したように、\s には改行文字(\n)や復帰文字(\r)も含まれます。
なので、何も考えずに \s だけを検索して削除すると、改行文字もろとも削除されて、全部の行が1行になってしまいます。

wRrTsozVLF.gif

というわけで、意図的に改行文字を削除したいのでなければ、他に文字列やメタ文字を組み合わせるなどして \s に不特定多数の空白文字がマッチしないような正規表現を作りましょう。

ちなみに、このあとで改行文字を意図的に削除する例題が登場します。

カンマ区切りをタブ区切りに、タブ区切りをカンマ区切りに置換する

非常に地味ですが、正規表現で意外と便利なのがタブ文字や改行文字など、検索・置換用のテキストボックスに入力しづらい文字を簡単に扱えることです。

たとえば次のようなカンマ区切りのテキストがあったとします。

name,email
alice,alice@example.com
bob,bob@example.com

これをカンマ区切りではなく、タブ文字区切りにしたい、と思ったら、以下のようにすればOKです。

  • 検索文字列 = , (これは正規表現ではないただの文字)
  • 置換文字列 = \t (これは正規表現のタブ文字)

以下はAtomでの実行結果です。

Yvj0QsQb1B.gif

タブ文字区切りにすると、ExcelやGoogleスプレッドシートにコピペで値を貼り付けられるので便利なんですよね。

Kobito.MERLe4.png

逆にタブ区切りからカンマ区切りに変換するのも簡単です。
もうおわかりだと思いますが、次のように指定すればOKです。

  • 検索文字列 = \t
  • 置換文字列 = ,

riwU517p69.gif

たったこれだけ?と思うようなことですが、正規表現(のメタ文字)を使わずにやろうとすると結構苦労するんじゃないでしょうか?(もしくは無理かも?)

ログから特定の文字を含む行を削除する

さて、次のサンプルに移りましょう。
今度はログファイルを扱います。

以下はHeroku上に出力されたRailsのログを、今回のサンプル用にちょっと加工したものです。

Feb 14 07:33:02 app/web.1:  Completed 302 Found ...
Feb 14 07:36:46 heroku/api:  Starting process ...
Feb 14 07:36:50 heroku/scheduler.7625:  Starting ...
Feb 14 07:36:50 heroku/scheduler.7625:  State ...
Feb 14 07:36:54 heroku/router:  at=info method=...
Feb 14 07:36:54 app/web.1:  Started HEAD "/" ...
Feb 14 07:36:54 app/web.1:  Completed 200 ...

このアプリケーションはブラウザからWebページにアクセスされる場合と、定期的に起動するスケジューラーが動作する場合の2パターンあります。

Webページへのアクセスは app/webheroku/router と表示され(1行目と5~7行目)、スケジューラーの実行ログは heroku/apiheroku/scheduler のように表示されます(2~4行目)。

今はWebページへのアクセスだけを確認したいのですが、スケジューラーの実行ログが混在していると非常に目障りです。
そこで、正規表現を使ってスケジューラーの実行ログを削除してやりましょう。

まず、上のテキストをRubularに貼り付けてください。

Kobito.sRDPAs.png

最初に heroku/api が含まれる行を選択します。
ここでは次のような正規表現を指定することにしましょう。

^.+heroku\/api.+$

この正規表現の意味は「行頭からの何らかの文字が1文字以上続き(^.+)、"heroku/api"が現れ(heroku\/api)、その後行末まで何らかの文字が1文字以上続く(.+$)」という意味です。
なお、\/ のバックスラッシュ(\)はスラッシュ(/)をエスケープするためのエスケープ文字です。(第2回の記事を参照)

この正規表現をRubularに入力すると heroku/api が含まれる行を選択できました。

Kobito.Wb9VQk.png

同じ要領で以下のような正規表現を作れば、heroku/scheduler が含まれる行を選択できます。

^.+heroku\/scheduler.+$

Kobito.XEL1a3.png

しかし、できれば heroku/api の行と heroku/scheduler の行をまとめて選択したいですね。

こういう場合は | というメタ文字を使います。
ABC|DEF のように書くと、「文字列ABC、または文字列DEF」という OR条件 の意味になります。
実際にはOR条件の範囲を明確にするため、(ABC|DEF) のようにグループ化の ( ) と一緒に使われることが多いです。

というわけで、「heroku/api または heroku/scheduler が含まれる行」を選択したい場合は次のような正規表現を使います。
(正規表現を簡潔にするため、(api|scheduler) というOR条件にしました)

^.+heroku\/(api|scheduler).+$

Rubularに入力すると、期待した通りに不要な行を検索できました!

Kobito.jSVXVD.png

さて、正規表現の動きを確認できたら置換処理を実行するために Atom にバトンを渡しましょう。
Rubularで使ったテキストと正規表現をAtomにコピペしてみてください。

Kobito.Ue52iI.png

これで Replace All ボタンをクリックすれば、邪魔な行が削除されるはずです。
えいっ!

Kobito.FtBWyx.png

んー。。。
これでいいと言えばいいかもしれませんが、なんかスッキリしませんね。
できれば以下のように空行を残さずに行を全部詰めてほしいところです。

Feb 14 07:33:02 app/web.1:  Completed 302 Found ...
Feb 14 07:36:54 heroku/router:  at=info method=...
Feb 14 07:36:54 app/web.1:  Started HEAD "/" ...
Feb 14 07:36:54 app/web.1:  Completed 200 ...

大丈夫です。正規表現をちょっと変えれば、ちゃんと空行を残さずに削除できます。

行を詰めるということはすなわち「改行文字を削除すること」です。
$ は「行末」を表すメタ文字ですが、このままだと改行文字は含まれません。
$ の代わりに \n を指定してやると、検索結果に改行文字も含まれるようになります。

よって、先ほどの正規表現を以下のように変更してみてください。

^.+heroku\/(api|scheduler).+\n

こうしてやると空行を残さずに削除できます。
(ただし、Windows環境では削除できないかもしれません。理由は後述します。)

Tn0ImcxbXw.gif

念のため、^.+heroku\/(api|scheduler).+\n の意味を確認しておくと次のようになります。

「行頭からの何らかの文字が1文字以上続き(^.+)、"heroku/"が現れ(heroku\/)、"api" または "scheduler" が続き((api|scheduler))、その後何らかの文字が1文字以上続いて(.+)、改行文字で終わる(\n)」

ちょっとややこしいですが、ここまでの内容をしっかり理解できていればこの正規表現も読み下せるはずです。
今回紹介する正規表現は以上になります。

捕捉:Windows環境とMac/Linux環境の改行コードの違いを考慮する

先ほど紹介した ^.+heroku\/(api|scheduler).+\n という正規表現ですが、筆者のWindows環境のAtomで実行するとうまく置換してくれませんでした。

ご存知の方も多いと思いますが、Windows環境の改行コードはCRLF(\r\n)です。
一方、Mac/Linux環境ではLF(\n)です。

そして、どうやらWindows環境のAtomにテキストをコピペすると、自動的に改行コードがCRLFに変換されてしまうみたいです。
Atomのウインドウにも CRLF と書いてあります。(Mac環境ではLFです)

Screen Shot 2016-02-15 at 05.15.46.png

.+\n と書くと「(\r\n を除く)任意の文字が1文字以上続き、\n で終わる」という意味になります。
このため、\r\n で終わるWindows環境のテキストにはマッチしなくなります。
これがうまく置換できない原因です。

そこで、\n でも \r\n でも動作するよう、先ほどの正規表現を工夫してやりましょう。
これは次のようにすればOKです。

^.+heroku\/(api|scheduler).+\r?\n

? は「直前の文字が1個、またはゼロ」を意味するメタ文字です。(第2回で紹介しました)
なので、\n でも \r\n でもマッチするようになります。

以下はWindows環境での実行例です。
ちゃんと不要な行を削除できてますね。

NlwlE1DlnP.gif

このようにWindowsとMac/Linuxでは改行コードの違いで正規表現の挙動にも差異が発生することがあるため、改行文字を含む正規表現を使うときは環境の違いに注意してください。

コラム:使われる場所によって役割が異なる ^ を理解する

ところで、今回「行頭を表すメタ文字」として登場した ^ は第2回の記事にも登場しました。
覚えていますか?
[^AB] のような使い方をするときの ^ です。
[ ] の中で ^ が使われると「AでもなくBでもない文字1文字」というふうに [ ] の意味を否定条件に変えるのでした。

もっというと、[AB^] にするとさらに意味が変わります。
これは「AまたはBまたは^のいずれか1文字」の意味になります。
否定条件になるのは [ ] の先頭に ^ が来たときだけです。

念のため、Rubular上で確かめてみましょう。
以下のテキストを検索用に使います。

ABCDEF
!@#$%^&*

[^AB] は「AでもなくBでもない文字1文字(AとB以外の文字)」に合致します。

Kobito.O4bYL6.png

[AB^] は「AまたはBまたは^のいずれか1文字」です。

Kobito.IUEacp.png

ついでにいうと、^. にすれば「行頭にくる任意の1文字」の意味になります。

Kobito.ZCJfzr.png

"^" という文字だけを検索したい場合は \^ というようにバックスラッシュを付けてエスケープしてやります。

Kobito.8uZ9hN.png

"^"という文字そのものを検索したいという機会はあまりないと思いますが、行頭を表す ^ と、否定条件を作る [^ ] は使用頻度が高いので、混同しないように注意してください。

まとめ

本記事では以下のようなことを学びました。

  • ^ は行頭を表す
  • $ は行末を表す
  • \t はタブ文字を表す
  • \n は改行文字を表す
  • \s は空白文字(スペース、タブ文字、改行文字等)を表す
  • ABC|DEF は「文字列ABCまたは文字列DEF」のOR条件を表す
  • 改行コードは環境によって異なる場合がある
  • ^ は行頭の意味になったり、[^ ] で否定の文字クラスの意味になったりする

第1回、第2回、そして今回の第3回の内容を理解できれば、日常的によく使う正規表現の大半をカバーできていると思います。
今まで正規表現が謎の暗号にしか見えなかった人も、そろそろ「正規表現が読めるっ!書けるぞっ!」と感じてきたのではないでしょうか?
そして単純な文字列検索にはマネできない「正規表現ならではの便利さ」を実感できてきているはずです!

さて、初心者向けにはこれでおしまい、としても良いのですが、知っておくと便利なテクニックやメタ文字がもうちょっとだけあります。
第4回ではそういった内容を説明する予定です。
いわば「中級者向け」の内容ですね。

具体的には以下のような内容になります。

  • \b\A\z\W\D\S\B の意味
  • {n,}{,n} の意味
  • 先読み、後読みを使った検索
  • 後方参照を使った検索
  • ややこしいエスケープ処理や、使われる場所によって役割が異なるメタ文字の扱い
  • メタ文字を組み合わせたちょっと複雑な正規表現
  • 正規表現のパフォーマンス

うーん、こうやってリストアップすると結構たくさんあるので、全部は紹介しきれない気もしてきました・・・。
実際に記事を書きながらどこまで説明するかを考えたいと思いますw

なお、この記事をストックしてもらえると、新しい記事を公開したときに更新を通知しますので、よかったら ストック をお願いします。

それでは今回はこのへんで!

2016.02.23追記:第4回の記事を公開しました!
第4回の記事を公開しました。
ここまで読めば中級者レベルの正規表現もマスターできます。
がんばって学習していってください!

初心者歓迎!手と目で覚える正規表現入門・その4(最終回)「中級者テクニックをマスターしよう」 - Qiita

説明する内容

  • \b の意味
  • 肯定の先読み、後読み
  • 否定の先読み、後読み
  • 後方参照
  • メタ文字の複雑な組み合わせ
  • 正規表現とパフォーマンス
  • メタ文字のエスケープ
  • [ ] 内のメタ文字の働き
  • {n,}{,n} の意味
  • \W\S\D\B の意味