javascriptで正規表現ぺろぺろ

正規表現の話はweb上の至る所に書いてあるので、今さら書くまでもないですが復習ついでにちょろっとまとめておきます。プログラマーなら知っていて当然、知らなきゃもぐり認定されます(します)。プログラマー以外のPC事務に携わる人にとっても便利な概念だと思いますが、そこまで使用頻度が高くないと結局忘れてしまうので、無理にはお勧めしません。

例えば

1 nanoha
2 fate
3 hayate

1 "nanoha"
2 "fate"
3 "hayate"

のように置換したい場合、10行程度であればメモ帳+手作業でもOKですが、10000行ある場合は何らかの方法で自動化せざるをえません。テキストエディタ正規表現を使えばお茶の子さいさいですが、この場合だったら正規表現を知らなくてもエクセルを使ったセル間の文字列結合(not VBA)でも実現することができます。適材適所で必要な知識を身につけましょう。無理強いダメゼッタイ。


さて、正規表現の話。主にjavascriptを使っていきます。

正規表現リテラル問題

javascript正規表現のマッチングを行うときにまず正規表現オブジェクトを生成する必要があります。リテラルを使うかnew演算子によって生成します。

var pattern = /nanoha/;
var pattern = new RegExp("nanoha");

この2つは等価……と思いきやそうでもない

JavaScript正規表現メモ。 - こせきの技術日記
Firefox正規表現リテラルは、状態を持っている。(なにそれ こわい)

gフラグを使うときは new RegExp(); を使おう
ただし、エスケープが面倒なので new RegExp(/nanoha/); がお勧め

正規表現エスケープだるい問題

\1,980 にマッチする正規表現

/\\1,980/
//or
new RegExp("\\\\1,980")

文字列リテラルで\を表現するためにエスケープが必要で、正規表現の中で\を表現するためにさらにエスケープが必要になります。言語によってはさらに恐ろしいことに…。ユーザ入力などの動的な文字列から正規表現オブジェクトを作ることは少ないと思うので、javascriptの場合は基本的にリテラルを使うのが良いです。

文字クラス覚えられない問題

  • [...] 角括弧内の任意の一文字
  • [^...] 角括弧内の文字以外の任意の一文字
  • . 改行以外の文字( [^\n]と同じ )
  • \s 任意のUnicode空白文字(Unicodeがミソで全角空白も含む)

以下略、僕は文字クラスをよく忘れるので基本的に角括弧内で明示的に文字種を指定してます。

どこまでマッチするの問題

繰り返しの指定。{n,m}はほとんど使ったことないですね。
たいがい + か * か ? で事足ります。

  • a{2,3} aが2個または3個マッチする
  • a{2,} aが2個以上マッチする
  • a{3} aがちょうど3個マッチする
  • a? aが0個か1個マッチする ({0,1}と同じ)
  • a+ aが1個以上マッチする ({1,}と同じ)
  • a* が0個以上マッチする ({0,}と同じ)
  • aは任意の文字(任意の正規表現

たまにひっかかるのが /a+//a+?/ の違い
わかりやすい例で言うと //// の違い

/<a.*>/.exec("<a href='nanoha.com'>nanoha chuchu</a>")

/<a.*?>/.exec("<a href='nanoha.com'>nanoha chuchu</a>")

正規表現の繰り返しは基本的に文字を消費できる限り続けようとします。上の場合は /.*/ によって最後の "< /a>" の "a" までマッチし続けます。一方、/.*?/ にすると文字を消費せずに次のパターン(ここでは />/)に進める場合は移動します。

括弧が多くてわけわからん問題

正規表現中の括弧には3つの役割があります。下の2つは一緒と言えば一緒ですが、正規表現上からの参照とプログラムからの参照で違う気がしたので分けました。

  • 正規表現のグループ化
  • マッチした文字列の後方参照(前方参照とも)
  • マッチした文字列のキャプチャ
正規表現のグループ化
/(nanoha |fate )*peropero/
//=> "nanoha nanoha fate fate nanoha peropero" にマッチ
マッチした文字列の後方参照
/(.+?)かわいいよ\1/
//=> "なのはかわいいよなのは" にマッチ
マッチした文字列のキャプチャ
/(.+?)かわいいよ\1/
//=> "なのはかわいいよなのは" にマッチ
// プログラムからは"なのはかわいいよなのは"と"なのは"を取得できる
グループ化したいけどキャプチャはいらない場合
/(?:nanoha |fate )*peropero/
//=> "なのはかわいいよなのは" にマッチ
// プログラムからは"なのはかわいいよなのは"だけ取得できる

先読みってなんじゃらほい

正規表現の能力アップさせるための拡張です。
まるなげー
正規表現の先読みについて解説してみる - (rubikitch loves (Emacs Ruby CUI Books))

否定先読みの機能は否定が苦手な正規表現にとってありがたい機能ですが、肯定先読みの使いどころがいまいちよくわからないんですよね。

/foo(?=.+1)b/
//=> "foobar1" にマッチして "foob" をキャプチャ

下の正規表現で同じことができる気がします。誰か使いどころの違いとか便利な使い方を教えてください。

/(foob).*1/
//=> "foobar1" にマッチして "foobar1" と "foob" をキャプチャ

後読み(戻り読み)ってのもありますが、Javascriptでは使えないようです。

JSでの正規表現の使用例はここのサイトを見ましょう。
JavaScript正規表現メモ。 - こせきの技術日記
/hoge/.test(string) は使いやすくていいですね。
pythonにも欲しいなぁー

test, exec, matchで大概事足りますが、マッチした位置も欲しいという場合があるので、そういうときは順繰りループを回しましょう。gフラグ付きの正規表現オブジェクトにexecすると、前回マッチした位置を記憶するので、ループで順番に取り出すことができます。定型文ですね。

var pattern = new RegExp(/peropero/g);
var text = "nanoha peropero 3peropero fate peropero 6peropero";
var result;
while((result = pattern.exec(text)) != null) {
  alert("Matched '" + result[0] + "'" + "at " +
    result.index + "; next at " + pattern.lastIndex);
}

正規表現どこにマッチする問題

正規表現自体は言語を問わずだいたい一緒(先読み後読みなどの拡張の違いはありますが)ですが、各言語やライブラリで用意している関数で微妙に挙動が違ったりするので注意しましょう。

javascriptの場合
"piyohoge".match(/hoge/)
// => "hoge" にマッチ
pythonの場合
import re
re.match("hoge", "piyohoge")
# => マッチしない
re.search("hoge", "piyohoge")
# => "hoge"にマッチ
# matchは pattern が先頭で一致しなければ一致しない
findコマンドの場合

カレントディレクトリにpiyohoge.txtファイルがある場合

find . -regex "hoge"
# => ヒットしない
find . -regex ".+hoge.+"
# => ./piyhoge.txtがヒット
# ファイル名全体にマッチする正規表現じゃないとヒットしない

なのはぺろぺろ
というわけでおしまい。